@dudousxd/nestjs-codegen 0.4.1 → 0.5.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.cjs CHANGED
@@ -160,6 +160,19 @@ function applyDefaults(userConfig, cwd) {
160
160
  enabled: userConfig.forms?.enabled ?? true,
161
161
  watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
162
162
  zodImport: userConfig.forms?.zodImport ?? "zod"
163
+ },
164
+ openapi: {
165
+ enabled: userConfig.openapi?.enabled ?? false,
166
+ fileName: userConfig.openapi?.fileName ?? "openapi.json",
167
+ title: userConfig.openapi?.title ?? "NestJS API",
168
+ version: userConfig.openapi?.version ?? "1.0.0",
169
+ description: userConfig.openapi?.description ?? null
170
+ },
171
+ mocks: {
172
+ enabled: userConfig.mocks?.enabled ?? false,
173
+ fileName: userConfig.mocks?.fileName ?? "mocks.ts",
174
+ seed: userConfig.mocks?.seed ?? 1,
175
+ baseUrl: userConfig.mocks?.baseUrl ?? ""
163
176
  }
164
177
  };
165
178
  }
@@ -197,8 +210,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
197
210
  }
198
211
 
199
212
  // src/generate.ts
200
- var import_promises9 = require("fs/promises");
201
- var import_node_path10 = require("path");
213
+ var import_promises11 = require("fs/promises");
214
+ var import_node_path12 = require("path");
202
215
 
203
216
  // src/discovery/pages.ts
204
217
  var import_promises2 = require("fs/promises");
@@ -727,17 +740,28 @@ function emitFilterQueryType(c) {
727
740
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
728
741
  }
