@dudousxd/nestjs-codegen 0.4.0 → 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,10 +2287,85 @@ 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
+ }
2352
+ var _findTypeCache = /* @__PURE__ */ new WeakMap();
2353
+ function clearTypeResolutionCaches(project) {
2354
+ _findTypeCache.delete(project);
2355
+ _resolveNamedRefCache.delete(project);
2356
+ }
1779
2357
  function findType(name, sourceFile, project) {
2358
+ let byKey = _findTypeCache.get(project);
2359
+ if (byKey === void 0) {
2360
+ byKey = /* @__PURE__ */ new Map();
2361
+ _findTypeCache.set(project, byKey);
2362
+ }
2363
+ const key = `${sourceFile.getFilePath()}\0${name}`;
2364
+ if (byKey.has(key)) return byKey.get(key) ?? null;
1780
2365
  const local = findTypeInFile(name, sourceFile);
1781
- if (local) return local;
1782
- return resolveImportedType(name, sourceFile, project);
2366
+ const result = local ?? resolveImportedType(name, sourceFile, project);
2367
+ byKey.set(key, result);
2368
+ return result;
1783
2369
  }
1784
2370
  var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
1785
2371
  function _localDeclForKinds(name, file, kinds) {
@@ -1816,6 +2402,26 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1816
2402
  if (_NON_REF_NAMES.has(refName)) return null;
1817
2403
  name = refName;
1818
2404
  }
2405
+ return _resolveNamedRef(name, sourceFile, project, opts);
2406
+ }
2407
+ var _resolveNamedRefCache = /* @__PURE__ */ new WeakMap();
2408
+ function _resolveNamedRef(name, sourceFile, project, opts) {
2409
+ let byKey = _resolveNamedRefCache.get(project);
2410
+ if (byKey === void 0) {
2411
+ byKey = /* @__PURE__ */ new Map();
2412
+ _resolveNamedRefCache.set(project, byKey);
2413
+ }
2414
+ const kindsKey = [...opts.kinds].sort().join(",");
2415
+ const key = `${sourceFile.getFilePath()}\0${name}\0${kindsKey}\0${opts.allowBareSpecifier ? 1 : 0}`;
2416
+ if (byKey.has(key)) {
2417
+ const cached = byKey.get(key) ?? null;
2418
+ return cached ? { ...cached } : null;
2419
+ }
2420
+ const computed = _computeNamedRef(name, sourceFile, project, opts);
2421
+ byKey.set(key, computed);
2422
+ return computed ? { ...computed } : null;
2423
+ }
2424
+ function _computeNamedRef(name, sourceFile, project, opts) {
1819
2425
  if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
1820
2426
  return { name, filePath: sourceFile.getFilePath() };
1821
2427
  }
@@ -1890,7 +2496,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1890
2496
  emittedClasses: /* @__PURE__ */ new Map(),
1891
2497
  visiting: /* @__PURE__ */ new Set(),
1892
2498
  recursiveSchemas: /* @__PURE__ */ new Set(),
1893
- depth: 0
2499
+ depth: 0,
2500
+ typeBindings: /* @__PURE__ */ new Map()
1894
2501
  };
1895
2502
  const root = buildObject(classDecl, sourceFile, ctx);
1896
2503
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -1914,11 +2521,34 @@ function buildProperty(prop, classFile, ctx) {
1914
2521
  const typeNode = prop.getTypeNode();
1915
2522
  const typeText = typeNode?.getText() ?? "unknown";
1916
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
+ }
1917
2546
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1918
2547
  if (has("ValidateNested") || typeRefName) {
2548
+ const typeArgs = genericTypeArgNames(typeNode);
1919
2549
  const childName = typeRefName ?? singularClassName(typeText);
1920
2550
  if (childName) {
1921
- const childNode = buildNestedReference(childName, classFile, ctx);
2551
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
1922
2552
  const wrapArray = has("IsArray") || isArrayType;
1923
2553
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
1924
2554
  return applyPresence(node2, decorators);
@@ -2043,10 +2673,12 @@ function baseFromType(typeText, isArrayType) {
2043
2673
  return { kind: "unknown" };
2044
2674
  }
2045
2675
  }
2046
- function buildNestedReference(className, fromFile, ctx) {
2047
- if (ctx.visiting.has(className)) {
2048
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2049
- 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);
2050
2682
  ctx.recursiveSchemas.add(reserved);
2051
2683
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2052
2684
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -2065,19 +2697,27 @@ function buildNestedReference(className, fromFile, ctx) {
2065
2697
  }
2066
2698
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2067
2699
  }
2068
- const existing = ctx.emittedClasses.get(className);
2700
+ const existing = ctx.emittedClasses.get(cacheKey);
2069
2701
  if (existing) return { kind: "ref", name: existing };
2070
- const schemaName = aliasFor(className, ctx);
2702
+ const schemaName = aliasFor(schemaBase, ctx);
2071
2703
  const resolved = findType(className, fromFile, ctx.project);
2072
2704
  if (!resolved || resolved.kind !== "class") {
2073
2705
  return { kind: "object", fields: [], passthrough: true };
2074
2706
  }
2075
- ctx.emittedClasses.set(className, schemaName);
2076
- 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);
2077
2716
  ctx.depth += 1;
