@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.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);
@@ -1371,11 +1409,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1371
1409
  await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
1372
1410
  }
1373
1411
 
1374
- // src/emit/emit-pages.ts
1412
+ // src/emit/emit-mocks.ts
1375
1413
  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 = {}) {
1414
+ import { join as join8 } from "path";
1415
+
1416
+ // src/ir/schema-node-to-json-schema.ts
1417
+ var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
1418
+ function parseLiteral(raw) {
1419
+ const t = raw.trim();
1420
+ if (t === "true") return true;
1421
+ if (t === "false") return false;
1422
+ if (t === "null") return null;
1423
+ const q = t[0];
1424
+ if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
1425
+ return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
1426
+ }
1427
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
1428
+ return Number(t);
1429
+ }
1430
+ return t;
1431
+ }
1432
+ function literalsType(values) {
1433
+ const types = new Set(values.map((v) => v === null ? "null" : typeof v));
1434
+ if (types.size === 1) {
1435
+ const only = [...types][0];
1436
+ if (only === "string") return "string";
1437
+ if (only === "number") return "number";
1438
+ if (only === "boolean") return "boolean";
1439
+ }
1440
+ return void 0;
1441
+ }
1442
+ function convert(node, ctx) {
1443
+ switch (node.kind) {
1444
+ case "string": {
1445
+ const out = { type: "string" };
1446
+ for (const c of node.checks) {
1447
+ if (c.check === "email") out.format = "email";
1448
+ else if (c.check === "url") out.format = "uri";
1449
+ else if (c.check === "uuid") out.format = "uuid";
1450
+ else if (c.check === "min") out.minLength = Number(c.value);
1451
+ else if (c.check === "max") out.maxLength = Number(c.value);
1452
+ else if (c.check === "regex") {
1453
+ const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
1454
+ out.pattern = m ? m[1] : c.pattern;
1455
+ }
1456
+ }
1457
+ return out;
1458
+ }
1459
+ case "number": {
1460
+ const out = { type: "number" };
1461
+ for (const c of node.checks) {
1462
+ if (c.check === "int") out.type = "integer";
1463
+ else if (c.check === "min") out.minimum = Number(c.value);
1464
+ else if (c.check === "max") out.maximum = Number(c.value);
1465
+ else if (c.check === "positive") out.exclusiveMinimum = 0;
1466
+ else if (c.check === "negative") out.exclusiveMaximum = 0;
1467
+ }
1468
+ return out;
1469
+ }
1470
+ case "boolean":
1471
+ return { type: "boolean" };
1472
+ case "date":
1473
+ return { type: "string", format: "date-time" };
1474
+ case "unknown":
1475
+ return node.note ? { description: node.note } : {};
1476
+ case "instanceof":
1477
+ return { type: "object", description: `instanceof ${node.ctor}` };
1478
+ case "enum": {
1479
+ const values = node.literals.map(parseLiteral);
1480
+ const t = literalsType(values);
1481
+ const out = { enum: values };
1482
+ if (t) out.type = t;
1483
+ return out;
1484
+ }
1485
+ case "literal": {
1486
+ const value = parseLiteral(node.raw);
1487
+ const out = { const: value };
1488
+ const t = literalsType([value]);
1489
+ if (t) out.type = t;
1490
+ return out;
1491
+ }
1492
+ case "union": {
1493
+ const options = node.options.map((o) => convert(o, ctx));
1494
+ const out = { oneOf: options };
1495
+ if (node.discriminator) {
1496
+ out.discriminator = { propertyName: node.discriminator };
1497
+ }
1498
+ return out;
1499
+ }
1500
+ case "object": {
1501
+ const properties = {};
1502
+ const required = [];
1503
+ for (const f of node.fields) {
1504
+ if (f.value.kind === "optional") {
1505
+ properties[f.key] = convert(f.value.inner, ctx);
1506
+ } else {
1507
+ properties[f.key] = convert(f.value, ctx);
1508
+ required.push(f.key);
1509
+ }
1510
+ }
1511
+ const out = { type: "object", properties };
1512
+ if (required.length > 0) out.required = required;
1513
+ out.additionalProperties = node.passthrough;
1514
+ return out;
1515
+ }
1516
+ case "array":
1517
+ return { type: "array", items: convert(node.element, ctx) };
1518
+ case "optional":
1519
+ return widenNullable(convert(node.inner, ctx));
1520
+ case "ref":
1521
+ case "lazyRef":
1522
+ return { $ref: `${ctx.refPrefix}${node.name}` };
1523
+ case "annotated":
1524
+ return convert(node.inner, ctx);
1525
+ }
1526
+ }
1527
+ function widenNullable(schema) {
1528
+ if (schema.$ref) {
1529
+ return { anyOf: [schema, { type: "null" }] };
1530
+ }
1531
+ if (typeof schema.type === "string") {
1532
+ return { ...schema, type: [schema.type, "null"] };
1533
+ }
1534
+ if (Array.isArray(schema.type)) {
1535
+ return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
1536
+ }
1537
+ return { anyOf: [schema, { type: "null" }] };
1538
+ }
1539
+ function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
1540
+ const named = {};
1541
+ for (const [name, node] of mod.named) {
1542
+ named[name] = convert(node, ctx);
1543
+ }
1544
+ return { root: convert(mod.root, ctx), named };
1545
+ }
1546
+
1547
+ // src/emit/mock-gen-runtime.ts
1548
+ var MOCK_GEN_RUNTIME = `
1549
+ /** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
1550
+ function makeRng(seed) {
1551
+ let a = seed >>> 0;
1552
+ return {
1553
+ next() {
1554
+ a |= 0;
1555
+ a = (a + 0x6d2b79f5) | 0;
1556
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
1557
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
1558
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
1559
+ },
1560
+ };
1561
+ }
1562
+
1563
+ function __pick(rng, items) {
1564
+ return items[Math.floor(rng.next() * items.length)];
1565
+ }
1566
+
1567
+ function __intBetween(rng, min, max) {
1568
+ return Math.floor(rng.next() * (max - min + 1)) + min;
1569
+ }
1570
+
1571
+ const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
1572
+ const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
1573
+ const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
1574
+
1575
+ function __fakeWords(rng, count) {
1576
+ let out = [];
1577
+ for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
1578
+ return out.join(' ');
1579
+ }
1580
+
1581
+ function __hex(rng, len) {
1582
+ let s = '';
1583
+ for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
1584
+ return s;
1585
+ }
1586
+
1587
+ function __fakeUuid(rng) {
1588
+ return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
1589
+ }
1590
+
1591
+ function __fakeString(rng, schema) {
1592
+ switch (schema.format) {
1593
+ case 'email':
1594
+ return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
1595
+ case 'uri':
1596
+ case 'url':
1597
+ return 'https://example.com/' + __pick(rng, __WORDS);
1598
+ case 'uuid':
1599
+ return __fakeUuid(rng);
1600
+ case 'date-time':
1601
+ return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
1602
+ default:
1603
+ return __fakeWords(rng, __intBetween(rng, 1, 3));
1604
+ }
1605
+ }
1606
+
1607
+ /** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
1608
+ function generateMock(schema, rng, defs, depth) {
1609
+ defs = defs || {};
1610
+ depth = depth || 0;
1611
+ if (schema.$ref) {
1612
+ const name = schema.$ref.replace('#/components/schemas/', '');
1613
+ const target = defs[name];
1614
+ if (!target || depth > 4) return null;
1615
+ return generateMock(target, rng, defs, depth + 1);
1616
+ }
1617
+ if ('const' in schema) return schema.const;
1618
+ if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
1619
+ if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
1620
+ if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
1621
+ let type = Array.isArray(schema.type)
1622
+ ? (schema.type.filter((t) => t !== 'null')[0] || 'null')
1623
+ : schema.type;
1624
+ switch (type) {
1625
+ case 'string':
1626
+ return __fakeString(rng, schema);
1627
+ case 'integer':
1628
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
1629
+ case 'number':
1630
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
1631
+ case 'boolean':
1632
+ return rng.next() < 0.5;
1633
+ case 'null':
1634
+ return null;
1635
+ case 'array': {
1636
+ const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
1637
+ const items = schema.items || {};
1638
+ let arr = [];
1639
+ for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
1640
+ return arr;
1641
+ }
1642
+ case 'object': {
1643
+ const out = {};
1644
+ const props = schema.properties || {};
1645
+ for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
1646
+ return out;
1647
+ }
1648
+ default:
1649
+ return {};
1650
+ }
1651
+ }
1652
+ `.trim();
1653
+
1654
+ // src/emit/emit-mocks.ts
1655
+ var REF_PREFIX = "#/components/schemas/";
1656
+ function toMswPath(path, baseUrl) {
1657
+ return `${baseUrl}${path}`;
1658
+ }
1659
+ function responseSchemaFor(route, defs) {
1660
+ const cs = route.contract.contractSource;
1661
+ if (cs.responseSchema) {
1662
+ const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
1663
+ for (const [name, node] of Object.entries(named)) {
1664
+ if (!(name in defs)) defs[name] = node;
1665
+ }
1666
+ return root;
1667
+ }
1668
+ return {};
1669
+ }
1670
+ function buildMocksFile(routes, opts = {}) {
1671
+ const seed = opts.seed ?? 1;
1672
+ const baseUrl = opts.baseUrl ?? "";
1673
+ const contracted = routes.filter((r) => r.contract);
1674
+ const defs = {};
1675
+ const handlers = [];
1676
+ for (const r of contracted) {
1677
+ const schema = responseSchemaFor(r, defs);
1678
+ const method = r.method.toLowerCase();
1679
+ const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
1680
+ const path = toMswPath(r.path, baseUrl);
1681
+ const cs = r.contract.contractSource;
1682
+ const schemaLiteral = JSON.stringify(schema);
1683
+ const pathLit = JSON.stringify(path);
1684
+ if (cs.stream) {
1685
+ handlers.push(
1686
+ [
1687
+ ` // ${r.name} (stream)`,
1688
+ ` http.${mswMethod}(${pathLit}, () => {`,
1689
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1690
+ " const body = `data: ${JSON.stringify(value)}\\n\\n`;",
1691
+ " return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
1692
+ " }),"
1693
+ ].join("\n")
1694
+ );
1695
+ } else {
1696
+ handlers.push(
1697
+ [
1698
+ ` // ${r.name}`,
1699
+ ` http.${mswMethod}(${pathLit}, () => {`,
1700
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1701
+ " return HttpResponse.json(value);",
1702
+ " }),"
1703
+ ].join("\n")
1704
+ );
1705
+ }
1706
+ }
1707
+ const lines = [
1708
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1709
+ "// MSW handlers returning deterministic, schema-shaped mock data.",
1710
+ "/* eslint-disable */",
1711
+ "// @ts-nocheck",
1712
+ "",
1713
+ "import { http, HttpResponse } from 'msw';",
1714
+ "",
1715
+ `const SEED = ${seed};`,
1716
+ "",
1717
+ "// ---------------------------------------------------------------------------",
1718
+ "// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
1719
+ "// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
1720
+ "// ---------------------------------------------------------------------------",
1721
+ MOCK_GEN_RUNTIME,
1722
+ "",
1723
+ "// Shared component schemas referenced by $ref.",
1724
+ `const DEFS = ${JSON.stringify(defs, null, 2)};`,
1725
+ "",
1726
+ "/** MSW request handlers, one per contracted route. */",
1727
+ "export const handlers = [",
1728
+ ...handlers,
1729
+ "];",
1730
+ ""
1731
+ ];
1732
+ return lines.join("\n");
1733
+ }
1734
+ async function emitMocks(routes, outDir, opts = {}) {
1378
1735
  await mkdir5(outDir, { recursive: true });
1736
+ const content = buildMocksFile(routes, opts);
1737
+ const fileName = opts.fileName ?? "mocks.ts";
1738
+ await writeFile5(join8(outDir, fileName), content, "utf8");
1739
+ }
1740
+
1741
+ // src/emit/emit-openapi.ts
1742
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1743
+ import { join as join9 } from "path";
1744
+ var REF_PREFIX2 = "#/components/schemas/";
1745
+ function toOpenApiPath(path) {
1746
+ return path.replace(/:([^/]+)/g, "{$1}");
1747
+ }
1748
+ function positionSchema(schema, tsType, components) {
1749
+ if (schema) {
1750
+ const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
1751
+ for (const [name, node] of Object.entries(named)) {
1752
+ if (!(name in components)) components[name] = node;
1753
+ }
1754
+ return root;
1755
+ }
1756
+ return tsType ? { description: tsType } : {};
1757
+ }
1758
+ function buildParameters(route) {
1759
+ const params = [];
1760
+ for (const p of route.params) {
1761
+ if (p.source === "path") {
1762
+ params.push({
1763
+ name: p.name,
1764
+ in: "path",
1765
+ required: true,
1766
+ schema: { type: "string" }
1767
+ });
1768
+ } else if (p.source === "query") {
1769
+ params.push({
1770
+ name: p.name,
1771
+ in: "query",
1772
+ required: false,
1773
+ schema: { type: "string" }
1774
+ });
1775
+ } else if (p.source === "header") {
1776
+ params.push({
1777
+ name: p.name,
1778
+ in: "header",
1779
+ required: false,
1780
+ schema: { type: "string" }
1781
+ });
1782
+ }
1783
+ }
1784
+ return params;
1785
+ }
1786
+ function buildResponses(cs, components) {
1787
+ const responses = {};
1788
+ const successSchema = positionSchema(
1789
+ // Prefer rich response IR when present; otherwise fall back to the TS type.
1790
+ cs.responseSchema ?? null,
1791
+ cs.response,
1792
+ components
1793
+ );
1794
+ const successContentType = cs.stream ? "text/event-stream" : "application/json";
1795
+ responses["200"] = {
1796
+ description: cs.stream ? "Server-sent event stream" : "Successful response",
1797
+ content: { [successContentType]: { schema: successSchema } }
1798
+ };
1799
+ const errorSchema = positionSchema(null, cs.error ?? null, components);
1800
+ const errorBody = {
1801
+ description: "Error response",
1802
+ content: { "application/json": { schema: errorSchema } }
1803
+ };
1804
+ if (cs.error || cs.errorRef) {
1805
+ responses["400"] = errorBody;
1806
+ responses.default = errorBody;
1807
+ } else {
1808
+ responses.default = {
1809
+ description: "Error response",
1810
+ content: { "application/json": { schema: {} } }
1811
+ };
1812
+ }
1813
+ return responses;
1814
+ }
1815
+ function buildOperation(route, components) {
1816
+ const cs = route.contract.contractSource;
1817
+ const op = {
1818
+ operationId: route.name,
1819
+ parameters: buildParameters(route),
1820
+ responses: buildResponses(cs, components)
1821
+ };
1822
+ const method = route.method.toUpperCase();
1823
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
1824
+ if (hasBody && (cs.bodySchema || cs.body)) {
1825
+ const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
1826
+ op.requestBody = {
1827
+ required: true,
1828
+ content: { "application/json": { schema: bodySchema } }
1829
+ };
1830
+ }
1831
+ return op;
1832
+ }
1833
+ function buildOpenApiSpec(routes, opts = {}) {
1834
+ const components = {};
1835
+ const paths = {};
1836
+ for (const route of routes) {
1837
+ if (!route.contract) continue;
1838
+ const oaPath = toOpenApiPath(route.path);
1839
+ const method = route.method.toLowerCase();
1840
+ let pathItem = paths[oaPath];
1841
+ if (!pathItem) {
1842
+ pathItem = {};
1843
+ paths[oaPath] = pathItem;
1844
+ }
1845
+ pathItem[method] = buildOperation(route, components);
1846
+ }
1847
+ const info = opts.info ?? {};
1848
+ const doc = {
1849
+ openapi: "3.1.0",
1850
+ info: {
1851
+ title: info.title ?? "NestJS API",
1852
+ version: info.version ?? "1.0.0",
1853
+ ...info.description ? { description: info.description } : {}
1854
+ },
1855
+ paths,
1856
+ components: { schemas: components }
1857
+ };
1858
+ return doc;
1859
+ }
1860
+ async function emitOpenApi(routes, outDir, opts = {}) {
1861
+ await mkdir6(outDir, { recursive: true });
1862
+ const doc = buildOpenApiSpec(routes, opts);
1863
+ const fileName = opts.fileName ?? "openapi.json";
1864
+ await writeFile6(join9(outDir, fileName), `${JSON.stringify(doc, null, 2)}
1865
+ `, "utf8");
1866
+ }
1867
+
1868
+ // src/emit/emit-pages.ts
1869
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1870
+ import { join as join10, relative as relative5 } from "path";
1871
+ async function emitPages(pages, outDir, _options = {}) {
1872
+ await mkdir7(outDir, { recursive: true });
1379
1873
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1380
1874
  const augBody = pages.map((p) => {
1381
1875
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
@@ -1394,7 +1888,7 @@ ${augBody}
1394
1888
  }
1395
1889
  ${sharedPropsBlock}}
1396
1890
  `;
1397
- await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
1891
+ await writeFile7(join10(outDir, "pages.d.ts"), content, "utf8");
1398
1892
  }
1399
1893
  function buildSharedPropsBlock(sharedProps) {
1400
1894
  if (!sharedProps) return "";
@@ -1424,12 +1918,12 @@ function needsQuotes(name) {
1424
1918
  }
1425
1919
 
1426
1920
  // src/emit/emit-routes.ts
1427
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1428
- import { join as join9 } from "path";
1921
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
1922
+ import { join as join11 } from "path";
1429
1923
  async function emitRoutes(routes, outDir) {
1430
- await mkdir6(outDir, { recursive: true });
1924
+ await mkdir8(outDir, { recursive: true });
1431
1925
  const content = buildRoutesFile(routes);
1432
- await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
1926
+ await writeFile8(join11(outDir, "routes.ts"), content, "utf8");
1433
1927
  }
1434
1928
  function buildRoutesFile(routes) {
1435
1929
  if (routes.length === 0) {
@@ -1577,24 +2071,41 @@ async function generate(config, inputRoutes = []) {
1577
2071
  });
1578
2072
  }
1579
2073
  const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
2074
+ if (hasContracts && config.openapi.enabled) {
2075
+ await emitOpenApi(routes, config.codegen.outDir, {
2076
+ fileName: config.openapi.fileName,
2077
+ info: {
2078
+ title: config.openapi.title,
2079
+ version: config.openapi.version,
2080
+ ...config.openapi.description ? { description: config.openapi.description } : {}
2081
+ }
2082
+ });
2083
+ }
2084
+ if (hasContracts && config.mocks.enabled) {
2085
+ await emitMocks(routes, config.codegen.outDir, {
2086
+ fileName: config.mocks.fileName,
2087
+ seed: config.mocks.seed,
2088
+ baseUrl: config.mocks.baseUrl
2089
+ });
2090
+ }
1580
2091
  await emitIndex(config.codegen.outDir, hasContracts, hasForms);
1581
2092
  if (extensions.length > 0) {
1582
2093
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1583
2094
  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");
2095
+ const dest = join12(config.codegen.outDir, file.path);
2096
+ await mkdir9(dirname2(dest), { recursive: true });
2097
+ await writeFile9(dest, file.contents, "utf8");
1587
2098
  }
1588
2099
  }
1589
2100
  }
1590
2101
 
1591
2102
  // src/watch/watcher.ts
1592
2103
  import { readFile as readFile3 } from "fs/promises";
1593
- import { join as join13 } from "path";
2104
+ import { join as join15 } from "path";
1594
2105
  import chokidar from "chokidar";
1595
2106
 
1596
2107
  // src/discovery/contracts-fast.ts
1597
- import { join as join11, resolve as resolve3 } from "path";
2108
+ import { join as join13, resolve as resolve3 } from "path";
1598
2109
  import fg2 from "fast-glob";
1599
2110
  import {
1600
2111
  Node as Node8,
@@ -1776,7 +2287,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
1776
2287
  }
1777
2288
  return null;
1778
2289
  }
2290
+ function resolveImportedVariable(name, sourceFile, project) {
2291
+ const local = sourceFile.getVariableDeclaration(name);
2292
+ if (local) return { decl: local, file: sourceFile };
2293
+ return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
2294
+ }
2295
+ function resolveVariableViaImports(name, sourceFile, project, seen) {
2296
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2297
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
2298
+ if (!namedImport) continue;
2299
+ const sourceName = namedImport.getName();
2300
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
2301
+ const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
2302
+ if (found) return found;
2303
+ }
2304
+ return null;
2305
+ }
2306
+ function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
2307
+ const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
2308
+ for (const candidate of candidates) {
2309
+ let importedFile = project.getSourceFile(candidate);
2310
+ if (!importedFile) {
2311
+ try {
2312
+ importedFile = project.addSourceFileAtPath(candidate);
2313
+ } catch {
2314
+ continue;
2315
+ }
2316
+ }
2317
+ const found = resolveVariableInFile(name, importedFile, project, seen);
2318
+ if (found) return found;
2319
+ }
2320
+ return null;
2321
+ }
2322
+ function resolveVariableInFile(name, file, project, seen) {
2323
+ const filePath = file.getFilePath();
2324
+ if (seen.has(filePath)) return null;
2325
+ seen.add(filePath);
2326
+ const local = file.getVariableDeclaration(name);
2327
+ if (local) return { decl: local, file };
2328
+ for (const exportDecl of file.getExportDeclarations()) {
2329
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue();
2330
+ const namedExports = exportDecl.getNamedExports();
2331
+ if (moduleSpecifier) {
2332
+ const hasStar = namedExports.length === 0;
2333
+ const reExport2 = namedExports.find(
2334
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2335
+ );
2336
+ if (!hasStar && !reExport2) continue;
2337
+ const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
2338
+ const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
2339
+ if (found) return found;
2340
+ continue;
2341
+ }
2342
+ const reExport = namedExports.find(
2343
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2344
+ );
2345
+ if (!reExport) continue;
2346
+ const sourceName = reExport.getName();
2347
+ const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
2348
+ if (viaImports) return viaImports;
2349
+ }
2350
+ return null;
2351
+ }
1779
2352
  var _findTypeCache = /* @__PURE__ */ new WeakMap();
2353
+ function clearTypeResolutionCaches(project) {
2354
+ _findTypeCache.delete(project);
2355
+ _resolveNamedRefCache.delete(project);
2356
+ }
1780
2357
  function findType(name, sourceFile, project) {
1781
2358
  let byKey = _findTypeCache.get(project);
1782
2359
  if (byKey === void 0) {
@@ -1919,7 +2496,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1919
2496
  emittedClasses: /* @__PURE__ */ new Map(),
1920
2497
  visiting: /* @__PURE__ */ new Set(),
1921
2498
  recursiveSchemas: /* @__PURE__ */ new Set(),
1922
- depth: 0
2499
+ depth: 0,
2500
+ typeBindings: /* @__PURE__ */ new Map()
1923
2501
  };
1924
2502
  const root = buildObject(classDecl, sourceFile, ctx);
1925
2503
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -1943,11 +2521,34 @@ function buildProperty(prop, classFile, ctx) {
1943
2521
  const typeNode = prop.getTypeNode();
1944
2522
  const typeText = typeNode?.getText() ?? "unknown";
1945
2523
  const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
2524
+ const discriminator = resolveDiscriminator(dec("Type"));
2525
+ if (discriminator) {
2526
+ const options = discriminator.subTypes.map(
2527
+ (name) => buildNestedReference(name, classFile, ctx)
2528
+ );
2529
+ const unionNode = {
2530
+ kind: "union",
2531
+ options,
2532
+ discriminator: discriminator.property
2533
+ };
2534
+ const wrapArray = has("IsArray") || isArrayType;
2535
+ const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
2536
+ return applyPresence(node2, decorators);
2537
+ }
2538
+ const propTypeParam = singularClassName(typeText);
2539
+ if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
2540
+ const bound = ctx.typeBindings.get(propTypeParam);
2541
+ const childNode = buildNestedReference(bound, classFile, ctx);
2542
+ const wrapArray = has("IsArray") || isArrayType;
2543
+ const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
2544
+ return applyPresence(node2, decorators);
2545
+ }
1946
2546
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1947
2547
  if (has("ValidateNested") || typeRefName) {
2548
+ const typeArgs = genericTypeArgNames(typeNode);
1948
2549
  const childName = typeRefName ?? singularClassName(typeText);
1949
2550
  if (childName) {
1950
- const childNode = buildNestedReference(childName, classFile, ctx);
2551
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
1951
2552
  const wrapArray = has("IsArray") || isArrayType;
1952
2553
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
1953
2554
  return applyPresence(node2, decorators);
@@ -2072,10 +2673,12 @@ function baseFromType(typeText, isArrayType) {
2072
2673
  return { kind: "unknown" };
2073
2674
  }
2074
2675
  }
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);
2676
+ function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
2677
+ const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
2678
+ const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
2679
+ if (ctx.visiting.has(cacheKey)) {
2680
+ const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
2681
+ ctx.emittedClasses.set(cacheKey, reserved);
2079
2682
  ctx.recursiveSchemas.add(reserved);
2080
2683
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2081
2684
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -2094,19 +2697,27 @@ function buildNestedReference(className, fromFile, ctx) {
2094
2697
  }
2095
2698
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2096
2699
  }
2097
- const existing = ctx.emittedClasses.get(className);
2700
+ const existing = ctx.emittedClasses.get(cacheKey);
2098
2701
  if (existing) return { kind: "ref", name: existing };
2099
- const schemaName = aliasFor(className, ctx);
2702
+ const schemaName = aliasFor(schemaBase, ctx);
2100
2703
  const resolved = findType(className, fromFile, ctx.project);
2101
2704
  if (!resolved || resolved.kind !== "class") {
2102
2705
  return { kind: "object", fields: [], passthrough: true };
2103
2706
  }
2104
- ctx.emittedClasses.set(className, schemaName);
2105
- ctx.visiting.add(className);
2707
+ const params = resolved.decl.getTypeParameters().map((p) => p.getName());
2708
+ const newBindings = [];
2709
+ params.forEach((param, i) => {
2710
+ const arg = typeArgs[i];
2711
+ if (arg) newBindings.push([param, arg]);
2712
+ });
2713
+ for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
2714
+ ctx.emittedClasses.set(cacheKey, schemaName);
2715
+ ctx.visiting.add(cacheKey);
2106
2716
  ctx.depth += 1;
2107
2717
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
2108
2718
  ctx.depth -= 1;
2109
- ctx.visiting.delete(className);
2719
+ ctx.visiting.delete(cacheKey);
2720
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
2110
2721
  ctx.named.set(schemaName, childNode);
2111
2722
  return { kind: "ref", name: schemaName };
2112
2723
  }
@@ -2153,6 +2764,39 @@ function messageRaw(decorator) {
2153
2764
  }
2154
2765
  return void 0;
2155
2766
  }
2767
+ function resolveDiscriminator(decorator) {
2768
+ const optsArg = decorator?.getArguments()[1];
2769
+ if (!optsArg || !Node3.isObjectLiteralExpression(optsArg)) return null;
2770
+ let discProp;
2771
+ for (const prop of optsArg.getProperties()) {
2772
+ if (Node3.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
2773
+ discProp = prop.getInitializer();
2774
+ }
2775
+ }
2776
+ if (!discProp || !Node3.isObjectLiteralExpression(discProp)) return null;
2777
+ let property = null;
2778
+ const subTypes = [];
2779
+ for (const prop of discProp.getProperties()) {
2780
+ if (!Node3.isPropertyAssignment(prop)) continue;
2781
+ const name = prop.getName();
2782
+ const init = prop.getInitializer();
2783
+ if (!init) continue;
2784
+ if (name === "property" && Node3.isStringLiteral(init)) {
2785
+ property = init.getLiteralValue();
2786
+ } else if (name === "subTypes" && Node3.isArrayLiteralExpression(init)) {
2787
+ for (const el of init.getElements()) {
2788
+ if (!Node3.isObjectLiteralExpression(el)) continue;
2789
+ for (const p of el.getProperties()) {
2790
+ if (!Node3.isPropertyAssignment(p) || p.getName() !== "name") continue;
2791
+ const nameInit = p.getInitializer();
2792
+ if (nameInit && Node3.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
2793
+ }
2794
+ }
2795
+ }
2796
+ }
2797
+ if (!property || subTypes.length === 0) return null;
2798
+ return { property, subTypes };
2799
+ }
2156
2800
  function resolveTypeFactoryName(decorator) {
2157
2801
  const arg = firstArg(decorator);
2158
2802
  if (!arg) return null;
@@ -2166,6 +2810,17 @@ function singularClassName(typeText) {
2166
2810
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2167
2811
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2168
2812
  }
2813
+ function genericTypeArgNames(typeNode) {
2814
+ if (!typeNode || !Node3.isTypeReference(typeNode)) return [];
2815
+ const names = [];
2816
+ for (const arg of typeNode.getTypeArguments()) {
2817
+ if (!Node3.isTypeReference(arg)) return [];
2818
+ const tn = arg.getTypeName();
2819
+ if (!Node3.isIdentifier(tn)) return [];
2820
+ names.push(tn.getText());
2821
+ }
2822
+ return names;
2823
+ }
2169
2824
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2170
2825
  const arg = firstArg(decorator);
2171
2826
  if (!arg) return null;
@@ -2225,6 +2880,9 @@ import {
2225
2880
 
2226
2881
  // src/discovery/enum-resolution.ts
2227
2882
  var _enumCache = /* @__PURE__ */ new WeakMap();
2883
+ function clearEnumCache(project) {
2884
+ _enumCache.delete(project);
2885
+ }
2228
2886
  function resolveEnumValues(name, sourceFile, project) {
2229
2887
  let byKey = _enumCache.get(project);
2230
2888
  if (byKey === void 0) {
@@ -2693,24 +3351,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2693
3351
  "Map",
2694
3352
  "Set"
2695
3353
  ]);
2696
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3354
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2697
3355
  if (depth <= 0) return "unknown";
2698
3356
  if (Node6.isArrayTypeNode(typeNode)) {
2699
3357
  const elementType = typeNode.getElementTypeNode();
2700
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3358
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
2701
3359
  }
2702
3360
  if (Node6.isUnionTypeNode(typeNode)) {
2703
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3361
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
2704
3362
  }
2705
3363
  if (Node6.isIntersectionTypeNode(typeNode)) {
2706
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3364
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
2707
3365
  }
2708
3366
  if (Node6.isParenthesizedTypeNode(typeNode)) {
2709
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3367
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
2710
3368
  }
2711
3369
  if (Node6.isTypeReference(typeNode)) {
2712
3370
  const typeName = typeNode.getTypeName();
2713
3371
  const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3372
+ const bound = subst.get(name);
3373
+ if (bound !== void 0) return bound;
2714
3374
  if (name === "string" || name === "number" || name === "boolean") return name;
2715
3375
  if (name === "Date") return "string";
2716
3376
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -2718,14 +3378,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2718
3378
  return "unknown";
2719
3379
  const wrapperMode = WRAPPER_TYPES[name];
2720
3380
  if (wrapperMode) {
2721
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3381
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
2722
3382
  }
2723
3383
  if (PASSTHROUGH_UTILITY.has(name)) {
2724
3384
  return typeNode.getText();
2725
3385
  }
2726
3386
  const resolved = findType(name, sourceFile, project);
2727
3387
  if (resolved) {
2728
- return expandTypeDecl(resolved, project, depth - 1);
3388
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
3389
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
2729
3390
  }
2730
3391
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
2731
3392
  return "unknown";
@@ -2738,32 +3399,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2738
3399
  if (kind === SyntaxKind3.AnyKeyword) return "unknown";
2739
3400
  return typeNode.getText();
2740
3401
  }
2741
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
3402
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
2742
3403
  const typeArgs = typeNode.getTypeArguments();
2743
3404
  const firstTypeArg = typeArgs[0];
2744
3405
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2745
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3406
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
2746
3407
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
2747
3408
  }
2748
3409
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2749
3410
  }
2750
- function expandTypeDecl(result, project, depth) {
3411
+ function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
3412
+ if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
3413
+ const params = result.decl.getTypeParameters().map((p) => p.getName());
3414
+ if (params.length === 0) return /* @__PURE__ */ new Map();
3415
+ const args = typeNode.getTypeArguments();
3416
+ const subst = /* @__PURE__ */ new Map();
3417
+ params.forEach((param, i) => {
3418
+ const arg = args[i];
3419
+ if (arg)
3420
+ subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
3421
+ });
3422
+ return subst;
3423
+ }
3424
+ function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
2751
3425
  if (depth < 0) return "unknown";
2752
3426
  switch (result.kind) {
2753
3427
  case "class":
2754
- return resolvePropertied(result.decl, result.file, project, depth);
3428
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2755
3429
  case "interface":
2756
- return resolvePropertied(result.decl, result.file, project, depth);
3430
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2757
3431
  case "typeAlias":
2758
3432
  if (result.typeNode) {
2759
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
3433
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
2760
3434
  }
2761
3435
  return result.text;
2762
3436
  case "enum":
2763
3437
  return result.members.join(" | ");
2764
3438
  }
2765
3439
  }
2766
- function resolvePropertied(decl, sourceFile, project, depth) {
3440
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2767
3441
  if (depth < 0) return "unknown";
2768
3442
  const lines = [];
2769
3443
  for (const prop of decl.getProperties()) {
@@ -2772,7 +3446,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
2772
3446
  const propTypeNode = prop.getTypeNode();
2773
3447
  let propType = "unknown";
2774
3448
  if (propTypeNode) {
2775
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
3449
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2776
3450
  }
2777
3451
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
2778
3452
  }
@@ -2821,7 +3495,7 @@ function extractParamsType(method, sourceFile, project) {
2821
3495
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
2822
3496
  }
2823
3497
  function extractResponseType(method, sourceFile, project) {
2824
- const apiResponseDecorator = method.getDecorator("ApiResponse");
3498
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2825
3499
  if (apiResponseDecorator) {
2826
3500
  const args = apiResponseDecorator.getArguments();
2827
3501
  const optsArg = args[0];
@@ -2850,6 +3524,59 @@ function extractResponseType(method, sourceFile, project) {
2850
3524
  }
2851
3525
  return "unknown";
2852
3526
  }
3527
+ function apiResponseStatus(decorator) {
3528
+ const optsArg = decorator.getArguments()[0];
3529
+ if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
3530
+ for (const prop of optsArg.getProperties()) {
3531
+ if (!Node6.isPropertyAssignment(prop)) continue;
3532
+ if (prop.getName() !== "status") continue;
3533
+ const val = prop.getInitializer();
3534
+ if (val && Node6.isNumericLiteral(val)) return Number(val.getLiteralValue());
3535
+ }
3536
+ return null;
3537
+ }
3538
+ function apiResponseTypeNode(decorator) {
3539
+ const optsArg = decorator.getArguments()[0];
3540
+ if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
3541
+ for (const prop of optsArg.getProperties()) {
3542
+ if (!Node6.isPropertyAssignment(prop)) continue;
3543
+ if (prop.getName() !== "type") continue;
3544
+ const val = prop.getInitializer();
3545
+ if (!val) return null;
3546
+ if (Node6.isArrayLiteralExpression(val)) {
3547
+ const first = val.getElements()[0];
3548
+ return first ? { node: first, isArray: true } : null;
3549
+ }
3550
+ return { node: val, isArray: false };
3551
+ }
3552
+ return null;
3553
+ }
3554
+ function extractErrorType(method, sourceFile, project) {
3555
+ for (const decorator of method.getDecorators()) {
3556
+ if (decorator.getName() !== "ApiResponse") continue;
3557
+ const status = apiResponseStatus(decorator);
3558
+ if (status === null || status < 400) continue;
3559
+ const typeInfo = apiResponseTypeNode(decorator);
3560
+ if (!typeInfo) continue;
3561
+ const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
3562
+ const type = typeInfo.isArray ? `Array<${inner}>` : inner;
3563
+ let ref = null;
3564
+ if (Node6.isIdentifier(typeInfo.node)) {
3565
+ const name = typeInfo.node.getText();
3566
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3567
+ if (localDecl?.isExported()) {
3568
+ ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
3569
+ } else {
3570
+ const resolved = resolveImportedType(name, sourceFile, project);
3571
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
3572
+ ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
3573
+ }
3574
+ }
3575
+ }
3576
+ return { type, ref };
3577
+ }
3578
+ return null;
3579
+ }
2853
3580
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
2854
3581
  if (!Node6.isIdentifier(node)) return "unknown";
2855
3582
  const name = node.getText();
@@ -2865,17 +3592,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
2865
3592
  unwrapContainers: true
2866
3593
  });
2867
3594
  }
3595
+ var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
3596
+ var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
3597
+ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
3598
+ function detectStreamElement(method) {
3599
+ const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
3600
+ let node = method.getReturnTypeNode();
3601
+ node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
3602
+ const containerEl = streamContainerElement(node);
3603
+ if (containerEl) {
3604
+ return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
3605
+ }
3606
+ if (hasSse) return node ?? null;
3607
+ return null;
3608
+ }
3609
+ function streamContainerElement(node) {
3610
+ if (!node || !Node6.isTypeReference(node)) return null;
3611
+ const typeName = node.getTypeName();
3612
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
3613
+ if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
3614
+ return node.getTypeArguments()[0] ?? null;
3615
+ }
3616
+ return null;
3617
+ }
3618
+ function unwrapNamedContainer(node, names) {
3619
+ if (!node || !Node6.isTypeReference(node)) return node;
3620
+ const typeName = node.getTypeName();
3621
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
3622
+ if (names.has(name)) {
3623
+ return node.getTypeArguments()[0] ?? node;
3624
+ }
3625
+ return node;
3626
+ }
2868
3627
  function extractDtoContract(method, sourceFile, project) {
2869
3628
  let body = extractBodyType(method, sourceFile, project);
2870
3629
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
2871
3630
  const query = extractQueryType(method, sourceFile, project);
3631
+ const streamElement = detectStreamElement(method);
3632
+ const isStream = streamElement !== null;
2872
3633
  if (filterInfo && filterInfo.source === "body") {
2873
3634
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
2874
3635
  body = body ?? bodyType;
2875
3636
  }
2876
3637
  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) {
3638
+ const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3639
+ const errorInfo = extractErrorType(method, sourceFile, project);
3640
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
2879
3641
  return null;
2880
3642
  }
2881
3643
  let bodyRef = null;
@@ -2889,12 +3651,12 @@ function extractDtoContract(method, sourceFile, project) {
2889
3651
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2890
3652
  }
2891
3653
  }
2892
- const returnTypeNode = method.getReturnTypeNode();
3654
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
2893
3655
  if (returnTypeNode) {
2894
3656
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
2895
3657
  }
2896
- if (!responseRef) {
2897
- const apiResp = method.getDecorator("ApiResponse");
3658
+ if (!responseRef && !isStream) {
3659
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2898
3660
  if (apiResp) {
2899
3661
  const args = apiResp.getArguments();
2900
3662
  const optsArg = args[0];
@@ -2936,16 +3698,19 @@ function extractDtoContract(method, sourceFile, project) {
2936
3698
  query,
2937
3699
  body,
2938
3700
  response,
3701
+ error: errorInfo?.type ?? null,
2939
3702
  params: paramsType,
2940
3703
  queryRef,
2941
3704
  bodyRef,
2942
3705
  responseRef,
3706
+ errorRef: errorInfo?.ref ?? null,
2943
3707
  filterFields: filterInfo?.fieldNames ?? null,
2944
3708
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
2945
3709
  filterSource: filterInfo?.source ?? null,
2946
3710
  formWarnings,
2947
3711
  bodySchema,
2948
- querySchema
3712
+ querySchema,
3713
+ stream: isStream
2949
3714
  };
2950
3715
  }
2951
3716
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -3063,6 +3828,7 @@ function parseDefineContractCall(callExpr) {
3063
3828
  let query = null;
3064
3829
  let body = null;
3065
3830
  let response = "unknown";
3831
+ let error = null;
3066
3832
  let bodyZodText = null;
3067
3833
  let queryZodText = null;
3068
3834
  for (const prop of optsArg.getProperties()) {
@@ -3078,25 +3844,38 @@ function parseDefineContractCall(callExpr) {
3078
3844
  bodyZodText = val.getText();
3079
3845
  } else if (propName === "response") {
3080
3846
  response = zodAstToTs(val);
3847
+ } else if (propName === "error") {
3848
+ error = zodAstToTs(val);
3081
3849
  }
3082
3850
  }
3083
- return { query, body, response, bodyZodText, queryZodText };
3851
+ return { query, body, response, error, bodyZodText, queryZodText };
3084
3852
  }
3085
3853
 
3086
3854
  // src/discovery/contracts-fast.ts
3087
3855
  async function discoverContractsFast(opts) {
3088
3856
  const { cwd, glob, tsconfig } = opts;
3089
- const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
3090
- let project;
3857
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3858
+ const project = createDiscoveryProject(tsconfigPath);
3859
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3860
+ for (const f of files) {
3861
+ project.addSourceFileAtPath(f);
3862
+ }
3863
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3864
+ return extractAllRoutes(project);
3865
+ }
3866
+ function resolveTsconfigPath(cwd, tsconfig) {
3867
+ return tsconfig ? resolve3(tsconfig) : join13(cwd, "tsconfig.json");
3868
+ }
3869
+ function createDiscoveryProject(tsconfigPath) {
3091
3870
  try {
3092
- project = new Project3({
3871
+ return new Project3({
3093
3872
  tsConfigFilePath: tsconfigPath,
3094
3873
  skipAddingFilesFromTsConfig: true,
3095
3874
  skipLoadingLibFiles: true,
3096
3875
  skipFileDependencyResolution: true
3097
3876
  });
3098
3877
  } catch {
3099
- project = new Project3({
3878
+ return new Project3({
3100
3879
  skipAddingFilesFromTsConfig: true,
3101
3880
  skipLoadingLibFiles: true,
3102
3881
  skipFileDependencyResolution: true,
@@ -3107,20 +3886,105 @@ async function discoverContractsFast(opts) {
3107
3886
  }
3108
3887
  });
3109
3888
  }
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 = [];
3889
+ }
3890
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
3115
3891
  setDiscoveryContext(project, {
3116
3892
  projectRoot: cwd,
3117
3893
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3118
3894
  });
3895
+ }
3896
+ function extractRoutesFrom(project, controllerPaths) {
3897
+ const routes = [];
3898
+ for (const path of controllerPaths) {
3899
+ const sourceFile = project.getSourceFile(path);
3900
+ if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
3901
+ }
3902
+ return routes;
3903
+ }
3904
+ function extractAllRoutes(project) {
3905
+ const routes = [];
3119
3906
  for (const sourceFile of project.getSourceFiles()) {
3120
3907
  routes.push(...extractFromSourceFile(sourceFile, project));
3121
3908
  }
3122
3909
  return routes;
3123
3910
  }
3911
+ var PersistentDiscovery = class _PersistentDiscovery {
3912
+ project;
3913
+ cwd;
3914
+ glob;
3915
+ /** Absolute paths of the controllers currently loaded as extraction roots. */
3916
+ controllerPaths = /* @__PURE__ */ new Set();
3917
+ constructor(project, cwd, glob) {
3918
+ this.project = project;
3919
+ this.cwd = cwd;
3920
+ this.glob = glob;
3921
+ }
3922
+ /**
3923
+ * Build the initial persistent Project: create it, glob + add all controllers,
3924
+ * bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
3925
+ */
3926
+ static async create(opts) {
3927
+ const { cwd, glob, tsconfig } = opts;
3928
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3929
+ const project = createDiscoveryProject(tsconfigPath);
3930
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3931
+ const instance = new _PersistentDiscovery(project, cwd, glob);
3932
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3933
+ for (const f of files) {
3934
+ project.addSourceFileAtPath(f);
3935
+ instance.controllerPaths.add(f);
3936
+ }
3937
+ return instance;
3938
+ }
3939
+ /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3940
+ discover() {
3941
+ return this.runExtraction();
3942
+ }
3943
+ /**
3944
+ * Re-discover after one or more files changed. Refreshes the changed file(s)
3945
+ * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3946
+ * to pick up added/removed controllers, clears the per-Project caches, then
3947
+ * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3948
+ * being exhaustive because re-globbing + refresh-on-presence covers the set.
3949
+ */
3950
+ async rediscover(changedPaths) {
3951
+ if (changedPaths) {
3952
+ for (const p of changedPaths) {
3953
+ const abs = resolve3(p);
3954
+ const sf = this.project.getSourceFile(abs);
3955
+ if (sf) {
3956
+ await sf.refreshFromFileSystem();
3957
+ }
3958
+ }
3959
+ }
3960
+ const globbed = new Set(
3961
+ await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
3962
+ );
3963
+ for (const f of globbed) {
3964
+ if (!this.controllerPaths.has(f)) {
3965
+ try {
3966
+ this.project.addSourceFileAtPath(f);
3967
+ this.controllerPaths.add(f);
3968
+ } catch {
3969
+ }
3970
+ }
3971
+ }
3972
+ for (const f of this.controllerPaths) {
3973
+ if (!globbed.has(f)) {
3974
+ const sf = this.project.getSourceFile(f);
3975
+ if (sf) this.project.removeSourceFile(sf);
3976
+ this.controllerPaths.delete(f);
3977
+ }
3978
+ }
3979
+ return this.runExtraction();
3980
+ }
3981
+ /** Clear stale per-Project caches, then extract over the controller set. */
3982
+ runExtraction() {
3983
+ clearTypeResolutionCaches(this.project);
3984
+ clearEnumCache(this.project);
3985
+ return extractRoutesFrom(this.project, this.controllerPaths);
3986
+ }
3987
+ };
3124
3988
  function decoratorStringArg(decoratorExpr) {
3125
3989
  if (!decoratorExpr) return void 0;
3126
3990
  if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -3176,6 +4040,11 @@ function resolveVerb(method) {
3176
4040
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3177
4041
  }
3178
4042
  }
4043
+ const sseDecorator = method.getDecorator("Sse");
4044
+ if (sseDecorator) {
4045
+ const pathArg = sseDecorator.getArguments()[0];
4046
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
4047
+ }
3179
4048
  return null;
3180
4049
  }
3181
4050
  function readAsDecorator(node, label) {
@@ -3218,7 +4087,17 @@ function buildRoute(args) {
3218
4087
  };
3219
4088
  }
3220
4089
  function extractContractRoute(args) {
3221
- const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
4090
+ const {
4091
+ cls,
4092
+ method,
4093
+ applyContractDecorator,
4094
+ verb,
4095
+ prefix,
4096
+ className,
4097
+ sourceFile,
4098
+ project,
4099
+ seenNames
4100
+ } = args;
3222
4101
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3223
4102
  if (!firstDecoratorArg) return null;
3224
4103
  let contractDef = null;
@@ -3228,18 +4107,19 @@ function extractContractRoute(args) {
3228
4107
  contractDef = parseDefineContractCall(firstDecoratorArg);
3229
4108
  } else if (Node8.isIdentifier(firstDecoratorArg)) {
3230
4109
  const identName = firstDecoratorArg.getText();
3231
- const varDecl = sourceFile.getVariableDeclaration(identName);
3232
- if (!varDecl) {
4110
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4111
+ if (!resolvedVar) {
3233
4112
  console.warn(
3234
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
4113
+ `[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
3235
4114
  );
3236
4115
  return null;
3237
4116
  }