729
742
  function buildResponseType(c, outDir) {
743
+ const respRef = c.contractSource.responseRef;
744
+ if (c.contractSource.stream) {
745
+ if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
746
+ return c.contractSource.response;
747
+ }
730
748
  if (c.controllerRef) {
731
749
  let relPath = (0, import_node_path4.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
732
750
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
733
751
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
734
752
  }
735
- const respRef = c.contractSource.responseRef;
736
753
  if (respRef) {
737
754
  return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
738
755
  }
739
756
  return c.contractSource.response;
740
757
  }
758
+ function buildErrorType(c) {
759
+ const errRef = c.contractSource.errorRef;
760
+ if (errRef) {
761
+ return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
762
+ }
763
+ return c.contractSource.error ?? "unknown";
764
+ }
741
765
  function emitRouterTypeBlock(tree, indent, outDir) {
742
766
  const pad = " ".repeat(indent);
743
767
  const lines = [];
@@ -752,12 +776,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
752
776
  const bodyRef = c.contractSource.bodyRef;
753
777
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
754
778
  const response = buildResponseType(c, outDir);
779
+ const error = buildErrorType(c);
755
780
  const params = buildParamsType(c.params);
756
781
  const safeMethod = JSON.stringify(method);
757
782
  const safeUrl = JSON.stringify(c.path);
758
783
  const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
784
+ const stream = c.contractSource.stream ? "true" : "false";
759
785
  lines.push(
760
- `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
786
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
761
787
  );
762
788
  } else {
763
789
  lines.push(`${pad}${objKey}: {`);
@@ -829,15 +855,21 @@ function emitReqHelper() {
829
855
  ""
830
856
  ];
831
857
  }
832
- function renderLeaf(pad, objKey, req, requestExpr, members) {
858
+ function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
833
859
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
834
860
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
861
+ if (streamExpr) {
862
+ lines.push(`${pad} stream: () => ${streamExpr},`);
863
+ }
835
864
  for (const [name, value] of Object.entries(members)) {
836
865
  lines.push(`${pad} ${name}: ${value},`);
837
866
  }
838
867
  lines.push(`${pad}}),`);
839
868
  return lines;
840
869
  }
870
+ function renderStreamExpr(req) {
871
+ return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
872
+ }
841
873
  function emitApiObjectBlock(tree, indent, p) {
842
874
  const pad = " ".repeat(indent);
843
875
  const lines = [];
@@ -872,7 +904,8 @@ function emitApiObjectBlock(tree, indent, p) {
872
904
  }
873
905
  const members = {};
874
906
  for (const [name, { value }] of owned) members[name] = value;
875
- lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
907
+ const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
908
+ lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
876
909
  }
877
910
  return lines;
878
911
  }
@@ -910,6 +943,8 @@ var ROUTE_NAMESPACE = [
910
943
  ' export type Params<K extends string> = ResolveByName<K, "params">;',
911
944
  ' export type Error<K extends string> = ResolveByName<K, "error">;',
912
945
  ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
946
+ " /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
947
+ ' export type Stream<K extends string> = ResolveByName<K, "response">;',
913
948
  " export type Request<K extends string> = {",
914
949
  " body: Body<K>;",
915
950
  " query: Query<K>;",
@@ -926,6 +961,7 @@ var PATH_NAMESPACE = [
926
961
  ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
927
962
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
928
963
  ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
964
+ ' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
929
965
  "}",
930
966
  ""
931
967
  ];
@@ -937,6 +973,7 @@ var EMPTY_ROUTE_NAMESPACE = [
937
973
  " export type Params<K extends string> = never;",
938
974
  " export type Error<K extends string> = never;",
939
975
  " export type FilterFields<K extends string> = never;",
976
+ " export type Stream<K extends string> = never;",
940
977
  " export type Request<K extends string> = { body: never; query: never; params: never };",
941
978
  "}",
942
979
  ""
@@ -949,6 +986,7 @@ var EMPTY_PATH_NAMESPACE = [
949
986
  " export type Params<M extends string, U extends string> = never;",
950
987
  " export type Error<M extends string, U extends string> = never;",
951
988
  " export type FilterFields<M extends string, U extends string> = never;",
989
+ " export type Stream<M extends string, U extends string> = never;",
952
990
  "}",
953
991
  ""
954
992
  ];
@@ -972,7 +1010,7 @@ function buildApiFile(routes, outDir, opts = {}) {
972
1010
  for (const r of contracted) {
973
1011
  const cs = r.contract?.contractSource;
974
1012
  if (!cs) continue;
975
- const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
1013
+ const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
976
1014
  for (const ref of refs) {
977
1015
  if (!ref) continue;
978
1016
  let names = importsByFile.get(ref.filePath);
@@ -1405,11 +1443,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1405
1443
  await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
1406
1444
  }
1407
1445
 
1408
- // src/emit/emit-pages.ts
1446
+ // src/emit/emit-mocks.ts
1409
1447
  var import_promises7 = require("fs/promises");
1410
1448
  var import_node_path8 = require("path");
1411
- async function emitPages(pages, outDir, _options = {}) {
1449
+
1450
+ // src/ir/schema-node-to-json-schema.ts
1451
+ var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
1452
+ function parseLiteral(raw) {
1453
+ const t = raw.trim();
1454
+ if (t === "true") return true;
1455
+ if (t === "false") return false;
1456
+ if (t === "null") return null;
1457
+ const q = t[0];
1458
+ if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
1459
+ return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
1460
+ }
1461
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
1462
+ return Number(t);
1463
+ }
1464
+ return t;
1465
+ }
1466
+ function literalsType(values) {
1467
+ const types = new Set(values.map((v) => v === null ? "null" : typeof v));
1468
+ if (types.size === 1) {
1469
+ const only = [...types][0];
1470
+ if (only === "string") return "string";
1471
+ if (only === "number") return "number";
1472
+ if (only === "boolean") return "boolean";
1473
+ }
1474
+ return void 0;
1475
+ }
1476
+ function convert(node, ctx) {
1477
+ switch (node.kind) {
1478
+ case "string": {
1479
+ const out = { type: "string" };
1480
+ for (const c of node.checks) {
1481
+ if (c.check === "email") out.format = "email";
1482
+ else if (c.check === "url") out.format = "uri";
1483
+ else if (c.check === "uuid") out.format = "uuid";
1484
+ else if (c.check === "min") out.minLength = Number(c.value);
1485
+ else if (c.check === "max") out.maxLength = Number(c.value);
1486
+ else if (c.check === "regex") {
1487
+ const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
1488
+ out.pattern = m ? m[1] : c.pattern;
1489
+ }
1490
+ }
1491
+ return out;
1492
+ }
1493
+ case "number": {
1494
+ const out = { type: "number" };
1495
+ for (const c of node.checks) {
1496
+ if (c.check === "int") out.type = "integer";
1497
+ else if (c.check === "min") out.minimum = Number(c.value);
1498
+ else if (c.check === "max") out.maximum = Number(c.value);
1499
+ else if (c.check === "positive") out.exclusiveMinimum = 0;
1500
+ else if (c.check === "negative") out.exclusiveMaximum = 0;
1501
+ }
1502
+ return out;
1503
+ }
1504
+ case "boolean":
1505
+ return { type: "boolean" };
1506
+ case "date":
1507
+ return { type: "string", format: "date-time" };
1508
+ case "unknown":
1509
+ return node.note ? { description: node.note } : {};
1510
+ case "instanceof":
1511
+ return { type: "object", description: `instanceof ${node.ctor}` };
1512
+ case "enum": {
1513
+ const values = node.literals.map(parseLiteral);
1514
+ const t = literalsType(values);
1515
+ const out = { enum: values };
1516
+ if (t) out.type = t;
1517
+ return out;
1518
+ }
1519
+ case "literal": {
1520
+ const value = parseLiteral(node.raw);
1521
+ const out = { const: value };
1522
+ const t = literalsType([value]);
1523
+ if (t) out.type = t;
1524
+ return out;
1525
+ }
1526
+ case "union": {
1527
+ const options = node.options.map((o) => convert(o, ctx));
1528
+ const out = { oneOf: options };
1529
+ if (node.discriminator) {
1530
+ out.discriminator = { propertyName: node.discriminator };
1531
+ }
1532
+ return out;
1533
+ }
1534
+ case "object": {
1535
+ const properties = {};
1536
+ const required = [];
1537
+ for (const f of node.fields) {
1538
+ if (f.value.kind === "optional") {
1539
+ properties[f.key] = convert(f.value.inner, ctx);
1540
+ } else {
1541
+ properties[f.key] = convert(f.value, ctx);
1542
+ required.push(f.key);
1543
+ }
1544
+ }
1545
+ const out = { type: "object", properties };
1546
+ if (required.length > 0) out.required = required;
1547
+ out.additionalProperties = node.passthrough;
1548
+ return out;
1549
+ }
1550
+ case "array":
1551
+ return { type: "array", items: convert(node.element, ctx) };
1552
+ case "optional":
1553
+ return widenNullable(convert(node.inner, ctx));
1554
+ case "ref":
1555
+ case "lazyRef":
1556
+ return { $ref: `${ctx.refPrefix}${node.name}` };
1557
+ case "annotated":
1558
+ return convert(node.inner, ctx);
1559
+ }
1560
+ }
1561
+ function widenNullable(schema) {
1562
+ if (schema.$ref) {
1563
+ return { anyOf: [schema, { type: "null" }] };
1564
+ }
1565
+ if (typeof schema.type === "string") {
1566
+ return { ...schema, type: [schema.type, "null"] };
1567
+ }
1568
+ if (Array.isArray(schema.type)) {
1569
+ return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
1570
+ }
1571
+ return { anyOf: [schema, { type: "null" }] };
1572
+ }
1573
+ function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
1574
+ const named = {};
1575
+ for (const [name, node] of mod.named) {
1576
+ named[name] = convert(node, ctx);
1577
+ }
1578
+ return { root: convert(mod.root, ctx), named };
1579
+ }
1580
+
1581
+ // src/emit/mock-gen-runtime.ts
1582
+ var MOCK_GEN_RUNTIME = `
1583
+ /** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
1584
+ function makeRng(seed) {
1585
+ let a = seed >>> 0;
1586
+ return {
1587
+ next() {
1588
+ a |= 0;
1589
+ a = (a + 0x6d2b79f5) | 0;
1590
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
1591
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
1592
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
1593
+ },
1594
+ };
1595
+ }
1596
+
1597
+ function __pick(rng, items) {
1598
+ return items[Math.floor(rng.next() * items.length)];
1599
+ }
1600
+
1601
+ function __intBetween(rng, min, max) {
1602
+ return Math.floor(rng.next() * (max - min + 1)) + min;
1603
+ }
1604
+
1605
+ const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
1606
+ const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
1607
+ const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
1608
+
1609
+ function __fakeWords(rng, count) {
1610
+ let out = [];
1611
+ for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
1612
+ return out.join(' ');
1613
+ }
1614
+
1615
+ function __hex(rng, len) {
1616
+ let s = '';
1617
+ for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
1618
+ return s;
1619
+ }
1620
+
1621
+ function __fakeUuid(rng) {
1622
+ return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
1623
+ }
1624
+
1625
+ function __fakeString(rng, schema) {
1626
+ switch (schema.format) {
1627
+ case 'email':
1628
+ return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
1629
+ case 'uri':
1630
+ case 'url':
1631
+ return 'https://example.com/' + __pick(rng, __WORDS);
1632
+ case 'uuid':
1633
+ return __fakeUuid(rng);
1634
+ case 'date-time':
1635
+ return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
1636
+ default:
1637
+ return __fakeWords(rng, __intBetween(rng, 1, 3));
1638
+ }
1639
+ }
1640
+
1641
+ /** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
1642
+ function generateMock(schema, rng, defs, depth) {
1643
+ defs = defs || {};
1644
+ depth = depth || 0;
1645
+ if (schema.$ref) {
1646
+ const name = schema.$ref.replace('#/components/schemas/', '');
1647
+ const target = defs[name];
1648
+ if (!target || depth > 4) return null;
1649
+ return generateMock(target, rng, defs, depth + 1);
1650
+ }
1651
+ if ('const' in schema) return schema.const;
1652
+ if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
1653
+ if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
1654
+ if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
1655
+ let type = Array.isArray(schema.type)
1656
+ ? (schema.type.filter((t) => t !== 'null')[0] || 'null')
1657
+ : schema.type;
1658
+ switch (type) {
1659
+ case 'string':
1660
+ return __fakeString(rng, schema);
1661
+ case 'integer':
1662
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
1663
+ case 'number':
1664
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
1665
+ case 'boolean':
1666
+ return rng.next() < 0.5;
1667
+ case 'null':
1668
+ return null;
1669
+ case 'array': {
1670
+ const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
1671
+ const items = schema.items || {};
1672
+ let arr = [];
1673
+ for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
1674
+ return arr;
1675
+ }
1676
+ case 'object': {
1677
+ const out = {};
1678
+ const props = schema.properties || {};
1679
+ for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
1680
+ return out;
1681
+ }
1682
+ default:
1683
+ return {};
1684
+ }
1685
+ }
1686
+ `.trim();
1687
+
1688
+ // src/emit/emit-mocks.ts
1689
+ var REF_PREFIX = "#/components/schemas/";
1690
+ function toMswPath(path, baseUrl) {
1691
+ return `${baseUrl}${path}`;
1692
+ }
1693
+ function responseSchemaFor(route, defs) {
1694
+ const cs = route.contract.contractSource;
1695
+ if (cs.responseSchema) {
1696
+ const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
1697
+ for (const [name, node] of Object.entries(named)) {
1698
+ if (!(name in defs)) defs[name] = node;
1699
+ }
1700
+ return root;
1701
+ }
1702
+ return {};
1703
+ }
1704
+ function buildMocksFile(routes, opts = {}) {
1705
+ const seed = opts.seed ?? 1;
1706
+ const baseUrl = opts.baseUrl ?? "";
1707
+ const contracted = routes.filter((r) => r.contract);
1708
+ const defs = {};
1709
+ const handlers = [];
1710
+ for (const r of contracted) {
1711
+ const schema = responseSchemaFor(r, defs);
1712
+ const method = r.method.toLowerCase();
1713
+ const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
1714
+ const path = toMswPath(r.path, baseUrl);
1715
+ const cs = r.contract.contractSource;
1716
+ const schemaLiteral = JSON.stringify(schema);
1717
+ const pathLit = JSON.stringify(path);
1718
+ if (cs.stream) {
1719
+ handlers.push(
1720
+ [
1721
+ ` // ${r.name} (stream)`,
1722
+ ` http.${mswMethod}(${pathLit}, () => {`,
1723
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1724
+ " const body = `data: ${JSON.stringify(value)}\\n\\n`;",
1725
+ " return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
1726
+ " }),"
1727
+ ].join("\n")
1728
+ );
1729
+ } else {
1730
+ handlers.push(
1731
+ [
1732
+ ` // ${r.name}`,
1733
+ ` http.${mswMethod}(${pathLit}, () => {`,
1734
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1735
+ " return HttpResponse.json(value);",
1736
+ " }),"
1737
+ ].join("\n")
1738
+ );
1739
+ }
1740
+ }
1741
+ const lines = [
1742
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1743
+ "// MSW handlers returning deterministic, schema-shaped mock data.",
1744
+ "/* eslint-disable */",
1745
+ "// @ts-nocheck",
1746
+ "",
1747
+ "import { http, HttpResponse } from 'msw';",
1748
+ "",
1749
+ `const SEED = ${seed};`,
1750
+ "",
1751
+ "// ---------------------------------------------------------------------------",
1752
+ "// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
1753
+ "// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
1754
+ "// ---------------------------------------------------------------------------",
1755
+ MOCK_GEN_RUNTIME,
1756
+ "",
1757
+ "// Shared component schemas referenced by $ref.",
1758
+ `const DEFS = ${JSON.stringify(defs, null, 2)};`,
1759
+ "",
1760
+ "/** MSW request handlers, one per contracted route. */",
1761
+ "export const handlers = [",
1762
+ ...handlers,
1763
+ "];",
1764
+ ""
1765
+ ];
1766
+ return lines.join("\n");
1767
+ }
1768
+ async function emitMocks(routes, outDir, opts = {}) {
1412
1769
  await (0, import_promises7.mkdir)(outDir, { recursive: true });
1770
+ const content = buildMocksFile(routes, opts);
1771
+ const fileName = opts.fileName ?? "mocks.ts";
1772
+ await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, fileName), content, "utf8");
1773
+ }
1774
+
1775
+ // src/emit/emit-openapi.ts
1776
+ var import_promises8 = require("fs/promises");
1777
+ var import_node_path9 = require("path");
1778
+ var REF_PREFIX2 = "#/components/schemas/";
1779
+ function toOpenApiPath(path) {
1780
+ return path.replace(/:([^/]+)/g, "{$1}");
1781
+ }
1782
+ function positionSchema(schema, tsType, components) {
1783
+ if (schema) {
1784
+ const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
1785
+ for (const [name, node] of Object.entries(named)) {
1786
+ if (!(name in components)) components[name] = node;
1787
+ }
1788
+ return root;
1789
+ }
1790
+ return tsType ? { description: tsType } : {};
1791
+ }
1792
+ function buildParameters(route) {
1793
+ const params = [];
1794
+ for (const p of route.params) {
1795
+ if (p.source === "path") {
1796
+ params.push({
1797
+ name: p.name,
1798
+ in: "path",
1799
+ required: true,
1800
+ schema: { type: "string" }
1801
+ });
1802
+ } else if (p.source === "query") {
1803
+ params.push({
1804
+ name: p.name,
1805
+ in: "query",
1806
+ required: false,
1807
+ schema: { type: "string" }
1808
+ });
1809
+ } else if (p.source === "header") {
1810
+ params.push({
1811
+ name: p.name,
1812
+ in: "header",
1813
+ required: false,
1814
+ schema: { type: "string" }
1815
+ });
1816
+ }
1817
+ }
1818
+ return params;
1819
+ }
1820
+ function buildResponses(cs, components) {
1821
+ const responses = {};
1822
+ const successSchema = positionSchema(
1823
+ // Prefer rich response IR when present; otherwise fall back to the TS type.
1824
+ cs.responseSchema ?? null,
1825
+ cs.response,
1826
+ components
1827
+ );
1828
+ const successContentType = cs.stream ? "text/event-stream" : "application/json";
1829
+ responses["200"] = {
1830
+ description: cs.stream ? "Server-sent event stream" : "Successful response",
1831
+ content: { [successContentType]: { schema: successSchema } }
1832
+ };
1833
+ const errorSchema = positionSchema(null, cs.error ?? null, components);
1834
+ const errorBody = {
1835
+ description: "Error response",
1836
+ content: { "application/json": { schema: errorSchema } }
1837
+ };
1838
+ if (cs.error || cs.errorRef) {
1839
+ responses["400"] = errorBody;
1840
+ responses.default = errorBody;
1841
+ } else {
1842
+ responses.default = {
1843
+ description: "Error response",
1844
+ content: { "application/json": { schema: {} } }
1845
+ };
1846
+ }
1847
+ return responses;
1848
+ }
1849
+ function buildOperation(route, components) {
1850
+ const cs = route.contract.contractSource;
1851
+ const op = {
1852
+ operationId: route.name,
1853
+ parameters: buildParameters(route),
1854
+ responses: buildResponses(cs, components)
1855
+ };
1856
+ const method = route.method.toUpperCase();
1857
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
1858
+ if (hasBody && (cs.bodySchema || cs.body)) {
1859
+ const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
1860
+ op.requestBody = {
1861
+ required: true,
1862
+ content: { "application/json": { schema: bodySchema } }
1863
+ };
1864
+ }
1865
+ return op;
1866
+ }
1867
+ function buildOpenApiSpec(routes, opts = {}) {
1868
+ const components = {};
1869
+ const paths = {};
1870
+ for (const route of routes) {
1871
+ if (!route.contract) continue;
1872
+ const oaPath = toOpenApiPath(route.path);
1873
+ const method = route.method.toLowerCase();
1874
+ let pathItem = paths[oaPath];
1875
+ if (!pathItem) {
1876
+ pathItem = {};
1877
+ paths[oaPath] = pathItem;
1878
+ }
1879
+ pathItem[method] = buildOperation(route, components);
1880
+ }
1881
+ const info = opts.info ?? {};
1882
+ const doc = {
1883
+ openapi: "3.1.0",
1884
+ info: {
1885
+ title: info.title ?? "NestJS API",
1886
+ version: info.version ?? "1.0.0",
1887
+ ...info.description ? { description: info.description } : {}
1888
+ },
1889
+ paths,
1890
+ components: { schemas: components }
1891
+ };
1892
+ return doc;
1893
+ }
1894
+ async function emitOpenApi(routes, outDir, opts = {}) {
1895
+ await (0, import_promises8.mkdir)(outDir, { recursive: true });
1896
+ const doc = buildOpenApiSpec(routes, opts);
1897
+ const fileName = opts.fileName ?? "openapi.json";
1898
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, fileName), `${JSON.stringify(doc, null, 2)}
1899
+ `, "utf8");
1900
+ }
1901
+
1902
+ // src/emit/emit-pages.ts
1903
+ var import_promises9 = require("fs/promises");
1904
+ var import_node_path10 = require("path");
1905
+ async function emitPages(pages, outDir, _options = {}) {
1906
+ await (0, import_promises9.mkdir)(outDir, { recursive: true });
1413
1907
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1414
1908
  const augBody = pages.map((p) => {
1415
1909
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
@@ -1428,7 +1922,7 @@ ${augBody}
1428
1922
  }
1429
1923
  ${sharedPropsBlock}}
1430
1924
  `;
1431
- await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, "pages.d.ts"), content, "utf8");
1925
+ await (0, import_promises9.writeFile)((0, import_node_path10.join)(outDir, "pages.d.ts"), content, "utf8");
1432
1926
  }
1433
1927
  function buildSharedPropsBlock(sharedProps) {
1434
1928
  if (!sharedProps) return "";
@@ -1447,7 +1941,7 @@ ${propsBody}
1447
1941
  `;
1448
1942
  }
