@dudousxd/nestjs-codegen 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -126,6 +126,19 @@ function applyDefaults(userConfig, cwd) {
126
126
  enabled: userConfig.forms?.enabled ?? true,
127
127
  watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
128
128
  zodImport: userConfig.forms?.zodImport ?? "zod"
129
+ },
130
+ openapi: {
131
+ enabled: userConfig.openapi?.enabled ?? false,
132
+ fileName: userConfig.openapi?.fileName ?? "openapi.json",
133
+ title: userConfig.openapi?.title ?? "NestJS API",
134
+ version: userConfig.openapi?.version ?? "1.0.0",
135
+ description: userConfig.openapi?.description ?? null
136
+ },
137
+ mocks: {
138
+ enabled: userConfig.mocks?.enabled ?? false,
139
+ fileName: userConfig.mocks?.fileName ?? "mocks.ts",
140
+ seed: userConfig.mocks?.seed ?? 1,
141
+ baseUrl: userConfig.mocks?.baseUrl ?? ""
129
142
  }
130
143
  };
131
144
  }
@@ -163,8 +176,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
163
176
  }
164
177
 
165
178
  // src/generate.ts
166
- import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
167
- import { dirname as dirname2, join as join10 } from "path";
179
+ import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
180
+ import { dirname as dirname2, join as join12 } from "path";
168
181
 
169
182
  // src/discovery/pages.ts
170
183
  import { readFile } from "fs/promises";
@@ -693,17 +706,28 @@ function emitFilterQueryType(c) {
693
706
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
694
707
  }
695
708
  function buildResponseType(c, outDir) {
709
+ const respRef = c.contractSource.responseRef;
710
+ if (c.contractSource.stream) {
711
+ if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
712
+ return c.contractSource.response;
713
+ }
696
714
  if (c.controllerRef) {
697
715
  let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
698
716
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
699
717
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
700
718
  }
701
- const respRef = c.contractSource.responseRef;
702
719
  if (respRef) {
703
720
  return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
704
721
  }
705
722
  return c.contractSource.response;
706
723
  }