4117
+ const { decl: varDecl, file: declFile } = resolvedVar;
3238
4118
  const initializer = varDecl.getInitializer();
3239
4119
  if (!initializer) return null;
3240
4120
  contractDef = parseDefineContractCall(initializer);
3241
4121
  if (contractDef && varDecl.isExported()) {
3242
- const filePath = sourceFile.getFilePath();
4122
+ const filePath = declFile.getFilePath();
3243
4123
  if (contractDef.body !== null) {
3244
4124
  bodyZodRef = { name: `${identName}.body`, filePath };
3245
4125
  }
@@ -3272,6 +4152,7 @@ function extractContractRoute(args) {
3272
4152
  query: contractDef.query,
3273
4153
  body: contractDef.body,
3274
4154
  response: contractDef.response,
4155
+ error: contractDef.error,
3275
4156
  // Path A: capture both the importable ref and the raw text. The emitter
3276
4157
  // prefers inlining the text (client-safe — re-exporting from a controller
3277
4158
  // would drag server-only deps into the client bundle).
@@ -3303,15 +4184,18 @@ function extractDtoRoute(args) {
3303
4184
  query: dtoContract?.query ?? null,
3304
4185
  body: dtoContract?.body ?? null,
3305
4186
  response: dtoContract?.response ?? "unknown",
4187
+ error: dtoContract?.error ?? null,
3306
4188
  queryRef: dtoContract?.queryRef ?? null,
3307
4189
  bodyRef: dtoContract?.bodyRef ?? null,
3308
4190
  responseRef: dtoContract?.responseRef ?? null,
4191
+ errorRef: dtoContract?.errorRef ?? null,
3309
4192
  filterFields: dtoContract?.filterFields ?? null,
3310
4193
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3311
4194
  filterSource: dtoContract?.filterSource ?? null,
3312
4195
  formWarnings: dtoContract?.formWarnings ?? [],
3313
4196
  bodySchema: dtoContract?.bodySchema ?? null,
3314
- querySchema: dtoContract?.querySchema ?? null
4197
+ querySchema: dtoContract?.querySchema ?? null,
4198
+ stream: dtoContract?.stream ?? false
3315
4199
  }
3316
4200
  });