1449
1943
  function buildAugmentationType(page, outDir) {
1450
- let importPath = (0, import_node_path8.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1944
+ let importPath = (0, import_node_path10.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1451
1945
  if (!importPath.startsWith(".")) {
1452
1946
  importPath = `./${importPath}`;
1453
1947
  }
@@ -1458,12 +1952,12 @@ function needsQuotes(name) {
1458
1952
  }
1459
1953
 
1460
1954
  // src/emit/emit-routes.ts
1461
- var import_promises8 = require("fs/promises");
1462
- var import_node_path9 = require("path");
1955
+ var import_promises10 = require("fs/promises");
1956
+ var import_node_path11 = require("path");
1463
1957
  async function emitRoutes(routes, outDir) {
1464
- await (0, import_promises8.mkdir)(outDir, { recursive: true });
1958
+ await (0, import_promises10.mkdir)(outDir, { recursive: true });
1465
1959
  const content = buildRoutesFile(routes);
1466
- await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, "routes.ts"), content, "utf8");
1960
+ await (0, import_promises10.writeFile)((0, import_node_path11.join)(outDir, "routes.ts"), content, "utf8");
1467
1961
  }
1468
1962
  function buildRoutesFile(routes) {
1469
1963
  if (routes.length === 0) {
@@ -1611,24 +2105,41 @@ async function generate(config, inputRoutes = []) {
1611
2105
  });
1612
2106
  }
1613
2107
  const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
2108
+ if (hasContracts && config.openapi.enabled) {
2109
+ await emitOpenApi(routes, config.codegen.outDir, {
2110
+ fileName: config.openapi.fileName,
2111
+ info: {
2112
+ title: config.openapi.title,
2113
+ version: config.openapi.version,
2114
+ ...config.openapi.description ? { description: config.openapi.description } : {}
2115
+ }
2116
+ });
2117
+ }
2118
+ if (hasContracts && config.mocks.enabled) {
2119
+ await emitMocks(routes, config.codegen.outDir, {
2120
+ fileName: config.mocks.fileName,
2121
+ seed: config.mocks.seed,
2122
+ baseUrl: config.mocks.baseUrl
2123
+ });
2124
+ }
1614
2125
  await emitIndex(config.codegen.outDir, hasContracts, hasForms);
1615
2126
  if (extensions.length > 0) {
1616
2127
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1617
2128
  for (const file of extraFiles) {
1618
- const dest = (0, import_node_path10.join)(config.codegen.outDir, file.path);
1619
- await (0, import_promises9.mkdir)((0, import_node_path10.dirname)(dest), { recursive: true });
1620
- await (0, import_promises9.writeFile)(dest, file.contents, "utf8");
2129
+ const dest = (0, import_node_path12.join)(config.codegen.outDir, file.path);
2130
+ await (0, import_promises11.mkdir)((0, import_node_path12.dirname)(dest), { recursive: true });
2131
+ await (0, import_promises11.writeFile)(dest, file.contents, "utf8");
1621
2132
  }
1622
2133
  }
1623
2134
  }
1624
2135
 
1625
2136
  // src/watch/watcher.ts
1626
- var import_promises12 = require("fs/promises");
1627
- var import_node_path14 = require("path");
2137
+ var import_promises14 = require("fs/promises");
2138
+ var import_node_path16 = require("path");
1628
2139
  var import_chokidar = __toESM(require("chokidar"), 1);
1629
2140
 
1630
2141
  // src/discovery/contracts-fast.ts
1631
- var import_node_path12 = require("path");
2142
+ var import_node_path14 = require("path");
1632
2143
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
1633
2144
  var import_ts_morph9 = require("ts-morph");
1634
2145
 
@@ -1640,7 +2151,7 @@ var import_ts_morph4 = require("ts-morph");
1640
2151
 
1641
2152
  // src/discovery/type-ref-resolution.ts
1642
2153
  var import_node_fs = require("fs");
1643
- var import_node_path11 = require("path");
2154
+ var import_node_path13 = require("path");
1644
2155
  var import_ts_morph3 = require("ts-morph");
1645
2156
  var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1646
2157
  var _ctxByProject = /* @__PURE__ */ new WeakMap();
@@ -1692,12 +2203,12 @@ function findTypeInFile(name, file) {
1692
2203
  }
1693
2204
  function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1694
2205
  if (moduleSpecifier.startsWith(".")) {
1695
- const dir = (0, import_node_path11.dirname)(sourceFile.getFilePath());
2206
+ const dir = (0, import_node_path13.dirname)(sourceFile.getFilePath());
1696
2207
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
1697
2208
  return [
1698
- (0, import_node_path11.resolve)(dir, `${noExt}.ts`),
1699
- (0, import_node_path11.resolve)(dir, `${moduleSpecifier}.ts`),
1700
- (0, import_node_path11.resolve)(dir, moduleSpecifier, "index.ts")
2209
+ (0, import_node_path13.resolve)(dir, `${noExt}.ts`),
2210
+ (0, import_node_path13.resolve)(dir, `${moduleSpecifier}.ts`),
2211
+ (0, import_node_path13.resolve)(dir, moduleSpecifier, "index.ts")
1701
2212
  ];
1702
2213
  }
1703
2214
  const ctx = _ctxFor(project);
@@ -1718,8 +2229,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1718
2229
  const rest = moduleSpecifier.slice(prefix.length);
1719
2230
  const candidates = [];
1720
2231
  for (const mapping of mappings) {
1721
- const resolved = (0, import_node_path11.resolve)(baseUrl, mapping.replace("*", rest));
1722
- candidates.push(`${resolved}.ts`, (0, import_node_path11.resolve)(resolved, "index.ts"));
2232
+ const resolved = (0, import_node_path13.resolve)(baseUrl, mapping.replace("*", rest));
2233
+ candidates.push(`${resolved}.ts`, (0, import_node_path13.resolve)(resolved, "index.ts"));
1723
2234
  }
1724
2235
  dbg(" resolved candidates:", candidates);
1725
2236
  return candidates;
@@ -1800,7 +2311,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
1800
2311
  }
1801
2312
  return null;
1802
2313
  }
2314
+ function resolveImportedVariable(name, sourceFile, project) {
2315
+ const local = sourceFile.getVariableDeclaration(name);
2316
+ if (local) return { decl: local, file: sourceFile };
2317
+ return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
2318
+ }
2319
+ function resolveVariableViaImports(name, sourceFile, project, seen) {
2320
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2321
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
2322
+ if (!namedImport) continue;
2323
+ const sourceName = namedImport.getName();
2324
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
2325
+ const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
2326
+ if (found) return found;
2327
+ }
2328
+ return null;
2329
+ }
2330
+ function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
2331
+ const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
2332
+ for (const candidate of candidates) {
2333
+ let importedFile = project.getSourceFile(candidate);
2334
+ if (!importedFile) {
2335
+ try {
2336
+ importedFile = project.addSourceFileAtPath(candidate);
2337
+ } catch {
2338
+ continue;
2339
+ }
2340
+ }
2341
+ const found = resolveVariableInFile(name, importedFile, project, seen);
2342
+ if (found) return found;
2343
+ }
2344
+ return null;
2345
+ }
2346
+ function resolveVariableInFile(name, file, project, seen) {
2347
+ const filePath = file.getFilePath();
2348
+ if (seen.has(filePath)) return null;
2349
+ seen.add(filePath);
2350
+ const local = file.getVariableDeclaration(name);
2351
+ if (local) return { decl: local, file };
2352
+ for (const exportDecl of file.getExportDeclarations()) {
2353
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue();
2354
+ const namedExports = exportDecl.getNamedExports();
2355
+ if (moduleSpecifier) {
2356
+ const hasStar = namedExports.length === 0;
2357
+ const reExport2 = namedExports.find(
2358
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2359
+ );
2360
+ if (!hasStar && !reExport2) continue;
2361
+ const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
2362
+ const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
2363
+ if (found) return found;
2364
+ continue;
2365
+ }
2366
+ const reExport = namedExports.find(
2367
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2368
+ );
2369
+ if (!reExport) continue;
2370
+ const sourceName = reExport.getName();
2371
+ const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
2372
+ if (viaImports) return viaImports;
2373
+ }
2374
+ return null;
2375
+ }
1803
2376
  var _findTypeCache = /* @__PURE__ */ new WeakMap();