724
+ function buildErrorType(c) {
725
+ const errRef = c.contractSource.errorRef;
726
+ if (errRef) {
727
+ return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
728
+ }
729
+ return c.contractSource.error ?? "unknown";
730
+ }
707
731
  function emitRouterTypeBlock(tree, indent, outDir) {
708
732
  const pad = " ".repeat(indent);
709
733
  const lines = [];
@@ -718,12 +742,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
718
742
  const bodyRef = c.contractSource.bodyRef;
719
743
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
720
744
  const response = buildResponseType(c, outDir);
745
+ const error = buildErrorType(c);
721
746
  const params = buildParamsType(c.params);
722
747
  const safeMethod = JSON.stringify(method);
723
748
  const safeUrl = JSON.stringify(c.path);
724
749
  const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
750
+ const stream = c.contractSource.stream ? "true" : "false";
725
751
  lines.push(
726
- `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
752
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
727
753
  );
728
754
  } else {
729
755
  lines.push(`${pad}${objKey}: {`);
@@ -795,15 +821,21 @@ function emitReqHelper() {
795
821
  ""
796
822
  ];
797
823
  }
798
- function renderLeaf(pad, objKey, req, requestExpr, members) {
824
+ function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
799
825
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
800
826
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
827
+ if (streamExpr) {
828
+ lines.push(`${pad} stream: () => ${streamExpr},`);
829
+ }
801
830
  for (const [name, value] of Object.entries(members)) {
802
831
  lines.push(`${pad} ${name}: ${value},`);
803
832
  }
804
833
  lines.push(`${pad}}),`);
805
834
  return lines;
806
835
  }
836
+ function renderStreamExpr(req) {
837
+ return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
838
+ }
807
839
  function emitApiObjectBlock(tree, indent, p) {
808
840
  const pad = " ".repeat(indent);
809
841
  const lines = [];
@@ -838,7 +870,8 @@ function emitApiObjectBlock(tree, indent, p) {
838
870
  }
839
871
  const members = {};
840
872
  for (const [name, { value }] of owned) members[name] = value;
841
- lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
873
+ const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
874
+ lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
842
875
  }
843
876
  return lines;
844
877
  }
@@ -876,6 +909,8 @@ var ROUTE_NAMESPACE = [
876
909
  ' export type Params<K extends string> = ResolveByName<K, "params">;',
877
910
  ' export type Error<K extends string> = ResolveByName<K, "error">;',
878
911
  ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
912
+ " /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
913
+ ' export type Stream<K extends string> = ResolveByName<K, "response">;',
879
914
  " export type Request<K extends string> = {",
880
915
  " body: Body<K>;",
881
916
  " query: Query<K>;",
@@ -892,6 +927,7 @@ var PATH_NAMESPACE = [
892
927
  ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
893
928
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
894
929
  ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
930
+ ' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
895
931
  "}",
896
932
  ""
897
933
  ];
@@ -903,6 +939,7 @@ var EMPTY_ROUTE_NAMESPACE = [
903
939
  " export type Params<K extends string> = never;",
904
940
  " export type Error<K extends string> = never;",
905
941
  " export type FilterFields<K extends string> = never;",
942
+ " export type Stream<K extends string> = never;",
906
943
  " export type Request<K extends string> = { body: never; query: never; params: never };",
907
944
  "}",
908
945
  ""
@@ -915,6 +952,7 @@ var EMPTY_PATH_NAMESPACE = [
915
952
  " export type Params<M extends string, U extends string> = never;",
916
953
  " export type Error<M extends string, U extends string> = never;",
917
954
  " export type FilterFields<M extends string, U extends string> = never;",
955
+ " export type Stream<M extends string, U extends string> = never;",
918
956
  "}",
919
957
  ""
920
958
  ];
@@ -938,7 +976,7 @@ function buildApiFile(routes, outDir, opts = {}) {
938
976
  for (const r of contracted) {
939
977
  const cs = r.contract?.contractSource;
940
978
  if (!cs) continue;
941
- const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
979
+ const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
942
980
  for (const ref of refs) {
943
981
  if (!ref) continue;
944
982
  let names = importsByFile.get(ref.filePath);
@@ -1116,18 +1154,27 @@ function refRootIdentifier(refName) {
1116
1154
  function hasSource(src) {
1117
1155
  return !!(src.schema || src.zodText || src.zodRef);
1118
1156
  }
1157
+ function escapeRegExp(s) {
1158
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1159
+ }
1160
+ var wordBoundaryRegexCache = /* @__PURE__ */ new Map();
1161
+ function wordBoundaryRegex(token) {
1162
+ let re = wordBoundaryRegexCache.get(token);
1163
+ if (re === void 0) {
1164
+ re = new RegExp(`\\b${escapeRegExp(token)}\\b`, "g");
1165
+ wordBoundaryRegexCache.set(token, re);
1166
+ }
1167
+ return re;
1168
+ }
1119
1169
  function applyRenames(text, renames) {
1120
1170
  if (!renames || renames.size === 0) return text;
1121
1171
  let out = text;
1122
1172
  for (const [from, to] of renames) {
1123
1173
  if (from === to) continue;
1124
- out = out.replace(new RegExp(`\\b${escapeRegExp(from)}\\b`, "g"), to);
1174
+ out = out.replace(wordBoundaryRegex(from), to);
1125
1175
  }
1126
1176
  return out;
1127
1177
  }
1128
- function escapeRegExp(s) {
1129
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1130
- }
1131
1178
  function isSelfReferential(name, text) {
1132
1179
  return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
1133
1180
  }
@@ -1139,7 +1186,23 @@ function planNestedSchemas(entries) {
1139
1186
  const local = Object.entries(entry.nestedSchemas);
1140
1187
  if (local.length === 0) continue;
1141
1188
  const rename = /* @__PURE__ */ new Map();
1142
- for (const [name] of local) rename.set(name, name);
1189
+ const renameValues = /* @__PURE__ */ new Set();
1190
+ const setRename = (key, value) => {
1191
+ const prev = rename.get(key);
1192
+ rename.set(key, value);
1193
+ if (prev !== void 0 && prev !== value) {
1194
+ let stillUsed = false;
1195
+ for (const v of rename.values()) {
1196
+ if (v === prev) {
1197
+ stillUsed = true;
1198
+ break;
1199
+ }
1200
+ }
1201
+ if (!stillUsed) renameValues.delete(prev);
1202
+ }
1203
+ renameValues.add(value);
1204
+ };
1205
+ for (const [name] of local) setRename(name, name);
1143
1206
  const textFor = (name) => {
1144
1207
  const raw = entry.nestedSchemas?.[name] ?? "";
1145
1208
  return applyRenames(raw, rename);
@@ -1157,11 +1220,11 @@ function planNestedSchemas(entries) {
1157
1220
  if (existing === text) continue;
1158
1221
  let i = 2;
1159
1222
  let candidate = `${name}_${i}`;
1160
- while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || [...rename.values()].includes(candidate)) {
1223
+ while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || renameValues.has(candidate)) {
1161
1224
  i += 1;
1162
1225
  candidate = `${name}_${i}`;
1163
1226
  }
1164
- rename.set(name, candidate);
1227
+ setRename(name, candidate);
1165
1228
  changed = true;
1166
1229
  }
1167
1230
  }
@@ -1371,11 +1434,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1371
1434
  await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
1372
1435
  }
1373
1436
 
1374
- // src/emit/emit-pages.ts
1437
+ // src/emit/emit-mocks.ts
1375
1438
  import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
1376
- import { join as join8, relative as relative5 } from "path";
1377
- async function emitPages(pages, outDir, _options = {}) {
1439
+ import { join as join8 } from "path";
1440
+
1441
+ // src/ir/schema-node-to-json-schema.ts
1442
+ var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
1443
+ function parseLiteral(raw) {
1444
+ const t = raw.trim();
1445
+ if (t === "true") return true;
1446
+ if (t === "false") return false;
1447
+ if (t === "null") return null;
1448
+ const q = t[0];
1449
+ if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
1450
+ return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
1451
+ }
1452
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
1453
+ return Number(t);
1454
+ }
1455
+ return t;
1456
+ }
1457
+ function literalsType(values) {
1458
+ const types = new Set(values.map((v) => v === null ? "null" : typeof v));
1459
+ if (types.size === 1) {
1460
+ const only = [...types][0];
1461
+ if (only === "string") return "string";
1462
+ if (only === "number") return "number";
1463
+ if (only === "boolean") return "boolean";
1464
+ }
1465
+ return void 0;
1466
+ }
1467
+ function convert(node, ctx) {
1468
+ switch (node.kind) {
1469
+ case "string": {
1470
+ const out = { type: "string" };
1471
+ for (const c of node.checks) {
1472
+ if (c.check === "email") out.format = "email";
1473
+ else if (c.check === "url") out.format = "uri";
1474
+ else if (c.check === "uuid") out.format = "uuid";
1475
+ else if (c.check === "min") out.minLength = Number(c.value);
1476
+ else if (c.check === "max") out.maxLength = Number(c.value);
1477
+ else if (c.check === "regex") {
1478
+ const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
1479
+ out.pattern = m ? m[1] : c.pattern;
1480
+ }
1481
+ }
1482
+ return out;
1483
+ }
1484
+ case "number": {
1485
+ const out = { type: "number" };
1486
+ for (const c of node.checks) {
1487
+ if (c.check === "int") out.type = "integer";
1488
+ else if (c.check === "min") out.minimum = Number(c.value);
1489
+ else if (c.check === "max") out.maximum = Number(c.value);
1490
+ else if (c.check === "positive") out.exclusiveMinimum = 0;
1491
+ else if (c.check === "negative") out.exclusiveMaximum = 0;
1492
+ }
1493
+ return out;
1494
+ }
1495
+ case "boolean":
1496
+ return { type: "boolean" };
1497
+ case "date":
1498
+ return { type: "string", format: "date-time" };
1499
+ case "unknown":
1500
+ return node.note ? { description: node.note } : {};
1501
+ case "instanceof":
1502
+ return { type: "object", description: `instanceof ${node.ctor}` };
1503
+ case "enum": {
1504
+ const values = node.literals.map(parseLiteral);
1505
+ const t = literalsType(values);
1506
+ const out = { enum: values };
1507
+ if (t) out.type = t;
1508
+ return out;
1509
+ }
1510
+ case "literal": {
1511
+ const value = parseLiteral(node.raw);
1512
+ const out = { const: value };
1513
+ const t = literalsType([value]);
1514
+ if (t) out.type = t;
1515
+ return out;
1516
+ }
1517
+ case "union": {
1518
+ const options = node.options.map((o) => convert(o, ctx));
1519
+ const out = { oneOf: options };
1520
+ if (node.discriminator) {
1521
+ out.discriminator = { propertyName: node.discriminator };
1522
+ }
1523
+ return out;
1524
+ }
1525
+ case "object": {
1526
+ const properties = {};
1527
+ const required = [];
1528
+ for (const f of node.fields) {
1529
+ if (f.value.kind === "optional") {
1530
+ properties[f.key] = convert(f.value.inner, ctx);
1531
+ } else {
1532
+ properties[f.key] = convert(f.value, ctx);
1533
+ required.push(f.key);
1534
+ }
1535
+ }
1536
+ const out = { type: "object", properties };
1537
+ if (required.length > 0) out.required = required;
1538
+ out.additionalProperties = node.passthrough;
1539
+ return out;
1540
+ }
1541
+ case "array":
1542
+ return { type: "array", items: convert(node.element, ctx) };
1543
+ case "optional":
1544
+ return widenNullable(convert(node.inner, ctx));
1545
+ case "ref":
1546
+ case "lazyRef":
1547
+ return { $ref: `${ctx.refPrefix}${node.name}` };
1548
+ case "annotated":
1549
+ return convert(node.inner, ctx);
1550
+ }
1551
+ }
1552
+ function widenNullable(schema) {
1553
+ if (schema.$ref) {
1554
+ return { anyOf: [schema, { type: "null" }] };
1555
+ }
1556
+ if (typeof schema.type === "string") {
1557
+ return { ...schema, type: [schema.type, "null"] };
1558
+ }
1559
+ if (Array.isArray(schema.type)) {
1560
+ return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
1561
+ }
1562
+ return { anyOf: [schema, { type: "null" }] };
1563
+ }
1564
+ function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
1565
+ const named = {};
1566
+ for (const [name, node] of mod.named) {
1567
+ named[name] = convert(node, ctx);
1568
+ }
1569
+ return { root: convert(mod.root, ctx), named };
1570
+ }
1571
+
1572
+ // src/emit/mock-gen-runtime.ts
1573
+ var MOCK_GEN_RUNTIME = `
1574
+ /** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
1575
+ function makeRng(seed) {
1576
+ let a = seed >>> 0;
1577
+ return {
1578
+ next() {
1579
+ a |= 0;
1580
+ a = (a + 0x6d2b79f5) | 0;
1581
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
1582
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
1583
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
1584
+ },
1585
+ };
1586
+ }
1587
+
1588
+ function __pick(rng, items) {
1589
+ return items[Math.floor(rng.next() * items.length)];
1590
+ }
1591
+
1592
+ function __intBetween(rng, min, max) {
1593
+ return Math.floor(rng.next() * (max - min + 1)) + min;
1594
+ }
1595
+
1596
+ const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
1597
+ const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
1598
+ const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
1599
+
1600
+ function __fakeWords(rng, count) {
1601
+ let out = [];
1602
+ for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
1603
+ return out.join(' ');
1604
+ }
1605
+
1606
+ function __hex(rng, len) {
1607
+ let s = '';
1608
+ for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
1609
+ return s;
1610
+ }
1611
+
1612
+ function __fakeUuid(rng) {
1613
+ return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
1614
+ }
1615
+
1616
+ function __fakeString(rng, schema) {
1617
+ switch (schema.format) {
1618
+ case 'email':
1619
+ return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
1620
+ case 'uri':
1621
+ case 'url':
1622
+ return 'https://example.com/' + __pick(rng, __WORDS);
1623
+ case 'uuid':
1624
+ return __fakeUuid(rng);
1625
+ case 'date-time':
1626
+ return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
1627
+ default:
1628
+ return __fakeWords(rng, __intBetween(rng, 1, 3));
1629
+ }
1630
+ }
1631
+
1632
+ /** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
1633
+ function generateMock(schema, rng, defs, depth) {
1634
+ defs = defs || {};
1635
+ depth = depth || 0;
1636
+ if (schema.$ref) {
1637
+ const name = schema.$ref.replace('#/components/schemas/', '');
1638
+ const target = defs[name];
1639
+ if (!target || depth > 4) return null;
1640
+ return generateMock(target, rng, defs, depth + 1);
1641
+ }
1642
+ if ('const' in schema) return schema.const;
1643
+ if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
1644
+ if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
1645
+ if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
1646
+ let type = Array.isArray(schema.type)
1647
+ ? (schema.type.filter((t) => t !== 'null')[0] || 'null')
1648
+ : schema.type;
1649
+ switch (type) {
1650
+ case 'string':
1651
+ return __fakeString(rng, schema);
1652
+ case 'integer':
1653
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
1654
+ case 'number':
1655
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
1656
+ case 'boolean':
1657
+ return rng.next() < 0.5;
1658
+ case 'null':
1659
+ return null;
1660
+ case 'array': {
1661
+ const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
1662
+ const items = schema.items || {};
1663
+ let arr = [];
1664
+ for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
1665
+ return arr;
1666
+ }
1667
+ case 'object': {
1668
+ const out = {};
1669
+ const props = schema.properties || {};
1670
+ for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
1671
+ return out;
1672
+ }
1673
+ default:
1674
+ return {};
1675
+ }
1676
+ }
1677
+ `.trim();
1678
+
1679
+ // src/emit/emit-mocks.ts
1680
+ var REF_PREFIX = "#/components/schemas/";
1681
+ function toMswPath(path, baseUrl) {
1682
+ return `${baseUrl}${path}`;
1683
+ }
1684
+ function responseSchemaFor(route, defs) {
1685
+ const cs = route.contract.contractSource;
1686
+ if (cs.responseSchema) {
1687
+ const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
1688
+ for (const [name, node] of Object.entries(named)) {
1689
+ if (!(name in defs)) defs[name] = node;
1690
+ }
1691
+ return root;
1692
+ }
1693
+ return {};
1694
+ }
1695
+ function buildMocksFile(routes, opts = {}) {
1696
+ const seed = opts.seed ?? 1;
1697
+ const baseUrl = opts.baseUrl ?? "";
1698
+ const contracted = routes.filter((r) => r.contract);
1699
+ const defs = {};
1700
+ const handlers = [];
1701
+ for (const r of contracted) {
1702
+ const schema = responseSchemaFor(r, defs);
1703
+ const method = r.method.toLowerCase();
1704
+ const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
1705
+ const path = toMswPath(r.path, baseUrl);
1706
+ const cs = r.contract.contractSource;
1707
+ const schemaLiteral = JSON.stringify(schema);
1708
+ const pathLit = JSON.stringify(path);
1709
+ if (cs.stream) {
1710
+ handlers.push(
1711
+ [
1712
+ ` // ${r.name} (stream)`,
1713
+ ` http.${mswMethod}(${pathLit}, () => {`,
1714
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1715
+ " const body = `data: ${JSON.stringify(value)}\\n\\n`;",
1716
+ " return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
1717
+ " }),"
1718
+ ].join("\n")
1719
+ );
1720
+ } else {
1721
+ handlers.push(
1722
+ [
1723
+ ` // ${r.name}`,
1724
+ ` http.${mswMethod}(${pathLit}, () => {`,
1725
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1726
+ " return HttpResponse.json(value);",
1727
+ " }),"
1728
+ ].join("\n")
1729
+ );
1730
+ }
1731
+ }
1732
+ const lines = [
1733
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1734
+ "// MSW handlers returning deterministic, schema-shaped mock data.",
1735
+ "/* eslint-disable */",
1736
+ "// @ts-nocheck",
1737
+ "",
1738
+ "import { http, HttpResponse } from 'msw';",
1739
+ "",
1740
+ `const SEED = ${seed};`,
1741
+ "",
1742
+ "// ---------------------------------------------------------------------------",
1743
+ "// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
1744
+ "// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
1745
+ "// ---------------------------------------------------------------------------",
1746
+ MOCK_GEN_RUNTIME,
1747
+ "",
1748
+ "// Shared component schemas referenced by $ref.",
1749
+ `const DEFS = ${JSON.stringify(defs, null, 2)};`,
1750
+ "",
1751
+ "/** MSW request handlers, one per contracted route. */",
1752
+ "export const handlers = [",
1753
+ ...handlers,
1754
+ "];",
1755
+ ""
1756
+ ];
1757
+ return lines.join("\n");
1758
+ }
1759
+ async function emitMocks(routes, outDir, opts = {}) {
1378
1760
  await mkdir5(outDir, { recursive: true });
1761
+ const content = buildMocksFile(routes, opts);
1762
+ const fileName = opts.fileName ?? "mocks.ts";
1763
+ await writeFile5(join8(outDir, fileName), content, "utf8");
1764
+ }
1765
+
1766
+ // src/emit/emit-openapi.ts
1767
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1768
+ import { join as join9 } from "path";
1769
+ var REF_PREFIX2 = "#/components/schemas/";
1770
+ function toOpenApiPath(path) {
1771
+ return path.replace(/:([^/]+)/g, "{$1}");
1772
+ }
1773
+ function positionSchema(schema, tsType, components) {
1774
+ if (schema) {
1775
+ const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
1776
+ for (const [name, node] of Object.entries(named)) {
1777
+ if (!(name in components)) components[name] = node;
1778
+ }
1779
+ return root;
1780
+ }
1781
+ return tsType ? { description: tsType } : {};
1782
+ }
1783
+ function buildParameters(route) {
1784
+ const params = [];
1785
+ for (const p of route.params) {
1786
+ if (p.source === "path") {
1787
+ params.push({
1788
+ name: p.name,
1789
+ in: "path",
1790
+ required: true,
1791
+ schema: { type: "string" }
1792
+ });
1793
+ } else if (p.source === "query") {
1794
+ params.push({
1795
+ name: p.name,
1796
+ in: "query",
1797
+ required: false,
1798
+ schema: { type: "string" }
1799
+ });
1800
+ } else if (p.source === "header") {
1801
+ params.push({
1802
+ name: p.name,
1803
+ in: "header",
1804
+ required: false,
1805
+ schema: { type: "string" }
1806
+ });
1807
+ }
1808
+ }
1809
+ return params;
1810
+ }
1811
+ function buildResponses(cs, components) {
1812
+ const responses = {};
1813
+ const successSchema = positionSchema(
1814
+ // Prefer rich response IR when present; otherwise fall back to the TS type.
1815
+ cs.responseSchema ?? null,
1816
+ cs.response,
1817
+ components
1818
+ );
1819
+ const successContentType = cs.stream ? "text/event-stream" : "application/json";
1820
+ responses["200"] = {
1821
+ description: cs.stream ? "Server-sent event stream" : "Successful response",
1822
+ content: { [successContentType]: { schema: successSchema } }
1823
+ };
1824
+ const errorSchema = positionSchema(null, cs.error ?? null, components);
1825
+ const errorBody = {
1826
+ description: "Error response",
1827
+ content: { "application/json": { schema: errorSchema } }
1828
+ };
1829
+ if (cs.error || cs.errorRef) {
1830
+ responses["400"] = errorBody;
1831
+ responses.default = errorBody;
1832
+ } else {
1833
+ responses.default = {
1834
+ description: "Error response",
1835
+ content: { "application/json": { schema: {} } }
1836
+ };
1837
+ }
1838
+ return responses;
1839
+ }
1840
+ function buildOperation(route, components) {
1841
+ const cs = route.contract.contractSource;
1842
+ const op = {
1843
+ operationId: route.name,
1844
+ parameters: buildParameters(route),
1845
+ responses: buildResponses(cs, components)
1846
+ };
1847
+ const method = route.method.toUpperCase();
1848
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
1849
+ if (hasBody && (cs.bodySchema || cs.body)) {
1850
+ const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
1851
+ op.requestBody = {
1852
+ required: true,
1853
+ content: { "application/json": { schema: bodySchema } }
1854
+ };
1855
+ }
1856
+ return op;
1857
+ }
1858
+ function buildOpenApiSpec(routes, opts = {}) {
1859
+ const components = {};
1860
+ const paths = {};
1861
+ for (const route of routes) {
1862
+ if (!route.contract) continue;
1863
+ const oaPath = toOpenApiPath(route.path);
1864
+ const method = route.method.toLowerCase();
1865
+ let pathItem = paths[oaPath];
1866
+ if (!pathItem) {
1867
+ pathItem = {};
1868
+ paths[oaPath] = pathItem;
1869
+ }
1870
+ pathItem[method] = buildOperation(route, components);
1871
+ }
1872
+ const info = opts.info ?? {};
1873
+ const doc = {
1874
+ openapi: "3.1.0",
1875
+ info: {
1876
+ title: info.title ?? "NestJS API",
1877
+ version: info.version ?? "1.0.0",
1878
+ ...info.description ? { description: info.description } : {}
1879
+ },
1880
+ paths,
1881
+ components: { schemas: components }
1882
+ };
1883
+ return doc;
1884
+ }
1885
+ async function emitOpenApi(routes, outDir, opts = {}) {
1886
+ await mkdir6(outDir, { recursive: true });
1887
+ const doc = buildOpenApiSpec(routes, opts);
1888
+ const fileName = opts.fileName ?? "openapi.json";
1889
+ await writeFile6(join9(outDir, fileName), `${JSON.stringify(doc, null, 2)}
1890
+ `, "utf8");
1891
+ }
1892
+
1893
+ // src/emit/emit-pages.ts
1894
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1895
+ import { join as join10, relative as relative5 } from "path";
1896
+ async function emitPages(pages, outDir, _options = {}) {
1897
+ await mkdir7(outDir, { recursive: true });
1379
1898
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1380
1899
  const augBody = pages.map((p) => {
1381
1900
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
@@ -1394,7 +1913,7 @@ ${augBody}
1394
1913
  }
1395
1914
  ${sharedPropsBlock}}
1396
1915
  `;
1397
- await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
1916
+ await writeFile7(join10(outDir, "pages.d.ts"), content, "utf8");
1398
1917
  }
1399
1918
  function buildSharedPropsBlock(sharedProps) {
1400
1919
  if (!sharedProps) return "";
@@ -1424,12 +1943,12 @@ function needsQuotes(name) {
1424
1943
  }
1425
1944
 
1426
1945
  // src/emit/emit-routes.ts
1427
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1428
- import { join as join9 } from "path";
1946
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
1947
+ import { join as join11 } from "path";
1429
1948
  async function emitRoutes(routes, outDir) {
1430
- await mkdir6(outDir, { recursive: true });
1949
+ await mkdir8(outDir, { recursive: true });
1431
1950
  const content = buildRoutesFile(routes);
1432
- await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
1951
+ await writeFile8(join11(outDir, "routes.ts"), content, "utf8");
1433
1952
  }
1434
1953
  function buildRoutesFile(routes) {
1435
1954
  if (routes.length === 0) {
@@ -1577,24 +2096,41 @@ async function generate(config, inputRoutes = []) {
1577
2096
  });
1578
2097
  }
1579
2098
  const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
2099
+ if (hasContracts && config.openapi.enabled) {
2100
+ await emitOpenApi(routes, config.codegen.outDir, {
2101
+ fileName: config.openapi.fileName,
2102
+ info: {
2103
+ title: config.openapi.title,
2104
+ version: config.openapi.version,
2105
+ ...config.openapi.description ? { description: config.openapi.description } : {}
2106
+ }
2107
+ });
2108
+ }
2109
+ if (hasContracts && config.mocks.enabled) {
2110
+ await emitMocks(routes, config.codegen.outDir, {
2111
+ fileName: config.mocks.fileName,
2112
+ seed: config.mocks.seed,
2113
+ baseUrl: config.mocks.baseUrl
2114
+ });
2115
+ }
1580
2116
  await emitIndex(config.codegen.outDir, hasContracts, hasForms);
1581
2117
  if (extensions.length > 0) {
1582
2118
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1583
2119
  for (const file of extraFiles) {
1584
- const dest = join10(config.codegen.outDir, file.path);
1585
- await mkdir7(dirname2(dest), { recursive: true });
1586
- await writeFile7(dest, file.contents, "utf8");
2120
+ const dest = join12(config.codegen.outDir, file.path);
2121
+ await mkdir9(dirname2(dest), { recursive: true });
2122
+ await writeFile9(dest, file.contents, "utf8");
1587
2123
  }
1588
2124
  }
1589
2125
  }
1590
2126
 
1591
2127
  // src/watch/watcher.ts
1592
2128
  import { readFile as readFile3 } from "fs/promises";
1593
- import { join as join13 } from "path";
2129
+ import { join as join15 } from "path";
1594
2130
  import chokidar from "chokidar";
1595
2131
 
1596
2132
  // src/discovery/contracts-fast.ts
1597
- import { join as join11, resolve as resolve3 } from "path";
2133
+ import { join as join13, resolve as resolve3 } from "path";
1598
2134
  import fg2 from "fast-glob";
1599
2135
  import {
1600
2136
  Node as Node8,
@@ -1776,7 +2312,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
1776
2312
  }
1777
2313
  return null;
1778
2314
  }
2315
+ function resolveImportedVariable(name, sourceFile, project) {
2316
+ const local = sourceFile.getVariableDeclaration(name);
2317
+ if (local) return { decl: local, file: sourceFile };
2318
+ return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
2319
+ }
2320
+ function resolveVariableViaImports(name, sourceFile, project, seen) {
2321
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2322
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
2323
+ if (!namedImport) continue;
2324
+ const sourceName = namedImport.getName();
2325
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
2326
+ const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
2327
+ if (found) return found;
2328
+ }
2329
+ return null;
2330
+ }
2331
+ function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
2332
+ const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
2333
+ for (const candidate of candidates) {
2334
+ let importedFile = project.getSourceFile(candidate);
2335
+ if (!importedFile) {
2336
+ try {
2337
+ importedFile = project.addSourceFileAtPath(candidate);
2338
+ } catch {
2339
+ continue;
2340
+ }
2341
+ }
2342
+ const found = resolveVariableInFile(name, importedFile, project, seen);
2343
+ if (found) return found;
2344
+ }
2345
+ return null;
2346
+ }
2347
+ function resolveVariableInFile(name, file, project, seen) {
2348
+ const filePath = file.getFilePath();
2349
+ if (seen.has(filePath)) return null;
2350
+ seen.add(filePath);
2351
+ const local = file.getVariableDeclaration(name);
2352
+ if (local) return { decl: local, file };
2353
+ for (const exportDecl of file.getExportDeclarations()) {
2354
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue();
2355
+ const namedExports = exportDecl.getNamedExports();
2356
+ if (moduleSpecifier) {
2357
+ const hasStar = namedExports.length === 0;
2358
+ const reExport2 = namedExports.find(
2359
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2360
+ );
2361
+ if (!hasStar && !reExport2) continue;
2362
+ const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
2363
+ const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
2364
+ if (found) return found;
2365
+ continue;
2366
+ }
2367
+ const reExport = namedExports.find(
2368
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2369
+ );
2370
+ if (!reExport) continue;
2371
+ const sourceName = reExport.getName();
2372
+ const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
2373
+ if (viaImports) return viaImports;
2374
+ }
2375
+ return null;
2376
+ }
1779
2377
  var _findTypeCache = /* @__PURE__ */ new WeakMap();
2378
+ function clearTypeResolutionCaches(project) {
2379
+ _findTypeCache.delete(project);
2380
+ _resolveNamedRefCache.delete(project);
2381
+ }
1780
2382
  function findType(name, sourceFile, project) {
1781
2383
  let byKey = _findTypeCache.get(project);
1782
2384
  if (byKey === void 0) {
@@ -1917,9 +2519,11 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1917
2519
  warnings: [],
1918
2520
  warnedDecorators: /* @__PURE__ */ new Set(),
1919
2521
  emittedClasses: /* @__PURE__ */ new Map(),
2522
+ usedSchemaNames: /* @__PURE__ */ new Set(),
1920
2523
  visiting: /* @__PURE__ */ new Set(),
1921
2524
  recursiveSchemas: /* @__PURE__ */ new Set(),
1922
- depth: 0
2525
+ depth: 0,
2526
+ typeBindings: /* @__PURE__ */ new Map()
1923
2527
  };
1924
2528
  const root = buildObject(classDecl, sourceFile, ctx);
1925
2529
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -1943,11 +2547,34 @@ function buildProperty(prop, classFile, ctx) {
1943
2547
  const typeNode = prop.getTypeNode();
1944
2548
  const typeText = typeNode?.getText() ?? "unknown";
1945
2549
  const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
2550
+ const discriminator = resolveDiscriminator(dec("Type"));
2551
+ if (discriminator) {
2552
+ const options = discriminator.subTypes.map(
2553
+ (name) => buildNestedReference(name, classFile, ctx)
2554
+ );
2555
+ const unionNode = {
2556
+ kind: "union",
2557
+ options,
2558
+ discriminator: discriminator.property
2559
+ };
2560
+ const wrapArray = has("IsArray") || isArrayType;
2561
+ const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
2562
+ return applyPresence(node2, decorators);
2563
+ }
2564
+ const propTypeParam = singularClassName(typeText);
2565
+ if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
2566
+ const bound = ctx.typeBindings.get(propTypeParam);
2567
+ const childNode = buildNestedReference(bound, classFile, ctx);
2568
+ const wrapArray = has("IsArray") || isArrayType;
2569
+ const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
2570
+ return applyPresence(node2, decorators);
2571
+ }
1946
2572
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1947
2573
  if (has("ValidateNested") || typeRefName) {
2574
+ const typeArgs = genericTypeArgNames(typeNode);
1948
2575
  const childName = typeRefName ?? singularClassName(typeText);
1949
2576
  if (childName) {
1950
- const childNode = buildNestedReference(childName, classFile, ctx);
2577
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
1951
2578
  const wrapArray = has("IsArray") || isArrayType;
1952
2579
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
1953
2580
  return applyPresence(node2, decorators);
@@ -2072,10 +2699,13 @@ function baseFromType(typeText, isArrayType) {
2072
2699
  return { kind: "unknown" };
2073
2700
  }
2074
2701
  }
2075
- function buildNestedReference(className, fromFile, ctx) {
2076
- if (ctx.visiting.has(className)) {
2077
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2078
- ctx.emittedClasses.set(className, reserved);
2702
+ function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
2703
+ const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
2704
+ const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
2705
+ if (ctx.visiting.has(cacheKey)) {
2706
+ const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
2707
+ ctx.emittedClasses.set(cacheKey, reserved);
2708
+ ctx.usedSchemaNames.add(reserved);
2079
2709
  ctx.recursiveSchemas.add(reserved);
2080
2710
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2081
2711
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -2094,29 +2724,37 @@ function buildNestedReference(className, fromFile, ctx) {
2094
2724
  }
2095
2725
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2096
2726
  }
2097
- const existing = ctx.emittedClasses.get(className);
2727
+ const existing = ctx.emittedClasses.get(cacheKey);
2098
2728
  if (existing) return { kind: "ref", name: existing };
2099
- const schemaName = aliasFor(className, ctx);
2729
+ const schemaName = aliasFor(schemaBase, ctx);
2100
2730
  const resolved = findType(className, fromFile, ctx.project);
2101
2731
  if (!resolved || resolved.kind !== "class") {
2102
2732
  return { kind: "object", fields: [], passthrough: true };
2103
2733
  }
2104
- ctx.emittedClasses.set(className, schemaName);
2105
- ctx.visiting.add(className);
2734
+ const params = resolved.decl.getTypeParameters().map((p) => p.getName());
2735
+ const newBindings = [];
2736
+ params.forEach((param, i) => {
2737
+ const arg = typeArgs[i];
2738
+ if (arg) newBindings.push([param, arg]);
2739
+ });
2740
+ for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
2741
+ ctx.emittedClasses.set(cacheKey, schemaName);
2742
+ ctx.usedSchemaNames.add(schemaName);
2743
+ ctx.visiting.add(cacheKey);
2106
2744
  ctx.depth += 1;
2107
2745
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
2108
2746
  ctx.depth -= 1;
2109
- ctx.visiting.delete(className);
2747
+ ctx.visiting.delete(cacheKey);
2748
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
2110
2749
  ctx.named.set(schemaName, childNode);
2750
+ ctx.usedSchemaNames.add(schemaName);
2111
2751
  return { kind: "ref", name: schemaName };
2112
2752
  }
2113
2753
  function aliasFor(className, ctx) {
2114
2754
  const baseName = `${className}Schema`;
2115
2755
  let candidate = baseName;
2116
2756
  let i = 1;
2117
- const used = new Set(ctx.named.keys());
2118
- for (const v of ctx.emittedClasses.values()) used.add(v);
2119
- while (used.has(candidate)) {
2757
+ while (ctx.usedSchemaNames.has(candidate)) {
2120
2758
  candidate = `${baseName}_${i}`;
2121
2759
  i += 1;
2122
2760
  }
@@ -2153,6 +2791,39 @@ function messageRaw(decorator) {
2153
2791
  }
2154
2792
  return void 0;
2155
2793
  }
2794
+ function resolveDiscriminator(decorator) {
2795
+ const optsArg = decorator?.getArguments()[1];
2796
+ if (!optsArg || !Node3.isObjectLiteralExpression(optsArg)) return null;
2797
+ let discProp;
2798
+ for (const prop of optsArg.getProperties()) {
2799
+ if (Node3.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
2800
+ discProp = prop.getInitializer();
2801
+ }
2802
+ }
2803
+ if (!discProp || !Node3.isObjectLiteralExpression(discProp)) return null;
2804
+ let property = null;
2805
+ const subTypes = [];
2806
+ for (const prop of discProp.getProperties()) {
2807
+ if (!Node3.isPropertyAssignment(prop)) continue;
2808
+ const name = prop.getName();
2809
+ const init = prop.getInitializer();
2810
+ if (!init) continue;
2811
+ if (name === "property" && Node3.isStringLiteral(init)) {
2812
+ property = init.getLiteralValue();
2813
+ } else if (name === "subTypes" && Node3.isArrayLiteralExpression(init)) {
2814
+ for (const el of init.getElements()) {
2815
+ if (!Node3.isObjectLiteralExpression(el)) continue;
2816
+ for (const p of el.getProperties()) {
2817
+ if (!Node3.isPropertyAssignment(p) || p.getName() !== "name") continue;
2818
+ const nameInit = p.getInitializer();
2819
+ if (nameInit && Node3.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
2820
+ }
2821
+ }
2822
+ }
2823
+ }
2824
+ if (!property || subTypes.length === 0) return null;
2825
+ return { property, subTypes };
2826
+ }
2156
2827
  function resolveTypeFactoryName(decorator) {
2157
2828
  const arg = firstArg(decorator);
2158
2829
  if (!arg) return null;
@@ -2166,6 +2837,17 @@ function singularClassName(typeText) {
2166
2837
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2167
2838
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2168
2839
  }
2840
+ function genericTypeArgNames(typeNode) {
2841
+ if (!typeNode || !Node3.isTypeReference(typeNode)) return [];
2842
+ const names = [];
2843
+ for (const arg of typeNode.getTypeArguments()) {
2844
+ if (!Node3.isTypeReference(arg)) return [];
2845
+ const tn = arg.getTypeName();
2846
+ if (!Node3.isIdentifier(tn)) return [];
2847
+ names.push(tn.getText());
2848
+ }
2849
+ return names;
2850
+ }
2169
2851
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2170
2852
  const arg = firstArg(decorator);
2171
2853
  if (!arg) return null;
@@ -2225,6 +2907,9 @@ import {
2225
2907
 
2226
2908
  // src/discovery/enum-resolution.ts
2227
2909
  var _enumCache = /* @__PURE__ */ new WeakMap();
2910
+ function clearEnumCache(project) {
2911
+ _enumCache.delete(project);
2912
+ }
2228
2913
  function resolveEnumValues(name, sourceFile, project) {
2229
2914
  let byKey = _enumCache.get(project);
2230
2915
  if (byKey === void 0) {
@@ -2693,24 +3378,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2693
3378
  "Map",
2694
3379
  "Set"
2695
3380
  ]);
2696
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3381
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2697
3382
  if (depth <= 0) return "unknown";
2698
3383
  if (Node6.isArrayTypeNode(typeNode)) {
2699
3384
  const elementType = typeNode.getElementTypeNode();
2700
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3385
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
2701
3386
  }
2702
3387
  if (Node6.isUnionTypeNode(typeNode)) {
2703
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3388
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
2704
3389
  }
2705
3390
  if (Node6.isIntersectionTypeNode(typeNode)) {
2706
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3391
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
2707
3392
  }
2708
3393
  if (Node6.isParenthesizedTypeNode(typeNode)) {
2709
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3394
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
2710
3395
  }
2711
3396
  if (Node6.isTypeReference(typeNode)) {
2712
3397
  const typeName = typeNode.getTypeName();
2713
3398
  const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3399
+ const bound = subst.get(name);
3400
+ if (bound !== void 0) return bound;
2714
3401
  if (name === "string" || name === "number" || name === "boolean") return name;
2715
3402
  if (name === "Date") return "string";
2716
3403
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -2718,14 +3405,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2718
3405
  return "unknown";
2719
3406
  const wrapperMode = WRAPPER_TYPES[name];
2720
3407
  if (wrapperMode) {
2721
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3408
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
2722
3409
  }
2723
3410
  if (PASSTHROUGH_UTILITY.has(name)) {
2724
3411
  return typeNode.getText();
2725
3412
  }
2726
3413
  const resolved = findType(name, sourceFile, project);
2727
3414
  if (resolved) {
2728
- return expandTypeDecl(resolved, project, depth - 1);
3415
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
3416
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
2729
3417
  }
2730
3418
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
2731
3419
  return "unknown";
@@ -2738,32 +3426,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2738
3426
  if (kind === SyntaxKind3.AnyKeyword) return "unknown";
2739
3427
  return typeNode.getText();
2740
3428
  }
2741
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
3429
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
2742
3430
  const typeArgs = typeNode.getTypeArguments();
2743
3431
  const firstTypeArg = typeArgs[0];
2744
3432
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2745
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3433
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
2746
3434
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
2747
3435
  }
2748
3436
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2749
3437
  }
2750
- function expandTypeDecl(result, project, depth) {
3438
+ function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
3439
+ if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
3440
+ const params = result.decl.getTypeParameters().map((p) => p.getName());
3441
+ if (params.length === 0) return /* @__PURE__ */ new Map();
3442
+ const args = typeNode.getTypeArguments();
3443
+ const subst = /* @__PURE__ */ new Map();
3444
+ params.forEach((param, i) => {
3445
+ const arg = args[i];
3446
+ if (arg)
3447
+ subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
3448
+ });
3449
+ return subst;
3450
+ }
3451
+ function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
2751
3452
  if (depth < 0) return "unknown";
2752
3453
  switch (result.kind) {
2753
3454
  case "class":
2754
- return resolvePropertied(result.decl, result.file, project, depth);
3455
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2755
3456
  case "interface":
2756
- return resolvePropertied(result.decl, result.file, project, depth);
3457
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2757
3458
  case "typeAlias":
2758
3459
  if (result.typeNode) {
2759
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
3460
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
2760
3461
  }
2761
3462
  return result.text;
2762
3463
  case "enum":
2763
3464
  return result.members.join(" | ");
2764
3465
  }
2765
3466
  }
2766
- function resolvePropertied(decl, sourceFile, project, depth) {
3467
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2767
3468
  if (depth < 0) return "unknown";
2768
3469
  const lines = [];
2769
3470
  for (const prop of decl.getProperties()) {
@@ -2772,7 +3473,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
2772
3473
  const propTypeNode = prop.getTypeNode();
2773
3474
  let propType = "unknown";
2774
3475
  if (propTypeNode) {
2775
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
3476
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2776
3477
  }
2777
3478
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
2778
3479
  }
@@ -2821,7 +3522,7 @@ function extractParamsType(method, sourceFile, project) {
2821
3522
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
2822
3523
  }
2823
3524
  function extractResponseType(method, sourceFile, project) {
2824
- const apiResponseDecorator = method.getDecorator("ApiResponse");
3525
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2825
3526
  if (apiResponseDecorator) {
2826
3527
  const args = apiResponseDecorator.getArguments();
2827
3528
  const optsArg = args[0];
@@ -2850,6 +3551,59 @@ function extractResponseType(method, sourceFile, project) {
2850
3551
  }
2851
3552
  return "unknown";
2852
3553
  }
3554
+ function apiResponseStatus(decorator) {
3555
+ const optsArg = decorator.getArguments()[0];
3556
+ if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
3557
+ for (const prop of optsArg.getProperties()) {
3558
+ if (!Node6.isPropertyAssignment(prop)) continue;
3559
+ if (prop.getName() !== "status") continue;
3560
+ const val = prop.getInitializer();
3561
+ if (val && Node6.isNumericLiteral(val)) return Number(val.getLiteralValue());
3562
+ }
3563
+ return null;
3564
+ }
3565
+ function apiResponseTypeNode(decorator) {
3566
+ const optsArg = decorator.getArguments()[0];
3567
+ if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
3568
+ for (const prop of optsArg.getProperties()) {
3569
+ if (!Node6.isPropertyAssignment(prop)) continue;
3570
+ if (prop.getName() !== "type") continue;
3571
+ const val = prop.getInitializer();
3572
+ if (!val) return null;
3573
+ if (Node6.isArrayLiteralExpression(val)) {
3574
+ const first = val.getElements()[0];
3575
+ return first ? { node: first, isArray: true } : null;
3576
+ }
3577
+ return { node: val, isArray: false };
3578
+ }
3579
+ return null;
3580
+ }
3581
+ function extractErrorType(method, sourceFile, project) {
3582
+ for (const decorator of method.getDecorators()) {
3583
+ if (decorator.getName() !== "ApiResponse") continue;
3584
+ const status = apiResponseStatus(decorator);
3585
+ if (status === null || status < 400) continue;
3586
+ const typeInfo = apiResponseTypeNode(decorator);
3587
+ if (!typeInfo) continue;
3588
+ const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
3589
+ const type = typeInfo.isArray ? `Array<${inner}>` : inner;
3590
+ let ref = null;
3591
+ if (Node6.isIdentifier(typeInfo.node)) {
3592
+ const name = typeInfo.node.getText();
3593
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3594
+ if (localDecl?.isExported()) {
3595
+ ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
3596
+ } else {
3597
+ const resolved = resolveImportedType(name, sourceFile, project);
3598
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
3599
+ ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
3600
+ }
3601
+ }
3602
+ }
3603
+ return { type, ref };
3604
+ }
3605
+ return null;
3606
+ }
2853
3607
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
2854
3608
  if (!Node6.isIdentifier(node)) return "unknown";
2855
3609
  const name = node.getText();
@@ -2865,17 +3619,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
2865
3619
  unwrapContainers: true
2866
3620
  });
2867
3621
  }
3622
+ var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
3623
+ var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
3624
+ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
3625
+ function detectStreamElement(method) {
3626
+ const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
3627
+ let node = method.getReturnTypeNode();
3628
+ node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
3629
+ const containerEl = streamContainerElement(node);
3630
+ if (containerEl) {
3631
+ return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
3632
+ }
3633
+ if (hasSse) return node ?? null;
3634
+ return null;
3635
+ }
3636
+ function streamContainerElement(node) {
3637
+ if (!node || !Node6.isTypeReference(node)) return null;
3638
+ const typeName = node.getTypeName();
3639
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
3640
+ if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
3641
+ return node.getTypeArguments()[0] ?? null;
3642
+ }
3643
+ return null;
3644
+ }
3645
+ function unwrapNamedContainer(node, names) {
3646
+ if (!node || !Node6.isTypeReference(node)) return node;
3647
+ const typeName = node.getTypeName();
3648
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
3649
+ if (names.has(name)) {
3650
+ return node.getTypeArguments()[0] ?? node;
3651
+ }
3652
+ return node;
3653
+ }
2868
3654
  function extractDtoContract(method, sourceFile, project) {
2869
3655
  let body = extractBodyType(method, sourceFile, project);
2870
3656
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
2871
3657
  const query = extractQueryType(method, sourceFile, project);
3658
+ const streamElement = detectStreamElement(method);
3659
+ const isStream = streamElement !== null;
2872
3660
  if (filterInfo && filterInfo.source === "body") {
2873
3661
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
2874
3662
  body = body ?? bodyType;
2875
3663
  }
2876
3664
  const paramsType = extractParamsType(method, sourceFile, project);
2877
- const response = extractResponseType(method, sourceFile, project);
2878
- if (body === null && query === null && paramsType === null && response === "unknown" && filterInfo === null) {
3665
+ const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3666
+ const errorInfo = extractErrorType(method, sourceFile, project);
3667
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
2879
3668
  return null;
2880
3669
  }
2881
3670
  let bodyRef = null;
@@ -2889,12 +3678,12 @@ function extractDtoContract(method, sourceFile, project) {
2889
3678
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2890
3679
  }
2891
3680
  }
2892
- const returnTypeNode = method.getReturnTypeNode();
3681
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
2893
3682
  if (returnTypeNode) {
2894
3683
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
2895
3684
  }
2896
- if (!responseRef) {
2897
- const apiResp = method.getDecorator("ApiResponse");
3685
+ if (!responseRef && !isStream) {
3686
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2898
3687
  if (apiResp) {
2899
3688
  const args = apiResp.getArguments();
2900
3689
  const optsArg = args[0];
@@ -2936,16 +3725,19 @@ function extractDtoContract(method, sourceFile, project) {
2936
3725
  query,
2937
3726
  body,
2938
3727
  response,
3728
+ error: errorInfo?.type ?? null,
2939
3729
  params: paramsType,
2940
3730
  queryRef,
2941
3731
  bodyRef,
2942
3732
  responseRef,
3733
+ errorRef: errorInfo?.ref ?? null,
2943
3734
  filterFields: filterInfo?.fieldNames ?? null,
2944
3735
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
2945
3736
  filterSource: filterInfo?.source ?? null,
2946
3737
  formWarnings,
2947
3738
  bodySchema,
2948
- querySchema
3739
+ querySchema,
3740
+ stream: isStream
2949
3741
  };
2950
3742
  }
2951
3743
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -3063,6 +3855,7 @@ function parseDefineContractCall(callExpr) {
3063
3855
  let query = null;
3064
3856
  let body = null;
3065
3857
  let response = "unknown";
3858
+ let error = null;
3066
3859
  let bodyZodText = null;
3067
3860
  let queryZodText = null;
3068
3861
  for (const prop of optsArg.getProperties()) {
@@ -3078,25 +3871,38 @@ function parseDefineContractCall(callExpr) {
3078
3871
  bodyZodText = val.getText();
3079
3872
  } else if (propName === "response") {
3080
3873
  response = zodAstToTs(val);
3874
+ } else if (propName === "error") {
3875
+ error = zodAstToTs(val);
3081
3876
  }
3082
3877
  }
3083
- return { query, body, response, bodyZodText, queryZodText };
3878
+ return { query, body, response, error, bodyZodText, queryZodText };
3084
3879
  }
3085
3880
 
3086
3881
  // src/discovery/contracts-fast.ts
3087
3882
  async function discoverContractsFast(opts) {
3088
3883
  const { cwd, glob, tsconfig } = opts;
3089
- const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
3090
- let project;
3884
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3885
+ const project = createDiscoveryProject(tsconfigPath);
3886
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3887
+ for (const f of files) {
3888
+ project.addSourceFileAtPath(f);
3889
+ }
3890
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3891
+ return extractAllRoutes(project);
3892
+ }
3893
+ function resolveTsconfigPath(cwd, tsconfig) {
3894
+ return tsconfig ? resolve3(tsconfig) : join13(cwd, "tsconfig.json");
3895
+ }
3896
+ function createDiscoveryProject(tsconfigPath) {
3091
3897
  try {
3092
- project = new Project3({
3898
+ return new Project3({
3093
3899
  tsConfigFilePath: tsconfigPath,
3094
3900
  skipAddingFilesFromTsConfig: true,
3095
3901
  skipLoadingLibFiles: true,
3096
3902
  skipFileDependencyResolution: true
3097
3903
  });
3098
3904
  } catch {
3099
- project = new Project3({
3905
+ return new Project3({
3100
3906
  skipAddingFilesFromTsConfig: true,
3101
3907
  skipLoadingLibFiles: true,
3102
3908
  skipFileDependencyResolution: true,
@@ -3107,20 +3913,105 @@ async function discoverContractsFast(opts) {
3107
3913
  }
3108
3914
  });
3109
3915
  }
3110
- const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3111
- for (const f of files) {
3112
- project.addSourceFileAtPath(f);
3113
- }
3114
- const routes = [];
3916
+ }
3917
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
3115
3918
  setDiscoveryContext(project, {
3116
3919
  projectRoot: cwd,
3117
3920
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3118
3921
  });
3922
+ }
3923
+ function extractRoutesFrom(project, controllerPaths) {
3924
+ const routes = [];
3925
+ for (const path of controllerPaths) {
3926
+ const sourceFile = project.getSourceFile(path);
3927
+ if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
3928
+ }
3929
+ return routes;
3930
+ }
3931
+ function extractAllRoutes(project) {
3932
+ const routes = [];
3119
3933
  for (const sourceFile of project.getSourceFiles()) {
3120
3934
  routes.push(...extractFromSourceFile(sourceFile, project));
3121
3935
  }
3122
3936
  return routes;
3123
3937
  }
3938
+ var PersistentDiscovery = class _PersistentDiscovery {
3939
+ project;
3940
+ cwd;
3941
+ glob;
3942
+ /** Absolute paths of the controllers currently loaded as extraction roots. */
3943
+ controllerPaths = /* @__PURE__ */ new Set();
3944
+ constructor(project, cwd, glob) {
3945
+ this.project = project;
3946
+ this.cwd = cwd;
3947
+ this.glob = glob;
3948
+ }
3949
+ /**
3950
+ * Build the initial persistent Project: create it, glob + add all controllers,
3951
+ * bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
3952
+ */
3953
+ static async create(opts) {
3954
+ const { cwd, glob, tsconfig } = opts;
3955
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3956
+ const project = createDiscoveryProject(tsconfigPath);
3957
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3958
+ const instance = new _PersistentDiscovery(project, cwd, glob);
3959
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3960
+ for (const f of files) {
3961
+ project.addSourceFileAtPath(f);
3962
+ instance.controllerPaths.add(f);
3963
+ }
3964
+ return instance;
3965
+ }
3966
+ /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3967
+ discover() {
3968
+ return this.runExtraction();
3969
+ }
3970
+ /**
3971
+ * Re-discover after one or more files changed. Refreshes the changed file(s)
3972
+ * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3973
+ * to pick up added/removed controllers, clears the per-Project caches, then
3974
+ * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3975
+ * being exhaustive because re-globbing + refresh-on-presence covers the set.
3976
+ */
3977
+ async rediscover(changedPaths) {
3978
+ if (changedPaths) {
3979
+ for (const p of changedPaths) {
3980
+ const abs = resolve3(p);
3981
+ const sf = this.project.getSourceFile(abs);
3982
+ if (sf) {
3983
+ await sf.refreshFromFileSystem();
3984
+ }
3985
+ }
3986
+ }
3987
+ const globbed = new Set(
3988
+ await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
3989
+ );
3990
+ for (const f of globbed) {
3991
+ if (!this.controllerPaths.has(f)) {
3992
+ try {
3993
+ this.project.addSourceFileAtPath(f);
3994
+ this.controllerPaths.add(f);
3995
+ } catch {
3996
+ }
3997
+ }
3998
+ }
3999
+ for (const f of this.controllerPaths) {
4000
+ if (!globbed.has(f)) {
4001
+ const sf = this.project.getSourceFile(f);
4002
+ if (sf) this.project.removeSourceFile(sf);
4003
+ this.controllerPaths.delete(f);
4004
+ }
4005
+ }
4006
+ return this.runExtraction();
4007
+ }
4008
+ /** Clear stale per-Project caches, then extract over the controller set. */
4009
+ runExtraction() {
4010
+ clearTypeResolutionCaches(this.project);
4011
+ clearEnumCache(this.project);
4012
+ return extractRoutesFrom(this.project, this.controllerPaths);
4013
+ }
4014
+ };
3124
4015
  function decoratorStringArg(decoratorExpr) {
3125
4016
  if (!decoratorExpr) return void 0;
3126
4017
  if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -3176,6 +4067,11 @@ function resolveVerb(method) {
3176
4067
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3177
4068
  }
3178
4069
  }
4070
+ const sseDecorator = method.getDecorator("Sse");
4071
+ if (sseDecorator) {
4072
+ const pathArg = sseDecorator.getArguments()[0];
4073
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
4074
+ }
3179
4075
  return null;
3180
4076
  }
3181
4077
  function readAsDecorator(node, label) {
@@ -3218,7 +4114,17 @@ function buildRoute(args) {
3218
4114
  };
3219
4115
  }
3220
4116
  function extractContractRoute(args) {
3221
- const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
4117
+ const {
4118
+ cls,
4119
+ method,
4120
+ applyContractDecorator,
4121
+ verb,
4122
+ prefix,
4123
+ className,
4124
+ sourceFile,
4125
+ project,
4126
+ seenNames
4127
+ } = args;
3222
4128
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3223
4129
  if (!firstDecoratorArg) return null;
3224
4130
  let contractDef = null;
@@ -3228,18 +4134,19 @@ function extractContractRoute(args) {
3228
4134
  contractDef = parseDefineContractCall(firstDecoratorArg);
3229
4135
  } else if (Node8.isIdentifier(firstDecoratorArg)) {
3230
4136
  const identName = firstDecoratorArg.getText();
3231
- const varDecl = sourceFile.getVariableDeclaration(identName);
3232
- if (!varDecl) {
4137
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4138
+ if (!resolvedVar) {
3233
4139
  console.warn(
3234
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
4140
+ `[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
3235
4141
  );
3236
4142
  return null;
3237
4143
  }