3317
4201
  }
@@ -3335,6 +4219,7 @@ function extractFromSourceFile(sourceFile, project) {
3335
4219
  prefix,
3336
4220
  className,
3337
4221
  sourceFile,
4222
+ project,
3338
4223
  seenNames
3339
4224
  }) : extractDtoRoute({
3340
4225
  cls,
@@ -3354,8 +4239,8 @@ function extractFromSourceFile(sourceFile, project) {
3354
4239
 
3355
4240
  // src/watch/lock-file.ts
3356
4241
  import { open } from "fs/promises";
3357
- import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3358
- import { join as join12 } from "path";
4242
+ import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
4243
+ import { join as join14 } from "path";
3359
4244
  var LOCK_FILE = ".watcher.lock";
3360
4245
  function isProcessAlive(pid) {
3361
4246
  try {
@@ -3366,8 +4251,8 @@ function isProcessAlive(pid) {
3366
4251
  }
3367
4252
  }
3368
4253
  async function acquireLock(outDir) {
3369
- await mkdir8(outDir, { recursive: true });
3370
- const lockPath = join12(outDir, LOCK_FILE);
4254
+ await mkdir10(outDir, { recursive: true });
4255
+ const lockPath = join14(outDir, LOCK_FILE);
3371
4256
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3372
4257
  try {
3373
4258
  const fd = await open(lockPath, "wx");
@@ -3407,7 +4292,7 @@ async function watch(config, onChange) {
3407
4292
  if (lock === null) {
3408
4293
  let holderPid = "unknown";
3409
4294
  try {
3410
- const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
4295
+ const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
3411
4296
  const data = JSON.parse(raw);
3412
4297
  if (data.pid !== void 0) holderPid = String(data.pid);
3413
4298
  } catch {
@@ -3417,12 +4302,20 @@ async function watch(config, onChange) {
3417
4302
  );
3418
4303
  return NO_OP_WATCHER;
3419
4304
  }
4305
+ let discovery = null;
4306
+ async function getDiscovery() {
4307
+ if (discovery === null) {
4308
+ discovery = await PersistentDiscovery.create({
4309
+ cwd: config.codegen.cwd,
4310
+ glob: config.contracts.glob,
4311
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4312
+ });
4313
+ return discovery;
4314
+ }
4315
+ return discovery;
4316
+ }
3420
4317
  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
- });
4318
+ const initialRoutes = (await getDiscovery()).discover();
3426
4319
  await generate(config, initialRoutes);
3427
4320
  } catch (err) {
3428
4321
  console.warn(
@@ -3435,7 +4328,7 @@ async function watch(config, onChange) {
3435
4328
  }
3436
4329
  let pagesDebounceTimer;
3437
4330
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3438
- const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
4331
+ const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
3439
4332
  ignoreInitial: true,
3440
4333
  persistent: true,
3441
4334
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3461,23 +4354,23 @@ async function watch(config, onChange) {
3461
4354
  pagesWatcher.on("change", schedulePagesRegenerate);
3462
4355
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3463
4356
  let contractsDebounceTimer;
3464
- const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
4357
+ const pendingChangedPaths = /* @__PURE__ */ new Set();
4358
+ const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
3465
4359
  ignoreInitial: true,
3466
4360
  persistent: true,
3467
4361
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3468
4362
  });
3469
- function scheduleContractsRegenerate() {
4363
+ function scheduleContractsRegenerate(changedPath) {
4364
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3470
4365
  if (contractsDebounceTimer !== void 0) {
3471
4366
  clearTimeout(contractsDebounceTimer);
3472
4367
  }
3473
4368
  contractsDebounceTimer = setTimeout(async () => {
3474
4369
  contractsDebounceTimer = void 0;
4370
+ const changed = [...pendingChangedPaths];
4371
+ pendingChangedPaths.clear();
3475
4372
  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
- });
4373
+ const routes = await (await getDiscovery()).rediscover(changed);
3481
4374
  await generate(config, routes);
3482
4375
  } catch (err) {
3483
4376
  console.error(
@@ -3488,17 +4381,17 @@ async function watch(config, onChange) {
3488
4381
  onChange?.();
3489
4382
  }, config.contracts.debounceMs);
3490
4383
  }
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), {
4384
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4385
+ contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4386
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4387
+ const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
3495
4388
  ignoreInitial: true,
3496
4389
  persistent: true,
3497
4390
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3498
4391
  });
3499
- formsWatcher.on("add", scheduleContractsRegenerate);
3500
- formsWatcher.on("change", scheduleContractsRegenerate);
3501
- formsWatcher.on("unlink", scheduleContractsRegenerate);
4392
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4393
+ formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4394
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
3502
4395
  return {
3503
4396
  close: async () => {
3504
4397
  if (pagesDebounceTimer !== void 0) {
@@ -3518,7 +4411,7 @@ async function watch(config, onChange) {
3518
4411
  }
3519
4412
 
3520
4413
  // src/index.ts
3521
- var VERSION = "0.4.1";
4414
+ var VERSION = "0.5.0";
3522
4415
 
3523
4416
  // src/cli/codegen.ts
3524
4417
  async function runCodegen(opts = {}) {
@@ -3547,13 +4440,13 @@ async function runCodegen(opts = {}) {
3547
4440
  // src/cli/doctor.ts
3548
4441
  import { execFileSync as execFileSync2 } from "child_process";
3549
4442
  import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
3550
- import { join as join15 } from "path";
4443
+ import { join as join17 } from "path";
3551
4444
 
3552
4445
  // src/cli/init.ts
3553
4446
  import { execFileSync } from "child_process";
3554
4447
  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";
4448
+ import { access as access2, mkdir as mkdir11, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
4449
+ import { join as join16 } from "path";
3557
4450
  import { createInterface } from "readline";
3558
4451
 
3559
4452
  // src/cli/patch-utils.ts
@@ -3615,7 +4508,7 @@ ${bold(title)}`);
3615
4508
  }
3616
4509
  async function readPackageJson(cwd) {
3617
4510
  try {
3618
- const raw = await readFile4(join14(cwd, "package.json"), "utf8");
4511
+ const raw = await readFile4(join16(cwd, "package.json"), "utf8");
3619
4512
  return JSON.parse(raw);
3620
4513
  } catch {
3621
4514
  return {};
@@ -3646,7 +4539,7 @@ async function detectTemplateEngine(cwd) {
3646
4539
  async function detectPackageManager(cwd) {
3647
4540
  async function exists(file) {
3648
4541
  try {
3649
- await access2(join14(cwd, file));
4542
+ await access2(join16(cwd, file));
3650
4543
  return true;
3651
4544
  } catch {
3652
4545
  return false;
@@ -3687,13 +4580,13 @@ async function writeIfNotExists(filePath, content, label) {
3687
4580
  }
3688
4581
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
3689
4582
  if (dir) {
3690
- await mkdir9(dir, { recursive: true });
4583
+ await mkdir11(dir, { recursive: true });
3691
4584
  }
3692
- await writeFile8(filePath, content, "utf8");
4585
+ await writeFile10(filePath, content, "utf8");
3693
4586
  logCreated(label);
3694
4587
  }
3695
4588
  async function handleViteConfig(cwd, framework) {
3696
- const filePath = join14(cwd, "vite.config.ts");
4589
+ const filePath = join16(cwd, "vite.config.ts");
3697
4590
  if (await fileExists2(filePath)) {
3698
4591
  const existing = await readFile4(filePath, "utf8");
3699
4592
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
@@ -3711,9 +4604,9 @@ async function handleViteConfig(cwd, framework) {
3711
4604
  }
3712
4605
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
3713
4606
  if (dir) {
3714
- await mkdir9(dir, { recursive: true });
4607
+ await mkdir11(dir, { recursive: true });
3715
4608
  }
3716
- await writeFile8(filePath, viteConfigTemplate(framework), "utf8");
4609
+ await writeFile10(filePath, viteConfigTemplate(framework), "utf8");
3717
4610
  logCreated("vite.config.ts");
3718
4611
  }
3719
4612
  async function patchGitignore(gitignorePath) {
@@ -3729,7 +4622,7 @@ async function patchGitignore(gitignorePath) {
3729
4622
  ` : `${existing}
3730
4623
  ${GITIGNORE_ENTRY}
3731
4624
  `;
3732
- await writeFile8(gitignorePath, newContent, "utf8");
4625
+ await writeFile10(gitignorePath, newContent, "utf8");
3733
4626
  logPatched(".gitignore", "added .nestjs-inertia/");
3734
4627
  }
3735
4628
  function installDeps(pkgManager, deps, dev) {
@@ -3751,7 +4644,7 @@ function installDeps(pkgManager, deps, dev) {
3751
4644
  }
3752
4645
  }
3753
4646
  async function patchPackageJsonScripts(cwd, scripts) {
3754
- const pkgPath = join14(cwd, "package.json");
4647
+ const pkgPath = join16(cwd, "package.json");
3755
4648
  let pkg = {};
3756
4649
  try {
3757
4650
  pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
@@ -3773,7 +4666,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
3773
4666
  return;
3774
4667
  }
3775
4668
  pkg.scripts = existing;
3776
- await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
4669
+ await writeFile10(pkgPath, `${JSON.stringify(pkg, null, 2)}
3777
4670
  `, "utf8");
3778
4671
  }
3779
4672
  function patchAppModule(filePath, rootView) {
@@ -4068,7 +4961,7 @@ export class HomeController {
4068
4961
  }
4069
4962
  `;
4070
4963
  function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4071
- const filePath = join14(cwd, filename);
4964
+ const filePath = join16(cwd, filename);
4072
4965
  return patchJsonFile(
4073
4966
  filePath,
4074
4967
  (json) => {
@@ -4083,7 +4976,7 @@ function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4083
4976
  );
4084
4977
  }
4085
4978
  function patchNestCliJson(cwd, shellDir) {
4086
- const filePath = join14(cwd, "nest-cli.json");
4979
+ const filePath = join16(cwd, "nest-cli.json");
4087
4980
  return patchJsonFile(filePath, (json) => {
4088
4981
  const compiler = json.compilerOptions ?? {};
4089
4982
  const assets = compiler.assets ?? [];
@@ -4106,46 +4999,46 @@ async function scaffoldFiles(ctx) {
4106
4999
  const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
4107
5000
  logSection("Scaffold files");
4108
5001
  await writeIfNotExists(
4109
- join14(cwd, "nestjs-inertia.config.ts"),
5002
+ join16(cwd, "nestjs-inertia.config.ts"),
4110
5003
  configTemplate(framework),
4111
5004
  "nestjs-inertia.config.ts"
4112
5005
  );
4113
- await writeIfNotExists(join14(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
5006
+ await writeIfNotExists(join16(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4114
5007
  await writeIfNotExists(
4115
- join14(cwd, "tsconfig.inertia.json"),
5008
+ join16(cwd, "tsconfig.inertia.json"),
4116
5009
  TSCONFIG_INERTIA_TEMPLATE,
4117
5010
  "tsconfig.inertia.json"
4118
5011
  );
4119
5012
  await writeIfNotExists(
4120
- join14(cwd, "inertia", "tsconfig.json"),
5013
+ join16(cwd, "inertia", "tsconfig.json"),
4121
5014
  INERTIA_TSCONFIG_TEMPLATE,
4122
5015
  "inertia/tsconfig.json"
4123
5016
  );
4124
5017
  await writeIfNotExists(
4125
- join14(cwd, "inertia", shellFileName),
5018
+ join16(cwd, "inertia", shellFileName),
4126
5019
  htmlShellTemplate(framework, engine),
4127
5020
  `inertia/${shellFileName}`
4128
5021
  );
4129
5022
  await handleViteConfig(cwd, framework);
4130
5023
  await writeIfNotExists(
4131
- join14(cwd, "inertia", "app", `client.${entryExt}`),
5024
+ join16(cwd, "inertia", "app", `client.${entryExt}`),
4132
5025
  entryPointTemplate(framework),
4133
5026
  `inertia/app/client.${entryExt}`
4134
5027
  );
4135
5028
  await writeIfNotExists(
4136
- join14(cwd, "inertia", "pages", `Home.${pageExt}`),
5029
+ join16(cwd, "inertia", "pages", `Home.${pageExt}`),
4137
5030
  samplePageTemplate(framework),
4138
5031
  `inertia/pages/Home.${pageExt}`
4139
5032
  );
4140
5033
  await writeIfNotExists(
4141
- join14(cwd, "src", "home.controller.ts"),
5034
+ join16(cwd, "src", "home.controller.ts"),
4142
5035
  SAMPLE_CONTROLLER,
4143
5036
  "src/home.controller.ts"
4144
5037
  );
4145
5038
  }
4146
5039
  function patchServerAppModule(ctx) {
4147
5040
  const { cwd, rootView } = ctx;
4148
- const appModulePath = join14(cwd, "src", "app.module.ts");
5041
+ const appModulePath = join16(cwd, "src", "app.module.ts");
4149
5042
  const appModuleResult = patchAppModule(appModulePath, rootView);
4150
5043
  if (appModuleResult === "patched") {
4151
5044
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -4159,7 +5052,7 @@ function patchServerAppModule(ctx) {
4159
5052
  }
4160
5053
  }
4161
5054
  function patchServerMainTs(ctx) {
4162
- const mainTsPath = join14(ctx.cwd, "src", "main.ts");
5055
+ const mainTsPath = join16(ctx.cwd, "src", "main.ts");
4163
5056
  const mainTsResult = patchMainTs(mainTsPath);
4164
5057
  if (mainTsResult === "patched") {
4165
5058
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -4194,7 +5087,7 @@ function patchBuildConfigs(ctx) {
4194
5087
  }
4195
5088
  async function patchGitignoreAndDist(ctx) {
4196
5089
  const { cwd } = ctx;
4197
- await patchGitignore(join14(cwd, ".gitignore"));
5090
+ await patchGitignore(join16(cwd, ".gitignore"));
4198
5091
  const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
4199
5092
  if (tsconfigDistResult === "patched") {
4200
5093
  logPatched("tsconfig.json", "excluded dist/ from server compilation");
@@ -4300,7 +5193,7 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4300
5193
 
4301
5194
  // src/cli/doctor.ts
4302
5195
  function checkFileExists(cwd, file) {
4303
- return existsSync(join15(cwd, file));
5196
+ return existsSync(join17(cwd, file));
4304
5197
  }
4305
5198
  function readJson(path) {
4306
5199
  try {
@@ -4336,15 +5229,15 @@ function writeJsonField(filePath, dotPath, value) {
4336
5229
  }
4337
5230
  function getPackageVersion(cwd, pkg) {
4338
5231
  try {
4339
- const pkgJson = readJson(join15(cwd, "node_modules", pkg, "package.json"));
5232
+ const pkgJson = readJson(join17(cwd, "node_modules", pkg, "package.json"));
4340
5233
  return pkgJson?.version ?? null;
4341
5234
  } catch {
4342
5235
  return null;
4343
5236
  }
4344
5237
  }
4345
5238
  function detectPkgManager(cwd) {
4346
- if (existsSync(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
4347
- if (existsSync(join15(cwd, "yarn.lock"))) return "yarn";
5239
+ if (existsSync(join17(cwd, "pnpm-lock.yaml"))) return "pnpm";
5240
+ if (existsSync(join17(cwd, "yarn.lock"))) return "yarn";
4348
5241
  return "npm";
4349
5242
  }
4350
5243
  async function runDoctor(opts) {
@@ -4382,7 +5275,7 @@ async function runDoctor(opts) {
4382
5275
  autoFix: () => runInit({ cwd })
4383
5276
  });
4384
5277
  if (foundShellDir) {
4385
- const nestCliPath = join15(cwd, "nest-cli.json");
5278
+ const nestCliPath = join17(cwd, "nest-cli.json");
4386
5279
  const nestCli = readJson(nestCliPath);
4387
5280
  const compiler = nestCli?.compilerOptions ?? {};
4388
5281
  const assets = compiler.assets ?? [];
@@ -4421,7 +5314,7 @@ async function runDoctor(opts) {
4421
5314
  fix: "Run: nestjs-codegen codegen",
4422
5315
  autoFix: () => runCodegen({ cwd })
4423
5316
  });
4424
- const tsconfigPath = join15(cwd, "tsconfig.json");
5317
+ const tsconfigPath = join17(cwd, "tsconfig.json");
4425
5318
  const tsconfig = readJson(tsconfigPath);
4426
5319
  const paths = tsconfig?.compilerOptions?.paths;
4427
5320
  checks.push({
@@ -4432,7 +5325,7 @@ async function runDoctor(opts) {
4432
5325
  });
4433
5326
  const inertiaDir = foundShellDir ?? "inertia";
4434
5327
  for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
4435
- const tsc = readJson(join15(cwd, tsconfigFile));
5328
+ const tsc = readJson(join17(cwd, tsconfigFile));
4436
5329
  if (!tsc) continue;
4437
5330
  const excl = tsc.exclude ?? [];
4438
5331
  const excludesIt = excl.includes(inertiaDir);
@@ -4458,7 +5351,7 @@ async function runDoctor(opts) {
4458
5351
  }
4459
5352
  });
4460
5353
  }
4461
- const inertiaTsconfigPath = join15(cwd, "tsconfig.inertia.json");
5354
+ const inertiaTsconfigPath = join17(cwd, "tsconfig.inertia.json");
4462
5355
  const inertiaTsconfig = readJson(inertiaTsconfigPath);
4463
5356
  checks.push({
4464
5357
  name: "tsconfig.inertia.json exists",
@@ -4510,7 +5403,7 @@ async function runDoctor(opts) {
4510
5403
  fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
4511
5404
  });
4512
5405
  }
4513
- const innerTsconfigPath = join15(cwd, "inertia", "tsconfig.json");
5406
+ const innerTsconfigPath = join17(cwd, "inertia", "tsconfig.json");
4514
5407
  checks.push({
4515
5408
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
4516
5409
  pass: existsSync(innerTsconfigPath),
@@ -4520,7 +5413,7 @@ async function runDoctor(opts) {
4520
5413
  }
4521
5414
  });
4522
5415
  if (checkFileExists(cwd, "vite.config.ts")) {
4523
- const viteContent = readFileSync4(join15(cwd, "vite.config.ts"), "utf8");
5416
+ const viteContent = readFileSync4(join17(cwd, "vite.config.ts"), "utf8");
4524
5417
  checks.push({
4525
5418
  name: "vite.config.ts has resolve.alias",
4526
5419
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -4585,7 +5478,7 @@ async function runDoctor(opts) {
4585
5478
  });
4586
5479
  }
4587
5480
  if (checkFileExists(cwd, ".gitignore")) {
4588
- const gitignorePath = join15(cwd, ".gitignore");
5481
+ const gitignorePath = join17(cwd, ".gitignore");
4589
5482
  const gitignore = readFileSync4(gitignorePath, "utf8");
4590
5483
  checks.push({
4591
5484
  name: ".gitignore includes .nestjs-inertia/",
@@ -4594,7 +5487,7 @@ async function runDoctor(opts) {
4594
5487
  autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
4595
5488
  });
4596
5489
  }
4597
- const pkgJsonPath = join15(cwd, "package.json");
5490
+ const pkgJsonPath = join17(cwd, "package.json");
4598
5491
  const pkgJson = readJson(pkgJsonPath);
4599
5492
  const scripts = pkgJson?.scripts ?? {};
4600
5493
  checks.push({