2377
+ function clearTypeResolutionCaches(project) {
2378
+ _findTypeCache.delete(project);
2379
+ _resolveNamedRefCache.delete(project);
2380
+ }
1804
2381
  function findType(name, sourceFile, project) {
1805
2382
  let byKey = _findTypeCache.get(project);
1806
2383
  if (byKey === void 0) {
@@ -1943,7 +2520,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1943
2520
  emittedClasses: /* @__PURE__ */ new Map(),
1944
2521
  visiting: /* @__PURE__ */ new Set(),
1945
2522
  recursiveSchemas: /* @__PURE__ */ new Set(),
1946
- depth: 0
2523
+ depth: 0,
2524
+ typeBindings: /* @__PURE__ */ new Map()
1947
2525
  };
1948
2526
  const root = buildObject(classDecl, sourceFile, ctx);
1949
2527
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -1967,11 +2545,34 @@ function buildProperty(prop, classFile, ctx) {
1967
2545
  const typeNode = prop.getTypeNode();
1968
2546
  const typeText = typeNode?.getText() ?? "unknown";
1969
2547
  const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
2548
+ const discriminator = resolveDiscriminator(dec("Type"));
2549
+ if (discriminator) {
2550
+ const options = discriminator.subTypes.map(
2551
+ (name) => buildNestedReference(name, classFile, ctx)
2552
+ );
2553
+ const unionNode = {
2554
+ kind: "union",
2555
+ options,
2556
+ discriminator: discriminator.property
2557
+ };
2558
+ const wrapArray = has("IsArray") || isArrayType;
2559
+ const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
2560
+ return applyPresence(node2, decorators);
2561
+ }
2562
+ const propTypeParam = singularClassName(typeText);
2563
+ if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
2564
+ const bound = ctx.typeBindings.get(propTypeParam);
2565
+ const childNode = buildNestedReference(bound, classFile, ctx);
2566
+ const wrapArray = has("IsArray") || isArrayType;
2567
+ const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
2568
+ return applyPresence(node2, decorators);
2569
+ }
1970
2570
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1971
2571
  if (has("ValidateNested") || typeRefName) {
2572
+ const typeArgs = genericTypeArgNames(typeNode);
1972
2573
  const childName = typeRefName ?? singularClassName(typeText);
1973
2574
  if (childName) {
1974
- const childNode = buildNestedReference(childName, classFile, ctx);
2575
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
1975
2576
  const wrapArray = has("IsArray") || isArrayType;
1976
2577
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
1977
2578
  return applyPresence(node2, decorators);
@@ -2096,10 +2697,12 @@ function baseFromType(typeText, isArrayType) {
2096
2697
  return { kind: "unknown" };
2097
2698
  }
2098
2699
  }
2099
- function buildNestedReference(className, fromFile, ctx) {
2100
- if (ctx.visiting.has(className)) {
2101
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2102
- ctx.emittedClasses.set(className, reserved);
2700
+ function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
2701
+ const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
2702
+ const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
2703
+ if (ctx.visiting.has(cacheKey)) {
2704
+ const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
2705
+ ctx.emittedClasses.set(cacheKey, reserved);
2103
2706
  ctx.recursiveSchemas.add(reserved);
2104
2707
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2105
2708
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -2118,19 +2721,27 @@ function buildNestedReference(className, fromFile, ctx) {
2118
2721
  }
2119
2722
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2120
2723
  }
2121
- const existing = ctx.emittedClasses.get(className);
2724
+ const existing = ctx.emittedClasses.get(cacheKey);
2122
2725
  if (existing) return { kind: "ref", name: existing };
2123
- const schemaName = aliasFor(className, ctx);
2726
+ const schemaName = aliasFor(schemaBase, ctx);
2124
2727
  const resolved = findType(className, fromFile, ctx.project);
2125
2728
  if (!resolved || resolved.kind !== "class") {
2126
2729
  return { kind: "object", fields: [], passthrough: true };
2127
2730
  }
2128
- ctx.emittedClasses.set(className, schemaName);
2129
- ctx.visiting.add(className);
2731
+ const params = resolved.decl.getTypeParameters().map((p) => p.getName());
2732
+ const newBindings = [];
2733
+ params.forEach((param, i) => {
2734
+ const arg = typeArgs[i];
2735
+ if (arg) newBindings.push([param, arg]);
2736
+ });
2737
+ for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
2738
+ ctx.emittedClasses.set(cacheKey, schemaName);
2739
+ ctx.visiting.add(cacheKey);
2130
2740
  ctx.depth += 1;
2131
2741
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
2132
2742
  ctx.depth -= 1;
2133
- ctx.visiting.delete(className);
2743
+ ctx.visiting.delete(cacheKey);
2744
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
2134
2745
  ctx.named.set(schemaName, childNode);
2135
2746
  return { kind: "ref", name: schemaName };
2136
2747
  }
@@ -2177,6 +2788,39 @@ function messageRaw(decorator) {
2177
2788
  }
2178
2789
  return void 0;
2179
2790
  }
2791
+ function resolveDiscriminator(decorator) {
2792
+ const optsArg = decorator?.getArguments()[1];
2793
+ if (!optsArg || !import_ts_morph4.Node.isObjectLiteralExpression(optsArg)) return null;
2794
+ let discProp;
2795
+ for (const prop of optsArg.getProperties()) {
2796
+ if (import_ts_morph4.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
2797
+ discProp = prop.getInitializer();
2798
+ }
2799
+ }
2800
+ if (!discProp || !import_ts_morph4.Node.isObjectLiteralExpression(discProp)) return null;
2801
+ let property = null;
2802
+ const subTypes = [];
2803
+ for (const prop of discProp.getProperties()) {
2804
+ if (!import_ts_morph4.Node.isPropertyAssignment(prop)) continue;
2805
+ const name = prop.getName();
2806
+ const init = prop.getInitializer();
2807
+ if (!init) continue;
2808
+ if (name === "property" && import_ts_morph4.Node.isStringLiteral(init)) {
2809
+ property = init.getLiteralValue();
2810
+ } else if (name === "subTypes" && import_ts_morph4.Node.isArrayLiteralExpression(init)) {
2811
+ for (const el of init.getElements()) {
2812
+ if (!import_ts_morph4.Node.isObjectLiteralExpression(el)) continue;
2813
+ for (const p of el.getProperties()) {
2814
+ if (!import_ts_morph4.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
2815
+ const nameInit = p.getInitializer();
2816
+ if (nameInit && import_ts_morph4.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
2817
+ }
2818
+ }
2819
+ }
2820
+ }
2821
+ if (!property || subTypes.length === 0) return null;
2822
+ return { property, subTypes };
2823
+ }
2180
2824
  function resolveTypeFactoryName(decorator) {
2181
2825
  const arg = firstArg(decorator);
2182
2826
  if (!arg) return null;
@@ -2190,6 +2834,17 @@ function singularClassName(typeText) {
2190
2834
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2191
2835
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2192
2836
  }
2837
+ function genericTypeArgNames(typeNode) {
2838
+ if (!typeNode || !import_ts_morph4.Node.isTypeReference(typeNode)) return [];
2839
+ const names = [];
2840
+ for (const arg of typeNode.getTypeArguments()) {
2841
+ if (!import_ts_morph4.Node.isTypeReference(arg)) return [];
2842
+ const tn = arg.getTypeName();
2843
+ if (!import_ts_morph4.Node.isIdentifier(tn)) return [];
2844
+ names.push(tn.getText());
2845
+ }
2846
+ return names;
2847
+ }
2193
2848
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2194
2849
  const arg = firstArg(decorator);
2195
2850
  if (!arg) return null;
@@ -2244,6 +2899,9 @@ var import_ts_morph5 = require("ts-morph");
2244
2899
 
2245
2900
  // src/discovery/enum-resolution.ts
2246
2901
  var _enumCache = /* @__PURE__ */ new WeakMap();
2902
+ function clearEnumCache(project) {
2903
+ _enumCache.delete(project);
2904
+ }
2247
2905
  function resolveEnumValues(name, sourceFile, project) {
2248
2906
  let byKey = _enumCache.get(project);
2249
2907
  if (byKey === void 0) {
@@ -2712,24 +3370,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2712
3370
  "Map",
2713
3371
  "Set"
2714
3372
  ]);
2715
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3373
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2716
3374
  if (depth <= 0) return "unknown";
2717
3375
  if (import_ts_morph7.Node.isArrayTypeNode(typeNode)) {
2718
3376
  const elementType = typeNode.getElementTypeNode();
2719
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3377
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
2720
3378
  }
2721
3379
  if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
2722
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3380
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
2723
3381
  }
2724
3382
  if (import_ts_morph7.Node.isIntersectionTypeNode(typeNode)) {
2725
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3383
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
2726
3384
  }
2727
3385
  if (import_ts_morph7.Node.isParenthesizedTypeNode(typeNode)) {
2728
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3386
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
2729
3387
  }
2730
3388
  if (import_ts_morph7.Node.isTypeReference(typeNode)) {
2731
3389
  const typeName = typeNode.getTypeName();
2732
3390
  const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3391
+ const bound = subst.get(name);
3392
+ if (bound !== void 0) return bound;
2733
3393
  if (name === "string" || name === "number" || name === "boolean") return name;
2734
3394
  if (name === "Date") return "string";
2735
3395
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -2737,14 +3397,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2737
3397
  return "unknown";
2738
3398
  const wrapperMode = WRAPPER_TYPES[name];
2739
3399
  if (wrapperMode) {
2740
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3400
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
2741
3401
  }
2742
3402
  if (PASSTHROUGH_UTILITY.has(name)) {
2743
3403
  return typeNode.getText();
2744
3404
  }
2745
3405
  const resolved = findType(name, sourceFile, project);
2746
3406
  if (resolved) {
2747
- return expandTypeDecl(resolved, project, depth - 1);
3407
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
3408
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
2748
3409
  }
2749
3410
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
2750
3411
  return "unknown";
@@ -2757,32 +3418,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2757
3418
  if (kind === import_ts_morph7.SyntaxKind.AnyKeyword) return "unknown";
2758
3419
  return typeNode.getText();
2759
3420
  }
2760
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
3421
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
2761
3422
  const typeArgs = typeNode.getTypeArguments();
2762
3423
  const firstTypeArg = typeArgs[0];
2763
3424
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2764
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3425
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
2765
3426
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
2766
3427
  }
2767
3428
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2768
3429
  }
2769
- function expandTypeDecl(result, project, depth) {
3430
+ function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
3431
+ if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
3432
+ const params = result.decl.getTypeParameters().map((p) => p.getName());
3433
+ if (params.length === 0) return /* @__PURE__ */ new Map();
3434
+ const args = typeNode.getTypeArguments();
3435
+ const subst = /* @__PURE__ */ new Map();
3436
+ params.forEach((param, i) => {
3437
+ const arg = args[i];
3438
+ if (arg)
3439
+ subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
3440
+ });
3441
+ return subst;
3442
+ }
3443
+ function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
2770
3444
  if (depth < 0) return "unknown";
2771
3445
  switch (result.kind) {
2772
3446
  case "class":
2773
- return resolvePropertied(result.decl, result.file, project, depth);
3447
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2774
3448
  case "interface":
2775
- return resolvePropertied(result.decl, result.file, project, depth);
3449
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2776
3450
  case "typeAlias":
2777
3451
  if (result.typeNode) {
2778
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
3452
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
2779
3453
  }
2780
3454
  return result.text;
2781
3455
  case "enum":
2782
3456
  return result.members.join(" | ");
2783
3457
  }
2784
3458
  }
2785
- function resolvePropertied(decl, sourceFile, project, depth) {
3459
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2786
3460
  if (depth < 0) return "unknown";
2787
3461
  const lines = [];
2788
3462
  for (const prop of decl.getProperties()) {
@@ -2791,7 +3465,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
2791
3465
  const propTypeNode = prop.getTypeNode();
2792
3466
  let propType = "unknown";
2793
3467
  if (propTypeNode) {
2794
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
3468
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2795
3469
  }
2796
3470
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
2797
3471
  }
@@ -2840,7 +3514,7 @@ function extractParamsType(method, sourceFile, project) {
2840
3514
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
2841
3515
  }
2842
3516
  function extractResponseType(method, sourceFile, project) {
2843
- const apiResponseDecorator = method.getDecorator("ApiResponse");
3517
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2844
3518
  if (apiResponseDecorator) {
2845
3519
  const args = apiResponseDecorator.getArguments();
2846
3520
  const optsArg = args[0];
@@ -2869,6 +3543,59 @@ function extractResponseType(method, sourceFile, project) {
2869
3543
  }
2870
3544
  return "unknown";
2871
3545
  }
3546
+ function apiResponseStatus(decorator) {
3547
+ const optsArg = decorator.getArguments()[0];
3548
+ if (!optsArg || !import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) return null;
3549
+ for (const prop of optsArg.getProperties()) {
3550
+ if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
3551
+ if (prop.getName() !== "status") continue;
3552
+ const val = prop.getInitializer();
3553
+ if (val && import_ts_morph7.Node.isNumericLiteral(val)) return Number(val.getLiteralValue());
3554
+ }
3555
+ return null;
3556
+ }
3557
+ function apiResponseTypeNode(decorator) {
3558
+ const optsArg = decorator.getArguments()[0];
3559
+ if (!optsArg || !import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) return null;
3560
+ for (const prop of optsArg.getProperties()) {
3561
+ if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
3562
+ if (prop.getName() !== "type") continue;
3563
+ const val = prop.getInitializer();
3564
+ if (!val) return null;
3565
+ if (import_ts_morph7.Node.isArrayLiteralExpression(val)) {
3566
+ const first = val.getElements()[0];
3567
+ return first ? { node: first, isArray: true } : null;
3568
+ }
3569
+ return { node: val, isArray: false };
3570
+ }
3571
+ return null;
3572
+ }
3573
+ function extractErrorType(method, sourceFile, project) {
3574
+ for (const decorator of method.getDecorators()) {
3575
+ if (decorator.getName() !== "ApiResponse") continue;
3576
+ const status = apiResponseStatus(decorator);
3577
+ if (status === null || status < 400) continue;
3578
+ const typeInfo = apiResponseTypeNode(decorator);
3579
+ if (!typeInfo) continue;
3580
+ const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
3581
+ const type = typeInfo.isArray ? `Array<${inner}>` : inner;
3582
+ let ref = null;
3583
+ if (import_ts_morph7.Node.isIdentifier(typeInfo.node)) {
3584
+ const name = typeInfo.node.getText();
3585
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3586
+ if (localDecl?.isExported()) {
3587
+ ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
3588
+ } else {
3589
+ const resolved = resolveImportedType(name, sourceFile, project);
3590
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
3591
+ ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
3592
+ }
3593
+ }
3594
+ }
3595
+ return { type, ref };
3596
+ }
3597
+ return null;
3598
+ }
2872
3599
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
2873
3600
  if (!import_ts_morph7.Node.isIdentifier(node)) return "unknown";
2874
3601
  const name = node.getText();
@@ -2884,17 +3611,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
2884
3611
  unwrapContainers: true
2885
3612
  });