2078
2717
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
2079
2718
  ctx.depth -= 1;
2080
- ctx.visiting.delete(className);
2719
+ ctx.visiting.delete(cacheKey);
2720
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
2081
2721
  ctx.named.set(schemaName, childNode);
2082
2722
  return { kind: "ref", name: schemaName };
2083
2723
  }
@@ -2124,6 +2764,39 @@ function messageRaw(decorator) {
2124
2764
  }
2125
2765
  return void 0;
2126
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
+ }
2127
2800
  function resolveTypeFactoryName(decorator) {
2128
2801
  const arg = firstArg(decorator);
2129
2802
  if (!arg) return null;
@@ -2137,6 +2810,17 @@ function singularClassName(typeText) {
2137
2810
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2138
2811
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2139
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
+ }
2140
2824
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2141
2825
  const arg = firstArg(decorator);
2142
2826
  if (!arg) return null;
@@ -2195,17 +2879,34 @@ import {
2195
2879
  } from "ts-morph";
2196
2880
 
2197
2881
  // src/discovery/enum-resolution.ts
2882
+ var _enumCache = /* @__PURE__ */ new WeakMap();
2883
+ function clearEnumCache(project) {
2884
+ _enumCache.delete(project);
2885
+ }
2198
2886
  function resolveEnumValues(name, sourceFile, project) {
2887
+ let byKey = _enumCache.get(project);
2888
+ if (byKey === void 0) {
2889
+ byKey = /* @__PURE__ */ new Map();
2890
+ _enumCache.set(project, byKey);
2891
+ }
2892
+ const key = `${sourceFile.getFilePath()}\0${name}`;
2893
+ if (byKey.has(key)) {
2894
+ const cached = byKey.get(key) ?? null;
2895
+ return cached ? { values: [...cached.values], numeric: cached.numeric } : null;
2896
+ }
2199
2897
  const resolved = findType(name, sourceFile, project);
2200
- if (!resolved || resolved.kind !== "enum") return null;
2201
- let numeric = true;
2202
- const values = resolved.members.map((m) => {
2203
- const parsed = JSON.parse(m);
2204
- if (typeof parsed === "string") numeric = false;
2205
- return String(parsed);
2206
- });
2207
- if (values.length === 0) return null;
2208
- return { values, numeric };
2898
+ let result = null;
2899
+ if (resolved && resolved.kind === "enum") {
2900
+ let numeric = true;
2901
+ const values = resolved.members.map((m) => {
2902
+ const parsed = JSON.parse(m);
2903
+ if (typeof parsed === "string") numeric = false;
2904
+ return String(parsed);
2905
+ });
2906
+ if (values.length > 0) result = { values, numeric };
2907
+ }
2908
+ byKey.set(key, result);
2909
+ return result ? { values: [...result.values], numeric: result.numeric } : null;
2209
2910
  }
2210
2911
 
2211
2912
  // src/discovery/filter-field-types.ts
@@ -2650,24 +3351,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2650
3351
  "Map",
2651
3352
  "Set"
2652
3353
  ]);
2653
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3354
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2654
3355
  if (depth <= 0) return "unknown";
2655
3356
  if (Node6.isArrayTypeNode(typeNode)) {
2656
3357
  const elementType = typeNode.getElementTypeNode();
2657
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3358
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
2658
3359
  }
2659
3360
  if (Node6.isUnionTypeNode(typeNode)) {
2660
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3361
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
2661
3362
  }
2662
3363
  if (Node6.isIntersectionTypeNode(typeNode)) {
2663
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3364
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
2664
3365
  }
2665
3366
  if (Node6.isParenthesizedTypeNode(typeNode)) {
2666
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3367
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
2667
3368
  }
2668
3369
  if (Node6.isTypeReference(typeNode)) {
2669
3370
  const typeName = typeNode.getTypeName();
2670
3371
  const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3372
+ const bound = subst.get(name);
3373
+ if (bound !== void 0) return bound;
2671
3374
  if (name === "string" || name === "number" || name === "boolean") return name;
2672
3375
  if (name === "Date") return "string";
2673
3376
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -2675,14 +3378,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2675
3378
  return "unknown";
2676
3379
  const wrapperMode = WRAPPER_TYPES[name];
2677
3380
  if (wrapperMode) {
2678
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3381
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
2679
3382
  }
2680
3383
  if (PASSTHROUGH_UTILITY.has(name)) {
2681
3384
  return typeNode.getText();
2682
3385
  }
2683
3386
  const resolved = findType(name, sourceFile, project);
2684
3387
  if (resolved) {
2685
- return expandTypeDecl(resolved, project, depth - 1);
3388
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
3389
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
2686
3390
  }
2687
3391
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
2688
3392
  return "unknown";
@@ -2695,32 +3399,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2695
3399
  if (kind === SyntaxKind3.AnyKeyword) return "unknown";
2696
3400
  return typeNode.getText();
2697
3401
  }