4144
+ const { decl: varDecl, file: declFile } = resolvedVar;
3238
4145
  const initializer = varDecl.getInitializer();
3239
4146
  if (!initializer) return null;
3240
4147
  contractDef = parseDefineContractCall(initializer);
3241
4148
  if (contractDef && varDecl.isExported()) {
3242
- const filePath = sourceFile.getFilePath();
4149
+ const filePath = declFile.getFilePath();
3243
4150
  if (contractDef.body !== null) {
3244
4151
  bodyZodRef = { name: `${identName}.body`, filePath };
3245
4152
  }
@@ -3272,6 +4179,7 @@ function extractContractRoute(args) {
3272
4179
  query: contractDef.query,
3273
4180
  body: contractDef.body,
3274
4181
  response: contractDef.response,
4182
+ error: contractDef.error,
3275
4183
  // Path A: capture both the importable ref and the raw text. The emitter
3276
4184
  // prefers inlining the text (client-safe — re-exporting from a controller
3277
4185
  // would drag server-only deps into the client bundle).
@@ -3303,15 +4211,18 @@ function extractDtoRoute(args) {
3303
4211
  query: dtoContract?.query ?? null,
3304
4212
  body: dtoContract?.body ?? null,
3305
4213
  response: dtoContract?.response ?? "unknown",
4214
+ error: dtoContract?.error ?? null,
3306
4215
  queryRef: dtoContract?.queryRef ?? null,
3307
4216
  bodyRef: dtoContract?.bodyRef ?? null,
3308
4217
  responseRef: dtoContract?.responseRef ?? null,
4218
+ errorRef: dtoContract?.errorRef ?? null,
3309
4219
  filterFields: dtoContract?.filterFields ?? null,
3310
4220
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3311
4221
  filterSource: dtoContract?.filterSource ?? null,
3312
4222
  formWarnings: dtoContract?.formWarnings ?? [],
3313
4223
  bodySchema: dtoContract?.bodySchema ?? null,
3314
- querySchema: dtoContract?.querySchema ?? null
4224
+ querySchema: dtoContract?.querySchema ?? null,
4225
+ stream: dtoContract?.stream ?? false
3315
4226
  }
3316
4227
  });