2886
3613
  }
3614
+ var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
3615
+ var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
3616
+ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
3617
+ function detectStreamElement(method) {
3618
+ const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
3619
+ let node = method.getReturnTypeNode();
3620
+ node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
3621
+ const containerEl = streamContainerElement(node);
3622
+ if (containerEl) {
3623
+ return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
3624
+ }
3625
+ if (hasSse) return node ?? null;
3626
+ return null;
3627
+ }
3628
+ function streamContainerElement(node) {
3629
+ if (!node || !import_ts_morph7.Node.isTypeReference(node)) return null;
3630
+ const typeName = node.getTypeName();
3631
+ const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : "";
3632
+ if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
3633
+ return node.getTypeArguments()[0] ?? null;
3634
+ }
3635
+ return null;
3636
+ }
3637
+ function unwrapNamedContainer(node, names) {
3638
+ if (!node || !import_ts_morph7.Node.isTypeReference(node)) return node;
3639
+ const typeName = node.getTypeName();
3640
+ const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : "";
3641
+ if (names.has(name)) {
3642
+ return node.getTypeArguments()[0] ?? node;
3643
+ }
3644
+ return node;
3645
+ }
2887
3646
  function extractDtoContract(method, sourceFile, project) {
2888
3647
  let body = extractBodyType(method, sourceFile, project);
2889
3648
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
2890
3649
  const query = extractQueryType(method, sourceFile, project);
3650
+ const streamElement = detectStreamElement(method);
3651
+ const isStream = streamElement !== null;
2891
3652
  if (filterInfo && filterInfo.source === "body") {
2892
3653
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
2893
3654
  body = body ?? bodyType;
2894
3655
  }
2895
3656
  const paramsType = extractParamsType(method, sourceFile, project);
2896
- const response = extractResponseType(method, sourceFile, project);
2897
- if (body === null && query === null && paramsType === null && response === "unknown" && filterInfo === null) {
3657
+ const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3658
+ const errorInfo = extractErrorType(method, sourceFile, project);
3659
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
2898
3660
  return null;
2899
3661
  }
2900
3662
  let bodyRef = null;
@@ -2908,12 +3670,12 @@ function extractDtoContract(method, sourceFile, project) {
2908
3670
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2909
3671
  }
2910
3672
  }
2911
- const returnTypeNode = method.getReturnTypeNode();
3673
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
2912
3674
  if (returnTypeNode) {
2913
3675
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
2914
3676
  }