2698
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
3402
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
2699
3403
  const typeArgs = typeNode.getTypeArguments();
2700
3404
  const firstTypeArg = typeArgs[0];
2701
3405
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2702
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3406
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
2703
3407
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
2704
3408
  }
2705
3409
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2706
3410
  }
2707
- 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()) {
2708
3425
  if (depth < 0) return "unknown";
2709
3426
  switch (result.kind) {
2710
3427
  case "class":
2711
- return resolvePropertied(result.decl, result.file, project, depth);
3428
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2712
3429
  case "interface":
2713
- return resolvePropertied(result.decl, result.file, project, depth);
3430
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2714
3431
  case "typeAlias":
2715
3432
  if (result.typeNode) {
2716
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
3433
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
2717
3434
  }
2718
3435
  return result.text;
2719
3436
  case "enum":
2720
3437
  return result.members.join(" | ");
2721
3438
  }
2722
3439
  }
2723
- function resolvePropertied(decl, sourceFile, project, depth) {
3440
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2724
3441
  if (depth < 0) return "unknown";
2725
3442
  const lines = [];
2726
3443
  for (const prop of decl.getProperties()) {
@@ -2729,7 +3446,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
2729
3446
  const propTypeNode = prop.getTypeNode();
2730
3447
  let propType = "unknown";
2731
3448
  if (propTypeNode) {
2732
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
3449
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2733
3450
  }
2734
3451
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
2735
3452
  }
@@ -2778,7 +3495,7 @@ function extractParamsType(method, sourceFile, project) {
2778
3495
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
2779
3496
  }
2780
3497
  function extractResponseType(method, sourceFile, project) {
2781
- const apiResponseDecorator = method.getDecorator("ApiResponse");
3498
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2782
3499
  if (apiResponseDecorator) {
2783
3500
  const args = apiResponseDecorator.getArguments();
2784
3501
  const optsArg = args[0];
@@ -2807,6 +3524,59 @@ function extractResponseType(method, sourceFile, project) {
2807
3524
  }
2808
3525
  return "unknown";
2809
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
+ }
2810
3580
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
2811
3581
  if (!Node6.isIdentifier(node)) return "unknown";
2812
3582
  const name = node.getText();
@@ -2822,17 +3592,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
2822
3592
  unwrapContainers: true
2823
3593
  });
2824
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
+ }
2825
3627
  function extractDtoContract(method, sourceFile, project) {
2826
3628
  let body = extractBodyType(method, sourceFile, project);
2827
3629
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
2828
3630
  const query = extractQueryType(method, sourceFile, project);
3631
+ const streamElement = detectStreamElement(method);
3632
+ const isStream = streamElement !== null;
2829
3633
  if (filterInfo && filterInfo.source === "body") {
2830
3634
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
2831
3635
  body = body ?? bodyType;
2832
3636
  }
2833
3637
  const paramsType = extractParamsType(method, sourceFile, project);
2834
- const response = extractResponseType(method, sourceFile, project);
2835
- 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) {
2836
3641
  return null;
2837
3642
  }
2838
3643
  let bodyRef = null;
@@ -2846,12 +3651,12 @@ function extractDtoContract(method, sourceFile, project) {
2846
3651
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2847
3652
  }
2848
3653
  }
2849
- const returnTypeNode = method.getReturnTypeNode();
3654
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
2850
3655
  if (returnTypeNode) {
2851
3656
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
2852
3657
  }