3317
4228
  }
@@ -3335,6 +4246,7 @@ function extractFromSourceFile(sourceFile, project) {
3335
4246
  prefix,
3336
4247
  className,
3337
4248
  sourceFile,
4249
+ project,
3338
4250
  seenNames
3339
4251
  }) : extractDtoRoute({
3340
4252
  cls,
@@ -3354,8 +4266,8 @@ function extractFromSourceFile(sourceFile, project) {
3354
4266
 
3355
4267
  // src/watch/lock-file.ts
3356
4268
  import { open } from "fs/promises";
3357
- import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3358
- import { join as join12 } from "path";
4269
+ import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
4270
+ import { join as join14 } from "path";
3359
4271
  var LOCK_FILE = ".watcher.lock";
3360
4272
  function isProcessAlive(pid) {
3361
4273
  try {
@@ -3366,8 +4278,8 @@ function isProcessAlive(pid) {
3366
4278
  }
3367
4279
  }
3368
4280
  async function acquireLock(outDir) {
3369
- await mkdir8(outDir, { recursive: true });
3370
- const lockPath = join12(outDir, LOCK_FILE);
4281
+ await mkdir10(outDir, { recursive: true });
4282
+ const lockPath = join14(outDir, LOCK_FILE);
3371
4283
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3372
4284
  try {
3373
4285
  const fd = await open(lockPath, "wx");
@@ -3407,7 +4319,7 @@ async function watch(config, onChange) {
3407
4319
  if (lock === null) {
3408
4320
  let holderPid = "unknown";
3409
4321
  try {
3410
- const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
4322
+ const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
3411
4323
  const data = JSON.parse(raw);
3412
4324
  if (data.pid !== void 0) holderPid = String(data.pid);
3413
4325
  } catch {
@@ -3417,12 +4329,20 @@ async function watch(config, onChange) {
3417
4329
  );
3418
4330
  return NO_OP_WATCHER;
3419
4331
  }
4332
+ let discovery = null;
4333
+ async function getDiscovery() {
4334
+ if (discovery === null) {
4335
+ discovery = await PersistentDiscovery.create({
4336
+ cwd: config.codegen.cwd,
4337
+ glob: config.contracts.glob,
4338
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4339
+ });
4340
+ return discovery;
4341
+ }
4342
+ return discovery;
4343
+ }
3420
4344
  try {
3421
- const initialRoutes = await discoverContractsFast({
3422
- cwd: config.codegen.cwd,
3423
- glob: config.contracts.glob,
3424
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3425
- });
4345
+ const initialRoutes = (await getDiscovery()).discover();
3426
4346
  await generate(config, initialRoutes);
3427
4347
  } catch (err) {
3428
4348
  console.warn(
@@ -3435,7 +4355,7 @@ async function watch(config, onChange) {
3435
4355
  }
3436
4356
  let pagesDebounceTimer;
3437
4357
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3438
- const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
4358
+ const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
3439
4359
  ignoreInitial: true,
3440
4360
  persistent: true,
3441
4361
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3461,23 +4381,23 @@ async function watch(config, onChange) {
3461
4381
  pagesWatcher.on("change", schedulePagesRegenerate);
3462
4382
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3463
4383
  let contractsDebounceTimer;
3464
- const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
4384
+ const pendingChangedPaths = /* @__PURE__ */ new Set();
4385
+ const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
3465
4386
  ignoreInitial: true,
3466
4387
  persistent: true,
3467
4388
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3468
4389
  });
3469
- function scheduleContractsRegenerate() {
4390
+ function scheduleContractsRegenerate(changedPath) {
4391
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3470
4392
  if (contractsDebounceTimer !== void 0) {
3471
4393
  clearTimeout(contractsDebounceTimer);
3472
4394
  }
3473
4395
  contractsDebounceTimer = setTimeout(async () => {
3474
4396
  contractsDebounceTimer = void 0;
4397
+ const changed = [...pendingChangedPaths];
4398
+ pendingChangedPaths.clear();
3475
4399
  try {
3476
- const routes = await discoverContractsFast({
3477
- cwd: config.codegen.cwd,
3478
- glob: config.contracts.glob,
3479
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3480
- });
4400
+ const routes = await (await getDiscovery()).rediscover(changed);
3481
4401
  await generate(config, routes);
3482
4402
  } catch (err) {
3483
4403
  console.error(
@@ -3488,17 +4408,17 @@ async function watch(config, onChange) {
3488
4408
  onChange?.();
3489
4409
  }, config.contracts.debounceMs);
3490
4410
  }
3491
- contractsWatcher.on("add", scheduleContractsRegenerate);
3492
- contractsWatcher.on("change", scheduleContractsRegenerate);
3493
- contractsWatcher.on("unlink", scheduleContractsRegenerate);
3494
- const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
4411
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4412
+ contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4413
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4414
+ const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
3495
4415
  ignoreInitial: true,
3496
4416
  persistent: true,
3497
4417
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3498
4418
  });
3499
- formsWatcher.on("add", scheduleContractsRegenerate);
3500
- formsWatcher.on("change", scheduleContractsRegenerate);
3501
- formsWatcher.on("unlink", scheduleContractsRegenerate);
4419
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4420
+ formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4421
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
3502
4422
  return {
3503
4423
  close: async () => {
3504
4424
  if (pagesDebounceTimer !== void 0) {
@@ -3518,7 +4438,7 @@ async function watch(config, onChange) {
3518
4438
  }
3519
4439
 
3520
4440
  // src/index.ts
3521
- var VERSION = "0.4.1";
4441
+ var VERSION = "0.5.1";
3522
4442
 
3523
4443
  // src/cli/codegen.ts
3524
4444
  async function runCodegen(opts = {}) {
@@ -3547,13 +4467,13 @@ async function runCodegen(opts = {}) {
3547
4467
  // src/cli/doctor.ts
3548
4468
  import { execFileSync as execFileSync2 } from "child_process";
3549
4469
  import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
3550
- import { join as join15 } from "path";
4470
+ import { join as join17 } from "path";
3551
4471
 
3552
4472
  // src/cli/init.ts
3553
4473
  import { execFileSync } from "child_process";
3554
4474
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3555
- import { access as access2, mkdir as mkdir9, readFile as readFile4, writeFile as writeFile8 } from "fs/promises";
3556
- import { join as join14 } from "path";
4475
+ import { access as access2, mkdir as mkdir11, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
4476
+ import { join as join16 } from "path";
3557
4477
  import { createInterface } from "readline";
3558
4478
 
3559
4479
  // src/cli/patch-utils.ts
@@ -3615,7 +4535,7 @@ ${bold(title)}`);
3615
4535
  }
3616
4536
  async function readPackageJson(cwd) {
3617
4537
  try {
3618
- const raw = await readFile4(join14(cwd, "package.json"), "utf8");
4538
+ const raw = await readFile4(join16(cwd, "package.json"), "utf8");
3619
4539
  return JSON.parse(raw);
3620
4540
  } catch {
3621
4541
  return {};
@@ -3646,7 +4566,7 @@ async function detectTemplateEngine(cwd) {
3646
4566
  async function detectPackageManager(cwd) {
3647
4567
  async function exists(file) {
3648
4568
  try {
3649
- await access2(join14(cwd, file));
4569
+ await access2(join16(cwd, file));
3650
4570
  return true;
3651
4571
  } catch {
3652
4572
  return false;
@@ -3687,13 +4607,13 @@ async function writeIfNotExists(filePath, content, label) {
3687
4607
  }
3688
4608
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
3689
4609
  if (dir) {
3690
- await mkdir9(dir, { recursive: true });
4610
+ await mkdir11(dir, { recursive: true });
3691
4611
  }
3692
- await writeFile8(filePath, content, "utf8");
4612
+ await writeFile10(filePath, content, "utf8");
3693
4613
  logCreated(label);
3694
4614
  }
3695
4615
  async function handleViteConfig(cwd, framework) {
3696
- const filePath = join14(cwd, "vite.config.ts");
4616
+ const filePath = join16(cwd, "vite.config.ts");
3697
4617
  if (await fileExists2(filePath)) {
3698
4618
  const existing = await readFile4(filePath, "utf8");
3699
4619
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
@@ -3711,9 +4631,9 @@ async function handleViteConfig(cwd, framework) {
3711
4631
  }
3712
4632
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
3713
4633
  if (dir) {
3714
- await mkdir9(dir, { recursive: true });
4634
+ await mkdir11(dir, { recursive: true });
3715
4635
  }
3716
- await writeFile8(filePath, viteConfigTemplate(framework), "utf8");
4636
+ await writeFile10(filePath, viteConfigTemplate(framework), "utf8");
3717
4637
  logCreated("vite.config.ts");
3718
4638
  }
3719
4639
  async function patchGitignore(gitignorePath) {
@@ -3729,7 +4649,7 @@ async function patchGitignore(gitignorePath) {
3729
4649
  ` : `${existing}
3730
4650
  ${GITIGNORE_ENTRY}
3731
4651
  `;
3732
- await writeFile8(gitignorePath, newContent, "utf8");
4652
+ await writeFile10(gitignorePath, newContent, "utf8");
3733
4653
  logPatched(".gitignore", "added .nestjs-inertia/");
3734
4654
  }
3735
4655
  function installDeps(pkgManager, deps, dev) {
@@ -3751,7 +4671,7 @@ function installDeps(pkgManager, deps, dev) {
3751
4671
  }
3752
4672
  }
3753
4673
  async function patchPackageJsonScripts(cwd, scripts) {
3754
- const pkgPath = join14(cwd, "package.json");
4674
+ const pkgPath = join16(cwd, "package.json");
3755
4675
  let pkg = {};
3756
4676
  try {
3757
4677
  pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
@@ -3773,7 +4693,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
3773
4693
  return;
3774
4694
  }
3775
4695
  pkg.scripts = existing;
3776
- await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
4696
+ await writeFile10(pkgPath, `${JSON.stringify(pkg, null, 2)}
3777
4697
  `, "utf8");
3778
4698
  }
3779
4699
  function patchAppModule(filePath, rootView) {
@@ -4068,7 +4988,7 @@ export class HomeController {
4068
4988
  }
4069
4989
  `;
4070
4990
  function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4071
- const filePath = join14(cwd, filename);
4991
+ const filePath = join16(cwd, filename);
4072
4992
  return patchJsonFile(
4073
4993
  filePath,
4074
4994
  (json) => {
@@ -4083,7 +5003,7 @@ function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4083
5003
  );
4084
5004
  }
4085
5005
  function patchNestCliJson(cwd, shellDir) {
4086
- const filePath = join14(cwd, "nest-cli.json");
5006
+ const filePath = join16(cwd, "nest-cli.json");
4087
5007
  return patchJsonFile(filePath, (json) => {
4088
5008
  const compiler = json.compilerOptions ?? {};
4089
5009
  const assets = compiler.assets ?? [];
@@ -4106,46 +5026,46 @@ async function scaffoldFiles(ctx) {
4106
5026
  const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
4107
5027
  logSection("Scaffold files");
4108
5028
  await writeIfNotExists(
4109
- join14(cwd, "nestjs-inertia.config.ts"),
5029
+ join16(cwd, "nestjs-inertia.config.ts"),
4110
5030
  configTemplate(framework),
4111
5031
  "nestjs-inertia.config.ts"
4112
5032
  );
4113
- await writeIfNotExists(join14(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
5033
+ await writeIfNotExists(join16(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4114
5034
  await writeIfNotExists(
4115
- join14(cwd, "tsconfig.inertia.json"),
5035
+ join16(cwd, "tsconfig.inertia.json"),
4116
5036
  TSCONFIG_INERTIA_TEMPLATE,
4117
5037
  "tsconfig.inertia.json"
4118
5038
  );
4119
5039
  await writeIfNotExists(
4120
- join14(cwd, "inertia", "tsconfig.json"),
5040
+ join16(cwd, "inertia", "tsconfig.json"),
4121
5041
  INERTIA_TSCONFIG_TEMPLATE,
4122
5042
  "inertia/tsconfig.json"
4123
5043
  );
4124
5044
  await writeIfNotExists(
4125
- join14(cwd, "inertia", shellFileName),
5045
+ join16(cwd, "inertia", shellFileName),
4126
5046
  htmlShellTemplate(framework, engine),
4127
5047
  `inertia/${shellFileName}`
4128
5048
  );
4129
5049
  await handleViteConfig(cwd, framework);
4130
5050
  await writeIfNotExists(
4131
- join14(cwd, "inertia", "app", `client.${entryExt}`),
5051
+ join16(cwd, "inertia", "app", `client.${entryExt}`),
4132
5052
  entryPointTemplate(framework),
4133
5053
  `inertia/app/client.${entryExt}`
4134
5054
  );
4135
5055
  await writeIfNotExists(
4136
- join14(cwd, "inertia", "pages", `Home.${pageExt}`),
5056
+ join16(cwd, "inertia", "pages", `Home.${pageExt}`),
4137
5057
  samplePageTemplate(framework),
4138
5058
  `inertia/pages/Home.${pageExt}`
4139
5059
  );
4140
5060
  await writeIfNotExists(
4141
- join14(cwd, "src", "home.controller.ts"),
5061
+ join16(cwd, "src", "home.controller.ts"),
4142
5062
  SAMPLE_CONTROLLER,
4143
5063
  "src/home.controller.ts"
4144
5064
  );
4145
5065
  }
4146
5066
  function patchServerAppModule(ctx) {
4147
5067
  const { cwd, rootView } = ctx;
4148
- const appModulePath = join14(cwd, "src", "app.module.ts");
5068
+ const appModulePath = join16(cwd, "src", "app.module.ts");
4149
5069
  const appModuleResult = patchAppModule(appModulePath, rootView);
4150
5070
  if (appModuleResult === "patched") {
4151
5071
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -4159,7 +5079,7 @@ function patchServerAppModule(ctx) {
4159
5079
  }
4160
5080
  }
4161
5081
  function patchServerMainTs(ctx) {
4162
- const mainTsPath = join14(ctx.cwd, "src", "main.ts");
5082
+ const mainTsPath = join16(ctx.cwd, "src", "main.ts");
4163
5083
  const mainTsResult = patchMainTs(mainTsPath);
4164
5084
  if (mainTsResult === "patched") {
4165
5085
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -4194,7 +5114,7 @@ function patchBuildConfigs(ctx) {
4194
5114
  }
4195
5115
  async function patchGitignoreAndDist(ctx) {
4196
5116
  const { cwd } = ctx;
4197
- await patchGitignore(join14(cwd, ".gitignore"));
5117
+ await patchGitignore(join16(cwd, ".gitignore"));
4198
5118
  const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
4199
5119
  if (tsconfigDistResult === "patched") {
4200
5120
  logPatched("tsconfig.json", "excluded dist/ from server compilation");
@@ -4300,7 +5220,7 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4300
5220
 
4301
5221
  // src/cli/doctor.ts
4302
5222
  function checkFileExists(cwd, file) {
4303
- return existsSync(join15(cwd, file));
5223
+ return existsSync(join17(cwd, file));
4304
5224
  }
4305
5225
  function readJson(path) {
4306
5226
  try {
@@ -4336,15 +5256,15 @@ function writeJsonField(filePath, dotPath, value) {
4336
5256
  }
4337
5257
  function getPackageVersion(cwd, pkg) {
4338
5258
  try {
4339
- const pkgJson = readJson(join15(cwd, "node_modules", pkg, "package.json"));
5259
+ const pkgJson = readJson(join17(cwd, "node_modules", pkg, "package.json"));
4340
5260
  return pkgJson?.version ?? null;
4341
5261
  } catch {
4342
5262
  return null;
4343
5263
  }
4344
5264
  }
4345
5265
  function detectPkgManager(cwd) {
4346
- if (existsSync(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
4347
- if (existsSync(join15(cwd, "yarn.lock"))) return "yarn";
5266
+ if (existsSync(join17(cwd, "pnpm-lock.yaml"))) return "pnpm";
5267
+ if (existsSync(join17(cwd, "yarn.lock"))) return "yarn";
4348
5268
  return "npm";
4349
5269
  }
4350
5270
  async function runDoctor(opts) {
@@ -4382,7 +5302,7 @@ async function runDoctor(opts) {
4382
5302
  autoFix: () => runInit({ cwd })
4383
5303
  });
4384
5304
  if (foundShellDir) {
4385
- const nestCliPath = join15(cwd, "nest-cli.json");
5305
+ const nestCliPath = join17(cwd, "nest-cli.json");
4386
5306
  const nestCli = readJson(nestCliPath);
4387
5307
  const compiler = nestCli?.compilerOptions ?? {};
4388
5308
  const assets = compiler.assets ?? [];
@@ -4421,7 +5341,7 @@ async function runDoctor(opts) {
4421
5341
  fix: "Run: nestjs-codegen codegen",
4422
5342
  autoFix: () => runCodegen({ cwd })
4423
5343
  });
4424
- const tsconfigPath = join15(cwd, "tsconfig.json");
5344
+ const tsconfigPath = join17(cwd, "tsconfig.json");
4425
5345
  const tsconfig = readJson(tsconfigPath);
4426
5346
  const paths = tsconfig?.compilerOptions?.paths;
4427
5347
  checks.push({
@@ -4432,7 +5352,7 @@ async function runDoctor(opts) {
4432
5352
  });
4433
5353
  const inertiaDir = foundShellDir ?? "inertia";
4434
5354
  for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
4435
- const tsc = readJson(join15(cwd, tsconfigFile));
5355
+ const tsc = readJson(join17(cwd, tsconfigFile));
4436
5356
  if (!tsc) continue;
4437
5357
  const excl = tsc.exclude ?? [];
4438
5358
  const excludesIt = excl.includes(inertiaDir);
@@ -4458,7 +5378,7 @@ async function runDoctor(opts) {
4458
5378
  }
4459
5379
  });
4460
5380
  }
4461
- const inertiaTsconfigPath = join15(cwd, "tsconfig.inertia.json");
5381
+ const inertiaTsconfigPath = join17(cwd, "tsconfig.inertia.json");
4462
5382
  const inertiaTsconfig = readJson(inertiaTsconfigPath);
4463
5383
  checks.push({
4464
5384
  name: "tsconfig.inertia.json exists",
@@ -4510,7 +5430,7 @@ async function runDoctor(opts) {
4510
5430
  fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
4511
5431
  });
4512
5432
  }
4513
- const innerTsconfigPath = join15(cwd, "inertia", "tsconfig.json");
5433
+ const innerTsconfigPath = join17(cwd, "inertia", "tsconfig.json");
4514
5434
  checks.push({
4515
5435
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
4516
5436
  pass: existsSync(innerTsconfigPath),
@@ -4520,7 +5440,7 @@ async function runDoctor(opts) {
4520
5440
  }
4521
5441
  });
4522
5442
  if (checkFileExists(cwd, "vite.config.ts")) {
4523
- const viteContent = readFileSync4(join15(cwd, "vite.config.ts"), "utf8");
5443
+ const viteContent = readFileSync4(join17(cwd, "vite.config.ts"), "utf8");
4524
5444
  checks.push({
4525
5445
  name: "vite.config.ts has resolve.alias",
4526
5446
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -4585,7 +5505,7 @@ async function runDoctor(opts) {
4585
5505
  });
4586
5506
  }
4587
5507
  if (checkFileExists(cwd, ".gitignore")) {
4588
- const gitignorePath = join15(cwd, ".gitignore");
5508
+ const gitignorePath = join17(cwd, ".gitignore");
4589
5509
  const gitignore = readFileSync4(gitignorePath, "utf8");
4590
5510
  checks.push({
4591
5511
  name: ".gitignore includes .nestjs-inertia/",
@@ -4594,7 +5514,7 @@ async function runDoctor(opts) {
4594
5514
  autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
4595
5515
  });
4596
5516
  }
4597
- const pkgJsonPath = join15(cwd, "package.json");
5517
+ const pkgJsonPath = join17(cwd, "package.json");
4598
5518
  const pkgJson = readJson(pkgJsonPath);
4599
5519
  const scripts = pkgJson?.scripts ?? {};
4600
5520
  checks.push({