2915
- if (!responseRef) {
2916
- const apiResp = method.getDecorator("ApiResponse");
3677
+ if (!responseRef && !isStream) {
3678
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2917
3679
  if (apiResp) {
2918
3680
  const args = apiResp.getArguments();
2919
3681
  const optsArg = args[0];
@@ -2955,16 +3717,19 @@ function extractDtoContract(method, sourceFile, project) {
2955
3717
  query,
2956
3718
  body,
2957
3719
  response,
3720
+ error: errorInfo?.type ?? null,
2958
3721
  params: paramsType,
2959
3722
  queryRef,
2960
3723
  bodyRef,
2961
3724
  responseRef,
3725
+ errorRef: errorInfo?.ref ?? null,
2962
3726
  filterFields: filterInfo?.fieldNames ?? null,
2963
3727
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
2964
3728
  filterSource: filterInfo?.source ?? null,
2965
3729
  formWarnings,
2966
3730
  bodySchema,
2967
- querySchema
3731
+ querySchema,
3732
+ stream: isStream
2968
3733
  };
2969
3734
  }
2970
3735
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -3082,6 +3847,7 @@ function parseDefineContractCall(callExpr) {
3082
3847
  let query = null;
3083
3848
  let body = null;
3084
3849
  let response = "unknown";
3850
+ let error = null;
3085
3851
  let bodyZodText = null;
3086
3852
  let queryZodText = null;
3087
3853
  for (const prop of optsArg.getProperties()) {
@@ -3097,25 +3863,38 @@ function parseDefineContractCall(callExpr) {
3097
3863
  bodyZodText = val.getText();
3098
3864
  } else if (propName === "response") {
3099
3865
  response = zodAstToTs(val);
3866
+ } else if (propName === "error") {
3867
+ error = zodAstToTs(val);
3100
3868
  }
3101
3869
  }
3102
- return { query, body, response, bodyZodText, queryZodText };
3870
+ return { query, body, response, error, bodyZodText, queryZodText };
3103
3871
  }
3104
3872
 
3105
3873
  // src/discovery/contracts-fast.ts
3106
3874
  async function discoverContractsFast(opts) {
3107
3875
  const { cwd, glob, tsconfig } = opts;
3108
- const tsconfigPath = tsconfig ? (0, import_node_path12.resolve)(tsconfig) : (0, import_node_path12.join)(cwd, "tsconfig.json");
3109
- let project;
3876
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3877
+ const project = createDiscoveryProject(tsconfigPath);
3878
+ const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3879
+ for (const f of files) {
3880
+ project.addSourceFileAtPath(f);
3881
+ }
3882
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3883
+ return extractAllRoutes(project);
3884
+ }
3885
+ function resolveTsconfigPath(cwd, tsconfig) {
3886
+ return tsconfig ? (0, import_node_path14.resolve)(tsconfig) : (0, import_node_path14.join)(cwd, "tsconfig.json");
3887
+ }
3888
+ function createDiscoveryProject(tsconfigPath) {
3110
3889
  try {
3111
- project = new import_ts_morph9.Project({
3890
+ return new import_ts_morph9.Project({
3112
3891
  tsConfigFilePath: tsconfigPath,
3113
3892
  skipAddingFilesFromTsConfig: true,
3114
3893
  skipLoadingLibFiles: true,
3115
3894
  skipFileDependencyResolution: true
3116
3895
  });
3117
3896
  } catch {
3118
- project = new import_ts_morph9.Project({
3897
+ return new import_ts_morph9.Project({
3119
3898
  skipAddingFilesFromTsConfig: true,
3120
3899
  skipLoadingLibFiles: true,
3121
3900
  skipFileDependencyResolution: true,
@@ -3126,20 +3905,105 @@ async function discoverContractsFast(opts) {
3126
3905
  }
3127
3906
  });
3128
3907
  }
3129
- const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3130
- for (const f of files) {
3131
- project.addSourceFileAtPath(f);
3132
- }
3133
- const routes = [];
3908
+ }
3909
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
3134
3910
  setDiscoveryContext(project, {
3135
3911
  projectRoot: cwd,
3136
3912
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3137
3913
  });
3914
+ }
3915
+ function extractRoutesFrom(project, controllerPaths) {
3916
+ const routes = [];
3917
+ for (const path of controllerPaths) {
3918
+ const sourceFile = project.getSourceFile(path);
3919
+ if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
3920
+ }
3921
+ return routes;
3922
+ }
3923
+ function extractAllRoutes(project) {
3924
+ const routes = [];
3138
3925
  for (const sourceFile of project.getSourceFiles()) {
3139
3926
  routes.push(...extractFromSourceFile(sourceFile, project));
3140
3927
  }
3141
3928
  return routes;
3142
3929
  }
3930
+ var PersistentDiscovery = class _PersistentDiscovery {
3931
+ project;
3932
+ cwd;
3933
+ glob;
3934
+ /** Absolute paths of the controllers currently loaded as extraction roots. */
3935
+ controllerPaths = /* @__PURE__ */ new Set();
3936
+ constructor(project, cwd, glob) {
3937
+ this.project = project;
3938
+ this.cwd = cwd;
3939
+ this.glob = glob;
3940
+ }
3941
+ /**
3942
+ * Build the initial persistent Project: create it, glob + add all controllers,
3943
+ * bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
3944
+ */
3945
+ static async create(opts) {
3946
+ const { cwd, glob, tsconfig } = opts;
3947
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3948
+ const project = createDiscoveryProject(tsconfigPath);
3949
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3950
+ const instance = new _PersistentDiscovery(project, cwd, glob);
3951
+ const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3952
+ for (const f of files) {
3953
+ project.addSourceFileAtPath(f);
3954
+ instance.controllerPaths.add(f);
3955
+ }
3956
+ return instance;
3957
+ }
3958
+ /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3959
+ discover() {
3960
+ return this.runExtraction();
3961
+ }
3962
+ /**
3963
+ * Re-discover after one or more files changed. Refreshes the changed file(s)
3964
+ * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3965
+ * to pick up added/removed controllers, clears the per-Project caches, then
3966
+ * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3967
+ * being exhaustive because re-globbing + refresh-on-presence covers the set.
3968
+ */
3969
+ async rediscover(changedPaths) {
3970
+ if (changedPaths) {
3971
+ for (const p of changedPaths) {
3972
+ const abs = (0, import_node_path14.resolve)(p);
3973
+ const sf = this.project.getSourceFile(abs);
3974
+ if (sf) {
3975
+ await sf.refreshFromFileSystem();
3976
+ }
3977
+ }
3978
+ }
3979
+ const globbed = new Set(
3980
+ await (0, import_fast_glob2.default)(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
3981
+ );
3982
+ for (const f of globbed) {
3983
+ if (!this.controllerPaths.has(f)) {
3984
+ try {
3985
+ this.project.addSourceFileAtPath(f);
3986
+ this.controllerPaths.add(f);
3987
+ } catch {
3988
+ }
3989
+ }
3990
+ }
3991
+ for (const f of this.controllerPaths) {
3992
+ if (!globbed.has(f)) {
3993
+ const sf = this.project.getSourceFile(f);
3994
+ if (sf) this.project.removeSourceFile(sf);
3995
+ this.controllerPaths.delete(f);
3996
+ }
3997
+ }
3998
+ return this.runExtraction();
3999
+ }
4000
+ /** Clear stale per-Project caches, then extract over the controller set. */
4001
+ runExtraction() {
4002
+ clearTypeResolutionCaches(this.project);
4003
+ clearEnumCache(this.project);
4004
+ return extractRoutesFrom(this.project, this.controllerPaths);
4005
+ }
4006
+ };
3143
4007
  function decoratorStringArg(decoratorExpr) {
3144
4008
  if (!decoratorExpr) return void 0;
3145
4009
  if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -3195,6 +4059,11 @@ function resolveVerb(method) {
3195
4059
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3196
4060
  }
3197
4061
  }
4062
+ const sseDecorator = method.getDecorator("Sse");
4063
+ if (sseDecorator) {
4064
+ const pathArg = sseDecorator.getArguments()[0];
4065
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
4066
+ }
3198
4067
  return null;
3199
4068
  }
3200
4069
  function readAsDecorator(node, label) {
@@ -3237,7 +4106,17 @@ function buildRoute(args) {
3237
4106
  };
3238
4107
  }
3239
4108
  function extractContractRoute(args) {
3240
- const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
4109
+ const {
4110
+ cls,
4111
+ method,
4112
+ applyContractDecorator,
4113
+ verb,
4114
+ prefix,
4115
+ className,
4116
+ sourceFile,
4117
+ project,
4118
+ seenNames
4119
+ } = args;
3241
4120
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3242
4121
  if (!firstDecoratorArg) return null;
3243
4122
  let contractDef = null;
@@ -3247,18 +4126,19 @@ function extractContractRoute(args) {
3247
4126
  contractDef = parseDefineContractCall(firstDecoratorArg);
3248
4127
  } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
3249
4128
  const identName = firstDecoratorArg.getText();
3250
- const varDecl = sourceFile.getVariableDeclaration(identName);
3251
- if (!varDecl) {
4129
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4130
+ if (!resolvedVar) {
3252
4131
  console.warn(
3253
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
4132
+ `[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
3254
4133
  );
3255
4134
  return null;
3256
4135
  }
4136
+ const { decl: varDecl, file: declFile } = resolvedVar;
3257
4137
  const initializer = varDecl.getInitializer();
3258
4138
  if (!initializer) return null;
3259
4139
  contractDef = parseDefineContractCall(initializer);
3260
4140
  if (contractDef && varDecl.isExported()) {
3261
- const filePath = sourceFile.getFilePath();
4141
+ const filePath = declFile.getFilePath();
3262
4142
  if (contractDef.body !== null) {
3263
4143
  bodyZodRef = { name: `${identName}.body`, filePath };
3264
4144
  }
@@ -3291,6 +4171,7 @@ function extractContractRoute(args) {
3291
4171
  query: contractDef.query,
3292
4172
  body: contractDef.body,
3293
4173
  response: contractDef.response,
4174
+ error: contractDef.error,
3294
4175
  // Path A: capture both the importable ref and the raw text. The emitter
3295
4176
  // prefers inlining the text (client-safe — re-exporting from a controller
3296
4177
  // would drag server-only deps into the client bundle).
@@ -3322,15 +4203,18 @@ function extractDtoRoute(args) {
3322
4203
  query: dtoContract?.query ?? null,
3323
4204
  body: dtoContract?.body ?? null,
3324
4205
  response: dtoContract?.response ?? "unknown",
4206
+ error: dtoContract?.error ?? null,
3325
4207
  queryRef: dtoContract?.queryRef ?? null,
3326
4208
  bodyRef: dtoContract?.bodyRef ?? null,
3327
4209
  responseRef: dtoContract?.responseRef ?? null,
4210
+ errorRef: dtoContract?.errorRef ?? null,
3328
4211
  filterFields: dtoContract?.filterFields ?? null,
3329
4212
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3330
4213
  filterSource: dtoContract?.filterSource ?? null,
3331
4214
  formWarnings: dtoContract?.formWarnings ?? [],
3332
4215
  bodySchema: dtoContract?.bodySchema ?? null,
3333
- querySchema: dtoContract?.querySchema ?? null
4216
+ querySchema: dtoContract?.querySchema ?? null,
4217
+ stream: dtoContract?.stream ?? false
3334
4218
  }
3335
4219
  });
3336
4220
  }
@@ -3354,6 +4238,7 @@ function extractFromSourceFile(sourceFile, project) {
3354
4238
  prefix,
3355
4239
  className,
3356
4240
  sourceFile,
4241
+ project,
3357
4242
  seenNames
3358
4243
  }) : extractDtoRoute({
3359
4244
  cls,
@@ -3372,9 +4257,9 @@ function extractFromSourceFile(sourceFile, project) {
3372
4257
  }
3373
4258
 
3374
4259
  // src/watch/lock-file.ts
3375
- var import_promises10 = require("fs/promises");
3376
- var import_promises11 = require("fs/promises");
3377
- var import_node_path13 = require("path");
4260
+ var import_promises12 = require("fs/promises");
4261
+ var import_promises13 = require("fs/promises");
4262
+ var import_node_path15 = require("path");
3378
4263
  var LOCK_FILE = ".watcher.lock";
3379
4264
  function isProcessAlive(pid) {
3380
4265
  try {
@@ -3385,21 +4270,21 @@ function isProcessAlive(pid) {
3385
4270
  }
3386
4271
  }
3387
4272
  async function acquireLock(outDir) {
3388
- await (0, import_promises11.mkdir)(outDir, { recursive: true });
3389
- const lockPath = (0, import_node_path13.join)(outDir, LOCK_FILE);
4273
+ await (0, import_promises13.mkdir)(outDir, { recursive: true });
4274
+ const lockPath = (0, import_node_path15.join)(outDir, LOCK_FILE);
3390
4275
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3391
4276
  try {
3392
- const fd = await (0, import_promises10.open)(lockPath, "wx");
4277
+ const fd = await (0, import_promises12.open)(lockPath, "wx");
3393
4278
  await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
3394
4279
  `, "utf8");
3395
4280
  await fd.close();
3396
4281
  } catch (err) {
3397
4282
  if (err.code === "EEXIST") {
3398
4283
  try {
3399
- const raw = await (0, import_promises11.readFile)(lockPath, "utf8");
4284
+ const raw = await (0, import_promises13.readFile)(lockPath, "utf8");
3400
4285
  const existing = JSON.parse(raw);
3401
4286
  if (isProcessAlive(existing.pid)) return null;
3402
- await (0, import_promises11.unlink)(lockPath);
4287
+ await (0, import_promises13.unlink)(lockPath);
3403
4288
  return acquireLock(outDir);
3404
4289
  } catch {
3405
4290
  return null;
@@ -3410,7 +4295,7 @@ async function acquireLock(outDir) {
3410
4295
  return {
3411
4296
  release: async () => {
3412
4297
  try {
3413
- await (0, import_promises11.unlink)(lockPath);
4298
+ await (0, import_promises13.unlink)(lockPath);
3414
4299
  } catch {
3415
4300
  }
3416
4301
  }
@@ -3426,7 +4311,7 @@ async function watch(config, onChange) {
3426
4311
  if (lock === null) {
3427
4312
  let holderPid = "unknown";
3428
4313
  try {
3429
- const raw = await (0, import_promises12.readFile)((0, import_node_path14.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
4314
+ const raw = await (0, import_promises14.readFile)((0, import_node_path16.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3430
4315
  const data = JSON.parse(raw);
3431
4316
  if (data.pid !== void 0) holderPid = String(data.pid);
3432
4317
  } catch {
@@ -3436,12 +4321,20 @@ async function watch(config, onChange) {
3436
4321
  );
3437
4322
  return NO_OP_WATCHER;
3438
4323
  }
4324
+ let discovery = null;
4325
+ async function getDiscovery() {
4326
+ if (discovery === null) {
4327
+ discovery = await PersistentDiscovery.create({
4328
+ cwd: config.codegen.cwd,
4329
+ glob: config.contracts.glob,
4330
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4331
+ });
4332
+ return discovery;
4333
+ }
4334
+ return discovery;
4335
+ }
3439
4336
  try {
3440
- const initialRoutes = await discoverContractsFast({
3441
- cwd: config.codegen.cwd,
3442
- glob: config.contracts.glob,
3443
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3444
- });
4337
+ const initialRoutes = (await getDiscovery()).discover();
3445
4338
  await generate(config, initialRoutes);
3446
4339
  } catch (err) {
3447
4340
  console.warn(
@@ -3454,7 +4347,7 @@ async function watch(config, onChange) {
3454
4347
  }
3455
4348
  let pagesDebounceTimer;
3456
4349
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3457
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, pagesGlob), {
4350
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, pagesGlob), {
3458
4351
  ignoreInitial: true,
3459
4352
  persistent: true,
3460
4353
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3480,23 +4373,23 @@ async function watch(config, onChange) {
3480
4373
  pagesWatcher.on("change", schedulePagesRegenerate);
3481
4374
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3482
4375
  let contractsDebounceTimer;
3483
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.contracts.glob), {
4376
+ const pendingChangedPaths = /* @__PURE__ */ new Set();
4377
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.contracts.glob), {
3484
4378
  ignoreInitial: true,
3485
4379
  persistent: true,
3486
4380
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3487
4381
  });
3488
- function scheduleContractsRegenerate() {
4382
+ function scheduleContractsRegenerate(changedPath) {
4383
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3489
4384
  if (contractsDebounceTimer !== void 0) {
3490
4385
  clearTimeout(contractsDebounceTimer);
3491
4386
  }
3492
4387
  contractsDebounceTimer = setTimeout(async () => {
3493
4388
  contractsDebounceTimer = void 0;
4389
+ const changed = [...pendingChangedPaths];
4390
+ pendingChangedPaths.clear();
3494
4391
  try {
3495
- const routes = await discoverContractsFast({
3496
- cwd: config.codegen.cwd,
3497
- glob: config.contracts.glob,
3498
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3499
- });
4392
+ const routes = await (await getDiscovery()).rediscover(changed);
3500
4393
  await generate(config, routes);
3501
4394
  } catch (err) {
3502
4395
  console.error(
@@ -3507,17 +4400,17 @@ async function watch(config, onChange) {
3507
4400
  onChange?.();
3508
4401
  }, config.contracts.debounceMs);
3509
4402
  }
3510
- contractsWatcher.on("add", scheduleContractsRegenerate);
3511
- contractsWatcher.on("change", scheduleContractsRegenerate);
3512
- contractsWatcher.on("unlink", scheduleContractsRegenerate);
3513
- const formsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.forms.watch), {
4403
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4404
+ contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4405
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4406
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.forms.watch), {
3514
4407
  ignoreInitial: true,
3515
4408
  persistent: true,
3516
4409
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3517
4410
  });
3518
- formsWatcher.on("add", scheduleContractsRegenerate);
3519
- formsWatcher.on("change", scheduleContractsRegenerate);
3520
- formsWatcher.on("unlink", scheduleContractsRegenerate);
4411
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4412
+ formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4413
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
3521
4414
  return {
3522
4415
  close: async () => {
3523
4416
  if (pagesDebounceTimer !== void 0) {
@@ -3537,7 +4430,7 @@ async function watch(config, onChange) {
3537
4430
  }
3538
4431
 
3539
4432
  // src/index.ts
3540
- var VERSION = "0.4.1";
4433
+ var VERSION = "0.5.0";
3541
4434
 
3542
4435
  // src/cli/codegen.ts
3543
4436
  async function runCodegen(opts = {}) {
@@ -3566,13 +4459,13 @@ async function runCodegen(opts = {}) {
3566
4459
  // src/cli/doctor.ts
3567
4460
  var import_node_child_process2 = require("child_process");
3568
4461
  var import_node_fs4 = require("fs");
3569
- var import_node_path16 = require("path");
4462
+ var import_node_path18 = require("path");
3570
4463
 
3571
4464
  // src/cli/init.ts
3572
4465
  var import_node_child_process = require("child_process");
3573
4466
  var import_node_fs3 = require("fs");
3574
- var import_promises13 = require("fs/promises");
3575
- var import_node_path15 = require("path");
4467
+ var import_promises15 = require("fs/promises");
4468
+ var import_node_path17 = require("path");
3576
4469
  var import_node_readline = require("readline");
3577
4470
 
3578
4471
  // src/cli/patch-utils.ts
@@ -3634,7 +4527,7 @@ ${bold(title)}`);
3634
4527
  }
3635
4528
  async function readPackageJson(cwd) {
3636
4529
  try {
3637
- const raw = await (0, import_promises13.readFile)((0, import_node_path15.join)(cwd, "package.json"), "utf8");
4530
+ const raw = await (0, import_promises15.readFile)((0, import_node_path17.join)(cwd, "package.json"), "utf8");
3638
4531
  return JSON.parse(raw);
3639
4532
  } catch {
3640
4533
  return {};
@@ -3665,7 +4558,7 @@ async function detectTemplateEngine(cwd) {
3665
4558
  async function detectPackageManager(cwd) {
3666
4559
  async function exists(file) {
3667
4560
  try {
3668
- await (0, import_promises13.access)((0, import_node_path15.join)(cwd, file));
4561
+ await (0, import_promises15.access)((0, import_node_path17.join)(cwd, file));
3669
4562
  return true;
3670
4563
  } catch {
3671
4564
  return false;
@@ -3693,7 +4586,7 @@ async function promptFramework() {
3693
4586
  }
3694
4587
  async function fileExists2(filePath) {
3695
4588
  try {
3696
- await (0, import_promises13.access)(filePath);
4589
+ await (0, import_promises15.access)(filePath);
3697
4590
  return true;
3698
4591
  } catch {
3699
4592
  return false;
@@ -3706,15 +4599,15 @@ async function writeIfNotExists(filePath, content, label) {
3706
4599
  }
3707
4600
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
3708
4601
  if (dir) {
3709
- await (0, import_promises13.mkdir)(dir, { recursive: true });
4602
+ await (0, import_promises15.mkdir)(dir, { recursive: true });
3710
4603
  }
3711
- await (0, import_promises13.writeFile)(filePath, content, "utf8");
4604
+ await (0, import_promises15.writeFile)(filePath, content, "utf8");
3712
4605
  logCreated(label);
3713
4606
  }
3714
4607
  async function handleViteConfig(cwd, framework) {
3715
- const filePath = (0, import_node_path15.join)(cwd, "vite.config.ts");
4608
+ const filePath = (0, import_node_path17.join)(cwd, "vite.config.ts");
3716
4609
  if (await fileExists2(filePath)) {
3717
- const existing = await (0, import_promises13.readFile)(filePath, "utf8");
4610
+ const existing = await (0, import_promises15.readFile)(filePath, "utf8");
3718
4611
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
3719
4612
  if (!hasPlugin) {
3720
4613
  logSkipped("vite.config.ts");
@@ -3730,15 +4623,15 @@ async function handleViteConfig(cwd, framework) {
3730
4623
  }
3731
4624
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
3732
4625
  if (dir) {
3733
- await (0, import_promises13.mkdir)(dir, { recursive: true });
4626
+ await (0, import_promises15.mkdir)(dir, { recursive: true });
3734
4627
  }
3735
- await (0, import_promises13.writeFile)(filePath, viteConfigTemplate(framework), "utf8");
4628
+ await (0, import_promises15.writeFile)(filePath, viteConfigTemplate(framework), "utf8");
3736
4629
  logCreated("vite.config.ts");
3737
4630
  }
3738
4631
  async function patchGitignore(gitignorePath) {
3739
4632
  let existing = "";
3740
4633
  if (await fileExists2(gitignorePath)) {
3741
- existing = await (0, import_promises13.readFile)(gitignorePath, "utf8");
4634
+ existing = await (0, import_promises15.readFile)(gitignorePath, "utf8");
3742
4635
  }
3743
4636
  if (existing.split("\n").some((line) => line.trim() === GITIGNORE_ENTRY)) {
3744
4637
  console.log(` ${cyan("\u2192")} .gitignore ${dim("(already contains .nestjs-inertia/, skipped)")}`);
@@ -3748,7 +4641,7 @@ async function patchGitignore(gitignorePath) {
3748
4641
  ` : `${existing}
3749
4642
  ${GITIGNORE_ENTRY}
3750
4643
  `;
3751
- await (0, import_promises13.writeFile)(gitignorePath, newContent, "utf8");
4644
+ await (0, import_promises15.writeFile)(gitignorePath, newContent, "utf8");
3752
4645
  logPatched(".gitignore", "added .nestjs-inertia/");
3753
4646
  }
3754
4647
  function installDeps(pkgManager, deps, dev) {
@@ -3770,10 +4663,10 @@ function installDeps(pkgManager, deps, dev) {
3770
4663
  }
3771
4664
  }
3772
4665
  async function patchPackageJsonScripts(cwd, scripts) {
3773
- const pkgPath = (0, import_node_path15.join)(cwd, "package.json");
4666
+ const pkgPath = (0, import_node_path17.join)(cwd, "package.json");
3774
4667
  let pkg = {};
3775
4668
  try {
3776
- pkg = JSON.parse(await (0, import_promises13.readFile)(pkgPath, "utf8"));
4669
+ pkg = JSON.parse(await (0, import_promises15.readFile)(pkgPath, "utf8"));
3777
4670
  } catch {
3778
4671
  return;
3779
4672
  }
@@ -3792,7 +4685,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
3792
4685
  return;
3793
4686
  }
3794
4687
  pkg.scripts = existing;
3795
- await (0, import_promises13.writeFile)(pkgPath, `${JSON.stringify(pkg, null, 2)}
4688
+ await (0, import_promises15.writeFile)(pkgPath, `${JSON.stringify(pkg, null, 2)}
3796
4689
  `, "utf8");
3797
4690
  }
3798
4691
  function patchAppModule(filePath, rootView) {
@@ -4087,7 +4980,7 @@ export class HomeController {
4087
4980
  }
4088
4981
  `;
4089
4982
  function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4090
- const filePath = (0, import_node_path15.join)(cwd, filename);
4983
+ const filePath = (0, import_node_path17.join)(cwd, filename);
4091
4984
  return patchJsonFile(
4092
4985
  filePath,
4093
4986
  (json) => {
@@ -4102,7 +4995,7 @@ function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4102
4995
  );
4103
4996
  }
4104
4997
  function patchNestCliJson(cwd, shellDir) {
4105
- const filePath = (0, import_node_path15.join)(cwd, "nest-cli.json");
4998
+ const filePath = (0, import_node_path17.join)(cwd, "nest-cli.json");
4106
4999
  return patchJsonFile(filePath, (json) => {
4107
5000
  const compiler = json.compilerOptions ?? {};
4108
5001
  const assets = compiler.assets ?? [];
@@ -4125,46 +5018,46 @@ async function scaffoldFiles(ctx) {
4125
5018
  const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
4126
5019
  logSection("Scaffold files");
4127
5020
  await writeIfNotExists(
4128
- (0, import_node_path15.join)(cwd, "nestjs-inertia.config.ts"),
5021
+ (0, import_node_path17.join)(cwd, "nestjs-inertia.config.ts"),
4129
5022
  configTemplate(framework),
4130
5023
  "nestjs-inertia.config.ts"
4131
5024
  );
4132
- await writeIfNotExists((0, import_node_path15.join)(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
5025
+ await writeIfNotExists((0, import_node_path17.join)(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4133
5026
  await writeIfNotExists(
4134
- (0, import_node_path15.join)(cwd, "tsconfig.inertia.json"),
5027
+ (0, import_node_path17.join)(cwd, "tsconfig.inertia.json"),
4135
5028
  TSCONFIG_INERTIA_TEMPLATE,
4136
5029
  "tsconfig.inertia.json"
4137
5030
  );
4138
5031
  await writeIfNotExists(
4139
- (0, import_node_path15.join)(cwd, "inertia", "tsconfig.json"),
5032
+ (0, import_node_path17.join)(cwd, "inertia", "tsconfig.json"),
4140
5033
  INERTIA_TSCONFIG_TEMPLATE,
4141
5034
  "inertia/tsconfig.json"
4142
5035
  );
4143
5036
  await writeIfNotExists(
4144
- (0, import_node_path15.join)(cwd, "inertia", shellFileName),
5037
+ (0, import_node_path17.join)(cwd, "inertia", shellFileName),
4145
5038
  htmlShellTemplate(framework, engine),
4146
5039
  `inertia/${shellFileName}`
4147
5040
  );
4148
5041
  await handleViteConfig(cwd, framework);
4149
5042
  await writeIfNotExists(
4150
- (0, import_node_path15.join)(cwd, "inertia", "app", `client.${entryExt}`),
5043
+ (0, import_node_path17.join)(cwd, "inertia", "app", `client.${entryExt}`),
4151
5044
  entryPointTemplate(framework),
4152
5045
  `inertia/app/client.${entryExt}`
4153
5046
  );
4154
5047
  await writeIfNotExists(
4155
- (0, import_node_path15.join)(cwd, "inertia", "pages", `Home.${pageExt}`),
5048
+ (0, import_node_path17.join)(cwd, "inertia", "pages", `Home.${pageExt}`),
4156
5049
  samplePageTemplate(framework),
4157
5050
  `inertia/pages/Home.${pageExt}`
4158
5051
  );
4159
5052
  await writeIfNotExists(
4160
- (0, import_node_path15.join)(cwd, "src", "home.controller.ts"),
5053
+ (0, import_node_path17.join)(cwd, "src", "home.controller.ts"),
4161
5054
  SAMPLE_CONTROLLER,
4162
5055
  "src/home.controller.ts"
4163
5056
  );
4164
5057
  }
4165
5058
  function patchServerAppModule(ctx) {
4166
5059
  const { cwd, rootView } = ctx;
4167
- const appModulePath = (0, import_node_path15.join)(cwd, "src", "app.module.ts");
5060
+ const appModulePath = (0, import_node_path17.join)(cwd, "src", "app.module.ts");
4168
5061
  const appModuleResult = patchAppModule(appModulePath, rootView);
4169
5062
  if (appModuleResult === "patched") {
4170
5063
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -4178,7 +5071,7 @@ function patchServerAppModule(ctx) {
4178
5071
  }
4179
5072
  }
4180
5073
  function patchServerMainTs(ctx) {
4181
- const mainTsPath = (0, import_node_path15.join)(ctx.cwd, "src", "main.ts");
5074
+ const mainTsPath = (0, import_node_path17.join)(ctx.cwd, "src", "main.ts");
4182
5075
  const mainTsResult = patchMainTs(mainTsPath);
4183
5076
  if (mainTsResult === "patched") {
4184
5077
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -4213,7 +5106,7 @@ function patchBuildConfigs(ctx) {
4213
5106
  }
4214
5107
  async function patchGitignoreAndDist(ctx) {
4215
5108
  const { cwd } = ctx;
4216
- await patchGitignore((0, import_node_path15.join)(cwd, ".gitignore"));
5109
+ await patchGitignore((0, import_node_path17.join)(cwd, ".gitignore"));
4217
5110
  const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
4218
5111
  if (tsconfigDistResult === "patched") {
4219
5112
  logPatched("tsconfig.json", "excluded dist/ from server compilation");
@@ -4319,7 +5212,7 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4319
5212
 
4320
5213
  // src/cli/doctor.ts
4321
5214
  function checkFileExists(cwd, file) {
4322
- return (0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, file));
5215
+ return (0, import_node_fs4.existsSync)((0, import_node_path18.join)(cwd, file));
4323
5216
  }
4324
5217
  function readJson(path) {
4325
5218
  try {
@@ -4355,15 +5248,15 @@ function writeJsonField(filePath, dotPath, value) {
4355
5248
  }
4356
5249
  function getPackageVersion(cwd, pkg) {
4357
5250
  try {
4358
- const pkgJson = readJson((0, import_node_path16.join)(cwd, "node_modules", pkg, "package.json"));
5251
+ const pkgJson = readJson((0, import_node_path18.join)(cwd, "node_modules", pkg, "package.json"));
4359
5252
  return pkgJson?.version ?? null;
4360
5253
  } catch {
4361
5254
  return null;
4362
5255
  }
4363
5256
  }
4364
5257
  function detectPkgManager(cwd) {
4365
- if ((0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
4366
- if ((0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, "yarn.lock"))) return "yarn";
5258
+ if ((0, import_node_fs4.existsSync)((0, import_node_path18.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
5259
+ if ((0, import_node_fs4.existsSync)((0, import_node_path18.join)(cwd, "yarn.lock"))) return "yarn";
4367
5260
  return "npm";
4368
5261
  }
4369
5262
  async function runDoctor(opts) {
@@ -4401,7 +5294,7 @@ async function runDoctor(opts) {
4401
5294
  autoFix: () => runInit({ cwd })
4402
5295
  });
4403
5296
  if (foundShellDir) {
4404
- const nestCliPath = (0, import_node_path16.join)(cwd, "nest-cli.json");
5297
+ const nestCliPath = (0, import_node_path18.join)(cwd, "nest-cli.json");
4405
5298
  const nestCli = readJson(nestCliPath);
4406
5299
  const compiler = nestCli?.compilerOptions ?? {};
4407
5300
  const assets = compiler.assets ?? [];
@@ -4440,7 +5333,7 @@ async function runDoctor(opts) {
4440
5333
  fix: "Run: nestjs-codegen codegen",
4441
5334
  autoFix: () => runCodegen({ cwd })
4442
5335
  });
4443
- const tsconfigPath = (0, import_node_path16.join)(cwd, "tsconfig.json");
5336
+ const tsconfigPath = (0, import_node_path18.join)(cwd, "tsconfig.json");
4444
5337
  const tsconfig = readJson(tsconfigPath);
4445
5338
  const paths = tsconfig?.compilerOptions?.paths;
4446
5339
  checks.push({
@@ -4451,7 +5344,7 @@ async function runDoctor(opts) {
4451
5344
  });
4452
5345
  const inertiaDir = foundShellDir ?? "inertia";
4453
5346
  for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
4454
- const tsc = readJson((0, import_node_path16.join)(cwd, tsconfigFile));
5347
+ const tsc = readJson((0, import_node_path18.join)(cwd, tsconfigFile));
4455
5348
  if (!tsc) continue;
4456
5349
  const excl = tsc.exclude ?? [];
4457
5350
  const excludesIt = excl.includes(inertiaDir);
@@ -4477,7 +5370,7 @@ async function runDoctor(opts) {
4477
5370
  }
4478
5371
  });
4479
5372
  }
4480
- const inertiaTsconfigPath = (0, import_node_path16.join)(cwd, "tsconfig.inertia.json");
5373
+ const inertiaTsconfigPath = (0, import_node_path18.join)(cwd, "tsconfig.inertia.json");
4481
5374
  const inertiaTsconfig = readJson(inertiaTsconfigPath);
4482
5375
  checks.push({
4483
5376
  name: "tsconfig.inertia.json exists",
@@ -4529,7 +5422,7 @@ async function runDoctor(opts) {
4529
5422
  fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
4530
5423
  });
4531
5424
  }
4532
- const innerTsconfigPath = (0, import_node_path16.join)(cwd, "inertia", "tsconfig.json");
5425
+ const innerTsconfigPath = (0, import_node_path18.join)(cwd, "inertia", "tsconfig.json");
4533
5426
  checks.push({
4534
5427
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
4535
5428
  pass: (0, import_node_fs4.existsSync)(innerTsconfigPath),
@@ -4539,7 +5432,7 @@ async function runDoctor(opts) {
4539
5432
  }
4540
5433
  });
4541
5434
  if (checkFileExists(cwd, "vite.config.ts")) {
4542
- const viteContent = (0, import_node_fs4.readFileSync)((0, import_node_path16.join)(cwd, "vite.config.ts"), "utf8");
5435
+ const viteContent = (0, import_node_fs4.readFileSync)((0, import_node_path18.join)(cwd, "vite.config.ts"), "utf8");
4543
5436
  checks.push({
4544
5437
  name: "vite.config.ts has resolve.alias",
4545
5438
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -4604,7 +5497,7 @@ async function runDoctor(opts) {
4604
5497
  });
4605
5498
  }
4606
5499
  if (checkFileExists(cwd, ".gitignore")) {
4607
- const gitignorePath = (0, import_node_path16.join)(cwd, ".gitignore");
5500
+ const gitignorePath = (0, import_node_path18.join)(cwd, ".gitignore");
4608
5501
  const gitignore = (0, import_node_fs4.readFileSync)(gitignorePath, "utf8");
4609
5502
  checks.push({
4610
5503
  name: ".gitignore includes .nestjs-inertia/",
@@ -4613,7 +5506,7 @@ async function runDoctor(opts) {
4613
5506
  autoFix: () => (0, import_node_fs4.appendFileSync)(gitignorePath, "\n.nestjs-inertia/\n")
4614
5507
  });
4615
5508
  }
4616
- const pkgJsonPath = (0, import_node_path16.join)(cwd, "package.json");
5509
+ const pkgJsonPath = (0, import_node_path18.join)(cwd, "package.json");
4617
5510
  const pkgJson = readJson(pkgJsonPath);
4618
5511
  const scripts = pkgJson?.scripts ?? {};
4619
5512
  checks.push({