2853
- if (!responseRef) {
2854
- const apiResp = method.getDecorator("ApiResponse");
3658
+ if (!responseRef && !isStream) {
3659
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2855
3660
  if (apiResp) {
2856
3661
  const args = apiResp.getArguments();
2857
3662
  const optsArg = args[0];
@@ -2893,16 +3698,19 @@ function extractDtoContract(method, sourceFile, project) {
2893
3698
  query,
2894
3699
  body,
2895
3700
  response,
3701
+ error: errorInfo?.type ?? null,
2896
3702
  params: paramsType,
2897
3703
  queryRef,
2898
3704
  bodyRef,
2899
3705
  responseRef,
3706
+ errorRef: errorInfo?.ref ?? null,
2900
3707
  filterFields: filterInfo?.fieldNames ?? null,
2901
3708
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
2902
3709
  filterSource: filterInfo?.source ?? null,
2903
3710
  formWarnings,
2904
3711
  bodySchema,
2905
- querySchema
3712
+ querySchema,
3713
+ stream: isStream
2906
3714
  };
2907
3715
  }
2908
3716
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -3020,6 +3828,7 @@ function parseDefineContractCall(callExpr) {
3020
3828
  let query = null;
3021
3829
  let body = null;
3022
3830
  let response = "unknown";
3831
+ let error = null;
3023
3832
  let bodyZodText = null;
3024
3833
  let queryZodText = null;
3025
3834
  for (const prop of optsArg.getProperties()) {
@@ -3035,25 +3844,38 @@ function parseDefineContractCall(callExpr) {
3035
3844
  bodyZodText = val.getText();
3036
3845
  } else if (propName === "response") {
3037
3846
  response = zodAstToTs(val);
3847
+ } else if (propName === "error") {
3848
+ error = zodAstToTs(val);
3038
3849
  }
3039
3850
  }
3040
- return { query, body, response, bodyZodText, queryZodText };
3851
+ return { query, body, response, error, bodyZodText, queryZodText };
3041
3852
  }
3042
3853
 
3043
3854
  // src/discovery/contracts-fast.ts
3044
3855
  async function discoverContractsFast(opts) {
3045
3856
  const { cwd, glob, tsconfig } = opts;
3046
- const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
3047
- 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) {
3048
3870
  try {
3049
- project = new Project3({
3871
+ return new Project3({
3050
3872
  tsConfigFilePath: tsconfigPath,
3051
3873
  skipAddingFilesFromTsConfig: true,
3052
3874
  skipLoadingLibFiles: true,
3053
3875
  skipFileDependencyResolution: true
3054
3876
  });
3055
3877
  } catch {
3056
- project = new Project3({
3878
+ return new Project3({
3057
3879
  skipAddingFilesFromTsConfig: true,
3058
3880
  skipLoadingLibFiles: true,
3059
3881
  skipFileDependencyResolution: true,
@@ -3064,20 +3886,105 @@ async function discoverContractsFast(opts) {
3064
3886
  }
3065
3887
  });
3066
3888
  }
3067
- const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3068
- for (const f of files) {
3069
- project.addSourceFileAtPath(f);
3070
- }
3071
- const routes = [];
3889
+ }
3890
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
3072
3891
  setDiscoveryContext(project, {
3073
3892
  projectRoot: cwd,
3074
3893
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3075
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 = [];
3076
3906
  for (const sourceFile of project.getSourceFiles()) {
3077
3907
  routes.push(...extractFromSourceFile(sourceFile, project));
3078
3908
  }
3079
3909
  return routes;
3080
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
+ };
3081
3988
  function decoratorStringArg(decoratorExpr) {
3082
3989
  if (!decoratorExpr) return void 0;
3083
3990
  if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -3133,6 +4040,11 @@ function resolveVerb(method) {
3133
4040
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3134
4041
  }
3135
4042
  }
4043
+ const sseDecorator = method.getDecorator("Sse");
4044
+ if (sseDecorator) {
4045
+ const pathArg = sseDecorator.getArguments()[0];
4046
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
4047
+ }
3136
4048
  return null;
3137
4049
  }
3138
4050
  function readAsDecorator(node, label) {
@@ -3175,7 +4087,17 @@ function buildRoute(args) {
3175
4087
  };
3176
4088
  }
3177
4089
  function extractContractRoute(args) {
3178
- 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;
3179
4101
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3180
4102
  if (!firstDecoratorArg) return null;
3181
4103
  let contractDef = null;
@@ -3185,18 +4107,19 @@ function extractContractRoute(args) {
3185
4107
  contractDef = parseDefineContractCall(firstDecoratorArg);
3186
4108
  } else if (Node8.isIdentifier(firstDecoratorArg)) {
3187
4109
  const identName = firstDecoratorArg.getText();
3188
- const varDecl = sourceFile.getVariableDeclaration(identName);
3189
- if (!varDecl) {
4110
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4111
+ if (!resolvedVar) {
3190
4112
  console.warn(
3191
- `[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`
3192
4114
  );
3193
4115
  return null;
3194
4116
  }
4117
+ const { decl: varDecl, file: declFile } = resolvedVar;
3195
4118
  const initializer = varDecl.getInitializer();
3196
4119
  if (!initializer) return null;
3197
4120
  contractDef = parseDefineContractCall(initializer);
3198
4121
  if (contractDef && varDecl.isExported()) {
3199
- const filePath = sourceFile.getFilePath();
4122
+ const filePath = declFile.getFilePath();
3200
4123
  if (contractDef.body !== null) {
3201
4124
  bodyZodRef = { name: `${identName}.body`, filePath };
3202
4125
  }
@@ -3229,6 +4152,7 @@ function extractContractRoute(args) {
3229
4152
  query: contractDef.query,
3230
4153
  body: contractDef.body,
3231
4154
  response: contractDef.response,
4155
+ error: contractDef.error,
3232
4156
  // Path A: capture both the importable ref and the raw text. The emitter
3233
4157
  // prefers inlining the text (client-safe — re-exporting from a controller
3234
4158
  // would drag server-only deps into the client bundle).
@@ -3260,15 +4184,18 @@ function extractDtoRoute(args) {
3260
4184
  query: dtoContract?.query ?? null,
3261
4185
  body: dtoContract?.body ?? null,
3262
4186
  response: dtoContract?.response ?? "unknown",
4187
+ error: dtoContract?.error ?? null,
3263
4188
  queryRef: dtoContract?.queryRef ?? null,
3264
4189
  bodyRef: dtoContract?.bodyRef ?? null,
3265
4190
  responseRef: dtoContract?.responseRef ?? null,
4191
+ errorRef: dtoContract?.errorRef ?? null,
3266
4192
  filterFields: dtoContract?.filterFields ?? null,
3267
4193
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3268
4194
  filterSource: dtoContract?.filterSource ?? null,
3269
4195
  formWarnings: dtoContract?.formWarnings ?? [],
3270
4196
  bodySchema: dtoContract?.bodySchema ?? null,
3271
- querySchema: dtoContract?.querySchema ?? null
4197
+ querySchema: dtoContract?.querySchema ?? null,
4198
+ stream: dtoContract?.stream ?? false
3272
4199
  }
3273
4200
  });
3274
4201
  }
@@ -3292,6 +4219,7 @@ function extractFromSourceFile(sourceFile, project) {
3292
4219
  prefix,
3293
4220
  className,
3294
4221
  sourceFile,
4222
+ project,
3295
4223
  seenNames
3296
4224
  }) : extractDtoRoute({
3297
4225
  cls,
@@ -3311,8 +4239,8 @@ function extractFromSourceFile(sourceFile, project) {
3311
4239
 
3312
4240
  // src/watch/lock-file.ts
3313
4241
  import { open } from "fs/promises";
3314
- import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3315
- 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";
3316
4244
  var LOCK_FILE = ".watcher.lock";
3317
4245
  function isProcessAlive(pid) {
3318
4246
  try {
@@ -3323,8 +4251,8 @@ function isProcessAlive(pid) {
3323
4251
  }
3324
4252
  }
3325
4253
  async function acquireLock(outDir) {
3326
- await mkdir8(outDir, { recursive: true });
3327
- const lockPath = join12(outDir, LOCK_FILE);
4254
+ await mkdir10(outDir, { recursive: true });
4255
+ const lockPath = join14(outDir, LOCK_FILE);
3328
4256
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3329
4257
  try {
3330
4258
  const fd = await open(lockPath, "wx");
@@ -3364,7 +4292,7 @@ async function watch(config, onChange) {
3364
4292
  if (lock === null) {
3365
4293
  let holderPid = "unknown";
3366
4294
  try {
3367
- const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
4295
+ const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
3368
4296
  const data = JSON.parse(raw);
3369
4297
  if (data.pid !== void 0) holderPid = String(data.pid);
3370
4298
  } catch {
@@ -3374,12 +4302,20 @@ async function watch(config, onChange) {
3374
4302
  );
3375
4303
  return NO_OP_WATCHER;
3376
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
+ }
3377
4317
  try {
3378
- const initialRoutes = await discoverContractsFast({
3379
- cwd: config.codegen.cwd,
3380
- glob: config.contracts.glob,
3381
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3382
- });
4318
+ const initialRoutes = (await getDiscovery()).discover();
3383
4319
  await generate(config, initialRoutes);
3384
4320
  } catch (err) {
3385
4321
  console.warn(
@@ -3392,7 +4328,7 @@ async function watch(config, onChange) {
3392
4328
  }
3393
4329
  let pagesDebounceTimer;
3394
4330
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3395
- const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
4331
+ const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
3396
4332
  ignoreInitial: true,
3397
4333
  persistent: true,
3398
4334
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3418,23 +4354,23 @@ async function watch(config, onChange) {
3418
4354
  pagesWatcher.on("change", schedulePagesRegenerate);
3419
4355
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3420
4356
  let contractsDebounceTimer;
3421
- 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), {
3422
4359
  ignoreInitial: true,
3423
4360
  persistent: true,
3424
4361
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3425
4362
  });
3426
- function scheduleContractsRegenerate() {
4363
+ function scheduleContractsRegenerate(changedPath) {
4364
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3427
4365
  if (contractsDebounceTimer !== void 0) {
3428
4366
  clearTimeout(contractsDebounceTimer);
3429
4367
  }
3430
4368
  contractsDebounceTimer = setTimeout(async () => {
3431
4369
  contractsDebounceTimer = void 0;
4370
+ const changed = [...pendingChangedPaths];
4371
+ pendingChangedPaths.clear();
3432
4372
  try {
3433
- const routes = await discoverContractsFast({
3434
- cwd: config.codegen.cwd,
3435
- glob: config.contracts.glob,
3436
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3437
- });
4373
+ const routes = await (await getDiscovery()).rediscover(changed);
3438
4374
  await generate(config, routes);
3439
4375
  } catch (err) {
3440
4376
  console.error(
@@ -3445,17 +4381,17 @@ async function watch(config, onChange) {
3445
4381
  onChange?.();
3446
4382
  }, config.contracts.debounceMs);
3447
4383
  }
3448
- contractsWatcher.on("add", scheduleContractsRegenerate);
3449
- contractsWatcher.on("change", scheduleContractsRegenerate);
3450
- contractsWatcher.on("unlink", scheduleContractsRegenerate);
3451
- 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), {
3452
4388
  ignoreInitial: true,
3453
4389
  persistent: true,
3454
4390
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3455
4391
  });
3456
- formsWatcher.on("add", scheduleContractsRegenerate);
3457
- formsWatcher.on("change", scheduleContractsRegenerate);
3458
- 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));
3459
4395
  return {
3460
4396
  close: async () => {
3461
4397
  if (pagesDebounceTimer !== void 0) {
@@ -3475,7 +4411,7 @@ async function watch(config, onChange) {
3475
4411
  }
3476
4412
 
3477
4413
  // src/index.ts
3478
- var VERSION = "0.4.0";
4414
+ var VERSION = "0.5.0";
3479
4415
 
3480
4416
  // src/cli/codegen.ts
3481
4417
  async function runCodegen(opts = {}) {
@@ -3504,13 +4440,13 @@ async function runCodegen(opts = {}) {
3504
4440
  // src/cli/doctor.ts
3505
4441
  import { execFileSync as execFileSync2 } from "child_process";
3506
4442
  import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
3507
- import { join as join15 } from "path";
4443
+ import { join as join17 } from "path";
3508
4444
 
3509
4445
  // src/cli/init.ts
3510
4446
  import { execFileSync } from "child_process";
3511
4447
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3512
- import { access as access2, mkdir as mkdir9, readFile as readFile4, writeFile as writeFile8 } from "fs/promises";
3513
- 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";
3514
4450
  import { createInterface } from "readline";
3515
4451
 
3516
4452
  // src/cli/patch-utils.ts
@@ -3572,7 +4508,7 @@ ${bold(title)}`);
3572
4508
  }
3573
4509
  async function readPackageJson(cwd) {
3574
4510
  try {
3575
- const raw = await readFile4(join14(cwd, "package.json"), "utf8");
4511
+ const raw = await readFile4(join16(cwd, "package.json"), "utf8");
3576
4512
  return JSON.parse(raw);
3577
4513
  } catch {
3578
4514
  return {};
@@ -3603,7 +4539,7 @@ async function detectTemplateEngine(cwd) {
3603
4539
  async function detectPackageManager(cwd) {
3604
4540
  async function exists(file) {
3605
4541
  try {
3606
- await access2(join14(cwd, file));
4542
+ await access2(join16(cwd, file));
3607
4543
  return true;
3608
4544
  } catch {
3609
4545
  return false;
@@ -3644,13 +4580,13 @@ async function writeIfNotExists(filePath, content, label) {
3644
4580
  }
3645
4581
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
3646
4582
  if (dir) {
3647
- await mkdir9(dir, { recursive: true });
4583
+ await mkdir11(dir, { recursive: true });
3648
4584
  }
3649
- await writeFile8(filePath, content, "utf8");
4585
+ await writeFile10(filePath, content, "utf8");
3650
4586
  logCreated(label);
3651
4587
  }
3652
4588
  async function handleViteConfig(cwd, framework) {
3653
- const filePath = join14(cwd, "vite.config.ts");
4589
+ const filePath = join16(cwd, "vite.config.ts");
3654
4590
  if (await fileExists2(filePath)) {
3655
4591
  const existing = await readFile4(filePath, "utf8");
3656
4592
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
@@ -3668,9 +4604,9 @@ async function handleViteConfig(cwd, framework) {
3668
4604
  }
3669
4605
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
3670
4606
  if (dir) {
3671
- await mkdir9(dir, { recursive: true });
4607
+ await mkdir11(dir, { recursive: true });
3672
4608
  }
3673
- await writeFile8(filePath, viteConfigTemplate(framework), "utf8");
4609
+ await writeFile10(filePath, viteConfigTemplate(framework), "utf8");
3674
4610
  logCreated("vite.config.ts");
3675
4611
  }
3676
4612
  async function patchGitignore(gitignorePath) {
@@ -3686,7 +4622,7 @@ async function patchGitignore(gitignorePath) {
3686
4622
  ` : `${existing}
3687
4623
  ${GITIGNORE_ENTRY}
3688
4624
  `;
3689
- await writeFile8(gitignorePath, newContent, "utf8");
4625
+ await writeFile10(gitignorePath, newContent, "utf8");
3690
4626
  logPatched(".gitignore", "added .nestjs-inertia/");
3691
4627
  }
3692
4628
  function installDeps(pkgManager, deps, dev) {
@@ -3708,7 +4644,7 @@ function installDeps(pkgManager, deps, dev) {
3708
4644
  }
3709
4645
  }
3710
4646
  async function patchPackageJsonScripts(cwd, scripts) {
3711
- const pkgPath = join14(cwd, "package.json");
4647
+ const pkgPath = join16(cwd, "package.json");
3712
4648
  let pkg = {};
3713
4649
  try {
3714
4650
  pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
@@ -3730,7 +4666,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
3730
4666
  return;
3731
4667
  }
3732
4668
  pkg.scripts = existing;
3733
- await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
4669
+ await writeFile10(pkgPath, `${JSON.stringify(pkg, null, 2)}
3734
4670
  `, "utf8");
3735
4671
  }
3736
4672
  function patchAppModule(filePath, rootView) {
@@ -4025,7 +4961,7 @@ export class HomeController {
4025
4961
  }
4026
4962
  `;
4027
4963
  function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4028
- const filePath = join14(cwd, filename);
4964
+ const filePath = join16(cwd, filename);
4029
4965
  return patchJsonFile(
4030
4966
  filePath,
4031
4967
  (json) => {
@@ -4040,7 +4976,7 @@ function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4040
4976
  );
4041
4977
  }
4042
4978
  function patchNestCliJson(cwd, shellDir) {
4043
- const filePath = join14(cwd, "nest-cli.json");
4979
+ const filePath = join16(cwd, "nest-cli.json");
4044
4980
  return patchJsonFile(filePath, (json) => {
4045
4981
  const compiler = json.compilerOptions ?? {};
4046
4982
  const assets = compiler.assets ?? [];
@@ -4063,46 +4999,46 @@ async function scaffoldFiles(ctx) {
4063
4999
  const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
4064
5000
  logSection("Scaffold files");
4065
5001
  await writeIfNotExists(
4066
- join14(cwd, "nestjs-inertia.config.ts"),
5002
+ join16(cwd, "nestjs-inertia.config.ts"),
4067
5003
  configTemplate(framework),
4068
5004
  "nestjs-inertia.config.ts"
4069
5005
  );
4070
- 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");
4071
5007
  await writeIfNotExists(
4072
- join14(cwd, "tsconfig.inertia.json"),
5008
+ join16(cwd, "tsconfig.inertia.json"),
4073
5009
  TSCONFIG_INERTIA_TEMPLATE,
4074
5010
  "tsconfig.inertia.json"
4075
5011
  );
4076
5012
  await writeIfNotExists(
4077
- join14(cwd, "inertia", "tsconfig.json"),
5013
+ join16(cwd, "inertia", "tsconfig.json"),
4078
5014
  INERTIA_TSCONFIG_TEMPLATE,
4079
5015
  "inertia/tsconfig.json"
4080
5016
  );
4081
5017
  await writeIfNotExists(
4082
- join14(cwd, "inertia", shellFileName),
5018
+ join16(cwd, "inertia", shellFileName),
4083
5019
  htmlShellTemplate(framework, engine),
4084
5020
  `inertia/${shellFileName}`
4085
5021
  );
4086
5022
  await handleViteConfig(cwd, framework);
4087
5023
  await writeIfNotExists(
4088
- join14(cwd, "inertia", "app", `client.${entryExt}`),
5024
+ join16(cwd, "inertia", "app", `client.${entryExt}`),
4089
5025
  entryPointTemplate(framework),
4090
5026
  `inertia/app/client.${entryExt}`
4091
5027
  );
4092
5028
  await writeIfNotExists(
4093
- join14(cwd, "inertia", "pages", `Home.${pageExt}`),
5029
+ join16(cwd, "inertia", "pages", `Home.${pageExt}`),
4094
5030
  samplePageTemplate(framework),
4095
5031
  `inertia/pages/Home.${pageExt}`
4096
5032
  );
4097
5033
  await writeIfNotExists(
4098
- join14(cwd, "src", "home.controller.ts"),
5034
+ join16(cwd, "src", "home.controller.ts"),
4099
5035
  SAMPLE_CONTROLLER,
4100
5036
  "src/home.controller.ts"
4101
5037
  );
4102
5038
  }
4103
5039
  function patchServerAppModule(ctx) {
4104
5040
  const { cwd, rootView } = ctx;
4105
- const appModulePath = join14(cwd, "src", "app.module.ts");
5041
+ const appModulePath = join16(cwd, "src", "app.module.ts");
4106
5042
  const appModuleResult = patchAppModule(appModulePath, rootView);
4107
5043
  if (appModuleResult === "patched") {
4108
5044
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -4116,7 +5052,7 @@ function patchServerAppModule(ctx) {
4116
5052
  }
4117
5053
  }
4118
5054
  function patchServerMainTs(ctx) {
4119
- const mainTsPath = join14(ctx.cwd, "src", "main.ts");
5055
+ const mainTsPath = join16(ctx.cwd, "src", "main.ts");
4120
5056
  const mainTsResult = patchMainTs(mainTsPath);
4121
5057
  if (mainTsResult === "patched") {
4122
5058
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -4151,7 +5087,7 @@ function patchBuildConfigs(ctx) {
4151
5087
  }
4152
5088
  async function patchGitignoreAndDist(ctx) {
4153
5089
  const { cwd } = ctx;
4154
- await patchGitignore(join14(cwd, ".gitignore"));
5090
+ await patchGitignore(join16(cwd, ".gitignore"));
4155
5091
  const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
4156
5092
  if (tsconfigDistResult === "patched") {
4157
5093
  logPatched("tsconfig.json", "excluded dist/ from server compilation");
@@ -4257,7 +5193,7 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4257
5193
 
4258
5194
  // src/cli/doctor.ts
4259
5195
  function checkFileExists(cwd, file) {
4260
- return existsSync(join15(cwd, file));
5196
+ return existsSync(join17(cwd, file));
4261
5197
  }
4262
5198
  function readJson(path) {
4263
5199
  try {
@@ -4293,15 +5229,15 @@ function writeJsonField(filePath, dotPath, value) {
4293
5229
  }
4294
5230
  function getPackageVersion(cwd, pkg) {
4295
5231
  try {
4296
- const pkgJson = readJson(join15(cwd, "node_modules", pkg, "package.json"));
5232
+ const pkgJson = readJson(join17(cwd, "node_modules", pkg, "package.json"));
4297
5233
  return pkgJson?.version ?? null;
4298
5234
  } catch {
4299
5235
  return null;
4300
5236
  }
4301
5237
  }
4302
5238
  function detectPkgManager(cwd) {
4303
- if (existsSync(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
4304
- 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";
4305
5241
  return "npm";
4306
5242
  }
4307
5243
  async function runDoctor(opts) {
@@ -4339,7 +5275,7 @@ async function runDoctor(opts) {
4339
5275
  autoFix: () => runInit({ cwd })
4340
5276
  });
4341
5277
  if (foundShellDir) {
4342
- const nestCliPath = join15(cwd, "nest-cli.json");
5278
+ const nestCliPath = join17(cwd, "nest-cli.json");
4343
5279
  const nestCli = readJson(nestCliPath);
4344
5280
  const compiler = nestCli?.compilerOptions ?? {};
4345
5281
  const assets = compiler.assets ?? [];
@@ -4378,7 +5314,7 @@ async function runDoctor(opts) {
4378
5314
  fix: "Run: nestjs-codegen codegen",
4379
5315
  autoFix: () => runCodegen({ cwd })
4380
5316
  });
4381
- const tsconfigPath = join15(cwd, "tsconfig.json");
5317
+ const tsconfigPath = join17(cwd, "tsconfig.json");
4382
5318
  const tsconfig = readJson(tsconfigPath);
4383
5319
  const paths = tsconfig?.compilerOptions?.paths;
4384
5320
  checks.push({
@@ -4389,7 +5325,7 @@ async function runDoctor(opts) {
4389
5325
  });
4390
5326
  const inertiaDir = foundShellDir ?? "inertia";
4391
5327
  for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
4392
- const tsc = readJson(join15(cwd, tsconfigFile));
5328
+ const tsc = readJson(join17(cwd, tsconfigFile));
4393
5329
  if (!tsc) continue;
4394
5330
  const excl = tsc.exclude ?? [];
4395
5331
  const excludesIt = excl.includes(inertiaDir);
@@ -4415,7 +5351,7 @@ async function runDoctor(opts) {
4415
5351
  }
4416
5352
  });
4417
5353
  }
4418
- const inertiaTsconfigPath = join15(cwd, "tsconfig.inertia.json");
5354
+ const inertiaTsconfigPath = join17(cwd, "tsconfig.inertia.json");
4419
5355
  const inertiaTsconfig = readJson(inertiaTsconfigPath);
4420
5356
  checks.push({
4421
5357
  name: "tsconfig.inertia.json exists",
@@ -4467,7 +5403,7 @@ async function runDoctor(opts) {
4467
5403
  fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
4468
5404
  });
4469
5405
  }
4470
- const innerTsconfigPath = join15(cwd, "inertia", "tsconfig.json");
5406
+ const innerTsconfigPath = join17(cwd, "inertia", "tsconfig.json");
4471
5407
  checks.push({
4472
5408
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
4473
5409
  pass: existsSync(innerTsconfigPath),
@@ -4477,7 +5413,7 @@ async function runDoctor(opts) {
4477
5413
  }
4478
5414
  });
4479
5415
  if (checkFileExists(cwd, "vite.config.ts")) {
4480
- const viteContent = readFileSync4(join15(cwd, "vite.config.ts"), "utf8");
5416
+ const viteContent = readFileSync4(join17(cwd, "vite.config.ts"), "utf8");
4481
5417
  checks.push({
4482
5418
  name: "vite.config.ts has resolve.alias",
4483
5419
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -4542,7 +5478,7 @@ async function runDoctor(opts) {
4542
5478
  });
4543
5479
  }
4544
5480
  if (checkFileExists(cwd, ".gitignore")) {
4545
- const gitignorePath = join15(cwd, ".gitignore");
5481
+ const gitignorePath = join17(cwd, ".gitignore");
4546
5482
  const gitignore = readFileSync4(gitignorePath, "utf8");
4547
5483
  checks.push({
4548
5484
  name: ".gitignore includes .nestjs-inertia/",
@@ -4551,7 +5487,7 @@ async function runDoctor(opts) {
4551
5487
  autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
4552
5488
  });
4553
5489
  }
4554
- const pkgJsonPath = join15(cwd, "package.json");
5490
+ const pkgJsonPath = join17(cwd, "package.json");
4555
5491
  const pkgJson = readJson(pkgJsonPath);
4556
5492
  const scripts = pkgJson?.scripts ?? {};
4557
5493
  checks.push({