@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/index.cjs CHANGED
@@ -34,10 +34,14 @@ __export(src_exports, {
34
34
  ConfigError: () => ConfigError,
35
35
  VERSION: () => VERSION,
36
36
  acquireLock: () => acquireLock,
37
+ buildMocksFile: () => buildMocksFile,
38
+ buildOpenApiSpec: () => buildOpenApiSpec,
37
39
  defineConfig: () => defineConfig,
38
40
  discoverContractsFast: () => discoverContractsFast,
39
41
  emitApi: () => emitApi,
40
42
  emitForms: () => emitForms,
43
+ emitMocks: () => emitMocks,
44
+ emitOpenApi: () => emitOpenApi,
41
45
  emitRoutes: () => emitRoutes,
42
46
  extractSchemaFromDto: () => extractSchemaFromDto,
43
47
  generate: () => generate,
@@ -45,6 +49,8 @@ __export(src_exports, {
45
49
  renderTsType: () => renderTsType,
46
50
  resolveAdapter: () => resolveAdapter,
47
51
  resolveConfig: () => resolveConfig,
52
+ schemaModuleToJsonSchema: () => schemaModuleToJsonSchema,
53
+ schemaNodeToJsonSchema: () => schemaNodeToJsonSchema,
48
54
  watch: () => watch
49
55
  });
50
56
  module.exports = __toCommonJS(src_exports);
@@ -182,6 +188,19 @@ function applyDefaults(userConfig, cwd) {
182
188
  enabled: userConfig.forms?.enabled ?? true,
183
189
  watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
184
190
  zodImport: userConfig.forms?.zodImport ?? "zod"
191
+ },
192
+ openapi: {
193
+ enabled: userConfig.openapi?.enabled ?? false,
194
+ fileName: userConfig.openapi?.fileName ?? "openapi.json",
195
+ title: userConfig.openapi?.title ?? "NestJS API",
196
+ version: userConfig.openapi?.version ?? "1.0.0",
197
+ description: userConfig.openapi?.description ?? null
198
+ },
199
+ mocks: {
200
+ enabled: userConfig.mocks?.enabled ?? false,
201
+ fileName: userConfig.mocks?.fileName ?? "mocks.ts",
202
+ seed: userConfig.mocks?.seed ?? 1,
203
+ baseUrl: userConfig.mocks?.baseUrl ?? ""
185
204
  }
186
205
  };
187
206
  }
@@ -219,8 +238,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
219
238
  }
220
239
 
221
240
  // src/generate.ts
222
- var import_promises9 = require("fs/promises");
223
- var import_node_path10 = require("path");
241
+ var import_promises11 = require("fs/promises");
242
+ var import_node_path12 = require("path");
224
243
 
225
244
  // src/discovery/pages.ts
226
245
  var import_promises2 = require("fs/promises");
@@ -749,17 +768,28 @@ function emitFilterQueryType(c) {
749
768
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
750
769
  }
751
770
  function buildResponseType(c, outDir) {
771
+ const respRef = c.contractSource.responseRef;
772
+ if (c.contractSource.stream) {
773
+ if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
774
+ return c.contractSource.response;
775
+ }
752
776
  if (c.controllerRef) {
753
777
  let relPath = (0, import_node_path4.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
754
778
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
755
779
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
756
780
  }
757
- const respRef = c.contractSource.responseRef;
758
781
  if (respRef) {
759
782
  return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
760
783
  }
761
784
  return c.contractSource.response;
762
785
  }
786
+ function buildErrorType(c) {
787
+ const errRef = c.contractSource.errorRef;
788
+ if (errRef) {
789
+ return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
790
+ }
791
+ return c.contractSource.error ?? "unknown";
792
+ }
763
793
  function emitRouterTypeBlock(tree, indent, outDir) {
764
794
  const pad = " ".repeat(indent);
765
795
  const lines = [];
@@ -774,12 +804,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
774
804
  const bodyRef = c.contractSource.bodyRef;
775
805
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
776
806
  const response = buildResponseType(c, outDir);
807
+ const error = buildErrorType(c);
777
808
  const params = buildParamsType(c.params);
778
809
  const safeMethod = JSON.stringify(method);
779
810
  const safeUrl = JSON.stringify(c.path);
780
811
  const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
812
+ const stream = c.contractSource.stream ? "true" : "false";
781
813
  lines.push(
782
- `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
814
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
783
815
  );
784
816
  } else {
785
817
  lines.push(`${pad}${objKey}: {`);
@@ -851,15 +883,21 @@ function emitReqHelper() {
851
883
  ""
852
884
  ];
853
885
  }
854
- function renderLeaf(pad, objKey, req, requestExpr, members) {
886
+ function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
855
887
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
856
888
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
889
+ if (streamExpr) {
890
+ lines.push(`${pad} stream: () => ${streamExpr},`);
891
+ }
857
892
  for (const [name, value] of Object.entries(members)) {
858
893
  lines.push(`${pad} ${name}: ${value},`);
859
894
  }
860
895
  lines.push(`${pad}}),`);
861
896
  return lines;
862
897
  }
898
+ function renderStreamExpr(req) {
899
+ return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
900
+ }
863
901
  function emitApiObjectBlock(tree, indent, p) {
864
902
  const pad = " ".repeat(indent);
865
903
  const lines = [];
@@ -894,7 +932,8 @@ function emitApiObjectBlock(tree, indent, p) {
894
932
  }
895
933
  const members = {};
896
934
  for (const [name, { value }] of owned) members[name] = value;
897
- lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
935
+ const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
936
+ lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
898
937
  }
899
938
  return lines;
900
939
  }
@@ -932,6 +971,8 @@ var ROUTE_NAMESPACE = [
932
971
  ' export type Params<K extends string> = ResolveByName<K, "params">;',
933
972
  ' export type Error<K extends string> = ResolveByName<K, "error">;',
934
973
  ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
974
+ " /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
975
+ ' export type Stream<K extends string> = ResolveByName<K, "response">;',
935
976
  " export type Request<K extends string> = {",
936
977
  " body: Body<K>;",
937
978
  " query: Query<K>;",
@@ -948,6 +989,7 @@ var PATH_NAMESPACE = [
948
989
  ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
949
990
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
950
991
  ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
992
+ ' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
951
993
  "}",
952
994
  ""
953
995
  ];
@@ -959,6 +1001,7 @@ var EMPTY_ROUTE_NAMESPACE = [
959
1001
  " export type Params<K extends string> = never;",
960
1002
  " export type Error<K extends string> = never;",
961
1003
  " export type FilterFields<K extends string> = never;",
1004
+ " export type Stream<K extends string> = never;",
962
1005
  " export type Request<K extends string> = { body: never; query: never; params: never };",
963
1006
  "}",
964
1007
  ""
@@ -971,6 +1014,7 @@ var EMPTY_PATH_NAMESPACE = [
971
1014
  " export type Params<M extends string, U extends string> = never;",
972
1015
  " export type Error<M extends string, U extends string> = never;",
973
1016
  " export type FilterFields<M extends string, U extends string> = never;",
1017
+ " export type Stream<M extends string, U extends string> = never;",
974
1018
  "}",
975
1019
  ""
976
1020
  ];
@@ -994,7 +1038,7 @@ function buildApiFile(routes, outDir, opts = {}) {
994
1038
  for (const r of contracted) {
995
1039
  const cs = r.contract?.contractSource;
996
1040
  if (!cs) continue;
997
- const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
1041
+ const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
998
1042
  for (const ref of refs) {
999
1043
  if (!ref) continue;
1000
1044
  let names = importsByFile.get(ref.filePath);
@@ -1427,11 +1471,470 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1427
1471
  await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
1428
1472
  }
1429
1473
 
1430
- // src/emit/emit-pages.ts
1474
+ // src/emit/emit-mocks.ts
1431
1475
  var import_promises7 = require("fs/promises");
1432
1476
  var import_node_path8 = require("path");
1433
- async function emitPages(pages, outDir, _options = {}) {
1477
+
1478
+ // src/ir/schema-node-to-json-schema.ts
1479
+ var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
1480
+ function parseLiteral(raw) {
1481
+ const t = raw.trim();
1482
+ if (t === "true") return true;
1483
+ if (t === "false") return false;
1484
+ if (t === "null") return null;
1485
+ const q = t[0];
1486
+ if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
1487
+ return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
1488
+ }
1489
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
1490
+ return Number(t);
1491
+ }
1492
+ return t;
1493
+ }
1494
+ function literalsType(values) {
1495
+ const types = new Set(values.map((v) => v === null ? "null" : typeof v));
1496
+ if (types.size === 1) {
1497
+ const only = [...types][0];
1498
+ if (only === "string") return "string";
1499
+ if (only === "number") return "number";
1500
+ if (only === "boolean") return "boolean";
1501
+ }
1502
+ return void 0;
1503
+ }
1504
+ function convert(node, ctx) {
1505
+ switch (node.kind) {
1506
+ case "string": {
1507
+ const out = { type: "string" };
1508
+ for (const c of node.checks) {
1509
+ if (c.check === "email") out.format = "email";
1510
+ else if (c.check === "url") out.format = "uri";
1511
+ else if (c.check === "uuid") out.format = "uuid";
1512
+ else if (c.check === "min") out.minLength = Number(c.value);
1513
+ else if (c.check === "max") out.maxLength = Number(c.value);
1514
+ else if (c.check === "regex") {
1515
+ const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
1516
+ out.pattern = m ? m[1] : c.pattern;
1517
+ }
1518
+ }
1519
+ return out;
1520
+ }
1521
+ case "number": {
1522
+ const out = { type: "number" };
1523
+ for (const c of node.checks) {
1524
+ if (c.check === "int") out.type = "integer";
1525
+ else if (c.check === "min") out.minimum = Number(c.value);
1526
+ else if (c.check === "max") out.maximum = Number(c.value);
1527
+ else if (c.check === "positive") out.exclusiveMinimum = 0;
1528
+ else if (c.check === "negative") out.exclusiveMaximum = 0;
1529
+ }
1530
+ return out;
1531
+ }
1532
+ case "boolean":
1533
+ return { type: "boolean" };
1534
+ case "date":
1535
+ return { type: "string", format: "date-time" };
1536
+ case "unknown":
1537
+ return node.note ? { description: node.note } : {};
1538
+ case "instanceof":
1539
+ return { type: "object", description: `instanceof ${node.ctor}` };
1540
+ case "enum": {
1541
+ const values = node.literals.map(parseLiteral);
1542
+ const t = literalsType(values);
1543
+ const out = { enum: values };
1544
+ if (t) out.type = t;
1545
+ return out;
1546
+ }
1547
+ case "literal": {
1548
+ const value = parseLiteral(node.raw);
1549
+ const out = { const: value };
1550
+ const t = literalsType([value]);
1551
+ if (t) out.type = t;
1552
+ return out;
1553
+ }
1554
+ case "union": {
1555
+ const options = node.options.map((o) => convert(o, ctx));
1556
+ const out = { oneOf: options };
1557
+ if (node.discriminator) {
1558
+ out.discriminator = { propertyName: node.discriminator };
1559
+ }
1560
+ return out;
1561
+ }
1562
+ case "object": {
1563
+ const properties = {};
1564
+ const required = [];
1565
+ for (const f of node.fields) {
1566
+ if (f.value.kind === "optional") {
1567
+ properties[f.key] = convert(f.value.inner, ctx);
1568
+ } else {
1569
+ properties[f.key] = convert(f.value, ctx);
1570
+ required.push(f.key);
1571
+ }
1572
+ }
1573
+ const out = { type: "object", properties };
1574
+ if (required.length > 0) out.required = required;
1575
+ out.additionalProperties = node.passthrough;
1576
+ return out;
1577
+ }
1578
+ case "array":
1579
+ return { type: "array", items: convert(node.element, ctx) };
1580
+ case "optional":
1581
+ return widenNullable(convert(node.inner, ctx));
1582
+ case "ref":
1583
+ case "lazyRef":
1584
+ return { $ref: `${ctx.refPrefix}${node.name}` };
1585
+ case "annotated":
1586
+ return convert(node.inner, ctx);
1587
+ }
1588
+ }
1589
+ function widenNullable(schema) {
1590
+ if (schema.$ref) {
1591
+ return { anyOf: [schema, { type: "null" }] };
1592
+ }
1593
+ if (typeof schema.type === "string") {
1594
+ return { ...schema, type: [schema.type, "null"] };
1595
+ }
1596
+ if (Array.isArray(schema.type)) {
1597
+ return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
1598
+ }
1599
+ return { anyOf: [schema, { type: "null" }] };
1600
+ }
1601
+ function schemaNodeToJsonSchema(node, ctx = DEFAULT_CTX) {
1602
+ return convert(node, ctx);
1603
+ }
1604
+ function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
1605
+ const named = {};
1606
+ for (const [name, node] of mod.named) {
1607
+ named[name] = convert(node, ctx);
1608
+ }
1609
+ return { root: convert(mod.root, ctx), named };
1610
+ }
1611
+
1612
+ // src/emit/mock-gen-runtime.ts
1613
+ var MOCK_GEN_RUNTIME = `
1614
+ /** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
1615
+ function makeRng(seed) {
1616
+ let a = seed >>> 0;
1617
+ return {
1618
+ next() {
1619
+ a |= 0;
1620
+ a = (a + 0x6d2b79f5) | 0;
1621
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
1622
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
1623
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
1624
+ },
1625
+ };
1626
+ }
1627
+
1628
+ function __pick(rng, items) {
1629
+ return items[Math.floor(rng.next() * items.length)];
1630
+ }
1631
+
1632
+ function __intBetween(rng, min, max) {
1633
+ return Math.floor(rng.next() * (max - min + 1)) + min;
1634
+ }
1635
+
1636
+ const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
1637
+ const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
1638
+ const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
1639
+
1640
+ function __fakeWords(rng, count) {
1641
+ let out = [];
1642
+ for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
1643
+ return out.join(' ');
1644
+ }
1645
+
1646
+ function __hex(rng, len) {
1647
+ let s = '';
1648
+ for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
1649
+ return s;
1650
+ }
1651
+
1652
+ function __fakeUuid(rng) {
1653
+ return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
1654
+ }
1655
+
1656
+ function __fakeString(rng, schema) {
1657
+ switch (schema.format) {
1658
+ case 'email':
1659
+ return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
1660
+ case 'uri':
1661
+ case 'url':
1662
+ return 'https://example.com/' + __pick(rng, __WORDS);
1663
+ case 'uuid':
1664
+ return __fakeUuid(rng);
1665
+ case 'date-time':
1666
+ return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
1667
+ default:
1668
+ return __fakeWords(rng, __intBetween(rng, 1, 3));
1669
+ }
1670
+ }
1671
+
1672
+ /** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
1673
+ function generateMock(schema, rng, defs, depth) {
1674
+ defs = defs || {};
1675
+ depth = depth || 0;
1676
+ if (schema.$ref) {
1677
+ const name = schema.$ref.replace('#/components/schemas/', '');
1678
+ const target = defs[name];
1679
+ if (!target || depth > 4) return null;
1680
+ return generateMock(target, rng, defs, depth + 1);
1681
+ }
1682
+ if ('const' in schema) return schema.const;
1683
+ if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
1684
+ if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
1685
+ if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
1686
+ let type = Array.isArray(schema.type)
1687
+ ? (schema.type.filter((t) => t !== 'null')[0] || 'null')
1688
+ : schema.type;
1689
+ switch (type) {
1690
+ case 'string':
1691
+ return __fakeString(rng, schema);
1692
+ case 'integer':
1693
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
1694
+ case 'number':
1695
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
1696
+ case 'boolean':
1697
+ return rng.next() < 0.5;
1698
+ case 'null':
1699
+ return null;
1700
+ case 'array': {
1701
+ const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
1702
+ const items = schema.items || {};
1703
+ let arr = [];
1704
+ for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
1705
+ return arr;
1706
+ }
1707
+ case 'object': {
1708
+ const out = {};
1709
+ const props = schema.properties || {};
1710
+ for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
1711
+ return out;
1712
+ }
1713
+ default:
1714
+ return {};
1715
+ }
1716
+ }
1717
+ `.trim();
1718
+
1719
+ // src/emit/emit-mocks.ts
1720
+ var REF_PREFIX = "#/components/schemas/";
1721
+ function toMswPath(path, baseUrl) {
1722
+ return `${baseUrl}${path}`;
1723
+ }
1724
+ function responseSchemaFor(route, defs) {
1725
+ const cs = route.contract.contractSource;
1726
+ if (cs.responseSchema) {
1727
+ const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
1728
+ for (const [name, node] of Object.entries(named)) {
1729
+ if (!(name in defs)) defs[name] = node;
1730
+ }
1731
+ return root;
1732
+ }
1733
+ return {};
1734
+ }
1735
+ function buildMocksFile(routes, opts = {}) {
1736
+ const seed = opts.seed ?? 1;
1737
+ const baseUrl = opts.baseUrl ?? "";
1738
+ const contracted = routes.filter((r) => r.contract);
1739
+ const defs = {};
1740
+ const handlers = [];
1741
+ for (const r of contracted) {
1742
+ const schema = responseSchemaFor(r, defs);
1743
+ const method = r.method.toLowerCase();
1744
+ const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
1745
+ const path = toMswPath(r.path, baseUrl);
1746
+ const cs = r.contract.contractSource;
1747
+ const schemaLiteral = JSON.stringify(schema);
1748
+ const pathLit = JSON.stringify(path);
1749
+ if (cs.stream) {
1750
+ handlers.push(
1751
+ [
1752
+ ` // ${r.name} (stream)`,
1753
+ ` http.${mswMethod}(${pathLit}, () => {`,
1754
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1755
+ " const body = `data: ${JSON.stringify(value)}\\n\\n`;",
1756
+ " return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
1757
+ " }),"
1758
+ ].join("\n")
1759
+ );
1760
+ } else {
1761
+ handlers.push(
1762
+ [
1763
+ ` // ${r.name}`,
1764
+ ` http.${mswMethod}(${pathLit}, () => {`,
1765
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1766
+ " return HttpResponse.json(value);",
1767
+ " }),"
1768
+ ].join("\n")
1769
+ );
1770
+ }
1771
+ }
1772
+ const lines = [
1773
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1774
+ "// MSW handlers returning deterministic, schema-shaped mock data.",
1775
+ "/* eslint-disable */",
1776
+ "// @ts-nocheck",
1777
+ "",
1778
+ "import { http, HttpResponse } from 'msw';",
1779
+ "",
1780
+ `const SEED = ${seed};`,
1781
+ "",
1782
+ "// ---------------------------------------------------------------------------",
1783
+ "// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
1784
+ "// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
1785
+ "// ---------------------------------------------------------------------------",
1786
+ MOCK_GEN_RUNTIME,
1787
+ "",
1788
+ "// Shared component schemas referenced by $ref.",
1789
+ `const DEFS = ${JSON.stringify(defs, null, 2)};`,
1790
+ "",
1791
+ "/** MSW request handlers, one per contracted route. */",
1792
+ "export const handlers = [",
1793
+ ...handlers,
1794
+ "];",
1795
+ ""
1796
+ ];
1797
+ return lines.join("\n");
1798
+ }
1799
+ async function emitMocks(routes, outDir, opts = {}) {
1434
1800
  await (0, import_promises7.mkdir)(outDir, { recursive: true });
1801
+ const content = buildMocksFile(routes, opts);
1802
+ const fileName = opts.fileName ?? "mocks.ts";
1803
+ await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, fileName), content, "utf8");
1804
+ }
1805
+
1806
+ // src/emit/emit-openapi.ts
1807
+ var import_promises8 = require("fs/promises");
1808
+ var import_node_path9 = require("path");
1809
+ var REF_PREFIX2 = "#/components/schemas/";
1810
+ function toOpenApiPath(path) {
1811
+ return path.replace(/:([^/]+)/g, "{$1}");
1812
+ }
1813
+ function positionSchema(schema, tsType, components) {
1814
+ if (schema) {
1815
+ const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
1816
+ for (const [name, node] of Object.entries(named)) {
1817
+ if (!(name in components)) components[name] = node;
1818
+ }
1819
+ return root;
1820
+ }
1821
+ return tsType ? { description: tsType } : {};
1822
+ }
1823
+ function buildParameters(route) {
1824
+ const params = [];
1825
+ for (const p of route.params) {
1826
+ if (p.source === "path") {
1827
+ params.push({
1828
+ name: p.name,
1829
+ in: "path",
1830
+ required: true,
1831
+ schema: { type: "string" }
1832
+ });
1833
+ } else if (p.source === "query") {
1834
+ params.push({
1835
+ name: p.name,
1836
+ in: "query",
1837
+ required: false,
1838
+ schema: { type: "string" }
1839
+ });
1840
+ } else if (p.source === "header") {
1841
+ params.push({
1842
+ name: p.name,
1843
+ in: "header",
1844
+ required: false,
1845
+ schema: { type: "string" }
1846
+ });
1847
+ }
1848
+ }
1849
+ return params;
1850
+ }
1851
+ function buildResponses(cs, components) {
1852
+ const responses = {};
1853
+ const successSchema = positionSchema(
1854
+ // Prefer rich response IR when present; otherwise fall back to the TS type.
1855
+ cs.responseSchema ?? null,
1856
+ cs.response,
1857
+ components
1858
+ );
1859
+ const successContentType = cs.stream ? "text/event-stream" : "application/json";
1860
+ responses["200"] = {
1861
+ description: cs.stream ? "Server-sent event stream" : "Successful response",
1862
+ content: { [successContentType]: { schema: successSchema } }
1863
+ };
1864
+ const errorSchema = positionSchema(null, cs.error ?? null, components);
1865
+ const errorBody = {
1866
+ description: "Error response",
1867
+ content: { "application/json": { schema: errorSchema } }
1868
+ };
1869
+ if (cs.error || cs.errorRef) {
1870
+ responses["400"] = errorBody;
1871
+ responses.default = errorBody;
1872
+ } else {
1873
+ responses.default = {
1874
+ description: "Error response",
1875
+ content: { "application/json": { schema: {} } }
1876
+ };
1877
+ }
1878
+ return responses;
1879
+ }
1880
+ function buildOperation(route, components) {
1881
+ const cs = route.contract.contractSource;
1882
+ const op = {
1883
+ operationId: route.name,
1884
+ parameters: buildParameters(route),
1885
+ responses: buildResponses(cs, components)
1886
+ };
1887
+ const method = route.method.toUpperCase();
1888
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
1889
+ if (hasBody && (cs.bodySchema || cs.body)) {
1890
+ const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
1891
+ op.requestBody = {
1892
+ required: true,
1893
+ content: { "application/json": { schema: bodySchema } }
1894
+ };
1895
+ }
1896
+ return op;
1897
+ }
1898
+ function buildOpenApiSpec(routes, opts = {}) {
1899
+ const components = {};
1900
+ const paths = {};
1901
+ for (const route of routes) {
1902
+ if (!route.contract) continue;
1903
+ const oaPath = toOpenApiPath(route.path);
1904
+ const method = route.method.toLowerCase();
1905
+ let pathItem = paths[oaPath];
1906
+ if (!pathItem) {
1907
+ pathItem = {};
1908
+ paths[oaPath] = pathItem;
1909
+ }
1910
+ pathItem[method] = buildOperation(route, components);
1911
+ }
1912
+ const info = opts.info ?? {};
1913
+ const doc = {
1914
+ openapi: "3.1.0",
1915
+ info: {
1916
+ title: info.title ?? "NestJS API",
1917
+ version: info.version ?? "1.0.0",
1918
+ ...info.description ? { description: info.description } : {}
1919
+ },
1920
+ paths,
1921
+ components: { schemas: components }
1922
+ };
1923
+ return doc;
1924
+ }
1925
+ async function emitOpenApi(routes, outDir, opts = {}) {
1926
+ await (0, import_promises8.mkdir)(outDir, { recursive: true });
1927
+ const doc = buildOpenApiSpec(routes, opts);
1928
+ const fileName = opts.fileName ?? "openapi.json";
1929
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, fileName), `${JSON.stringify(doc, null, 2)}
1930
+ `, "utf8");
1931
+ }
1932
+
1933
+ // src/emit/emit-pages.ts
1934
+ var import_promises9 = require("fs/promises");
1935
+ var import_node_path10 = require("path");
1936
+ async function emitPages(pages, outDir, _options = {}) {
1937
+ await (0, import_promises9.mkdir)(outDir, { recursive: true });
1435
1938
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1436
1939
  const augBody = pages.map((p) => {
1437
1940
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
@@ -1450,7 +1953,7 @@ ${augBody}
1450
1953
  }
1451
1954
  ${sharedPropsBlock}}
1452
1955
  `;
1453
- await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, "pages.d.ts"), content, "utf8");
1956
+ await (0, import_promises9.writeFile)((0, import_node_path10.join)(outDir, "pages.d.ts"), content, "utf8");
1454
1957
  }
1455
1958
  function buildSharedPropsBlock(sharedProps) {
1456
1959
  if (!sharedProps) return "";
@@ -1469,7 +1972,7 @@ ${propsBody}
1469
1972
  `;
1470
1973
  }
1471
1974
  function buildAugmentationType(page, outDir) {
1472
- let importPath = (0, import_node_path8.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1975
+ let importPath = (0, import_node_path10.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1473
1976
  if (!importPath.startsWith(".")) {
1474
1977
  importPath = `./${importPath}`;
1475
1978
  }
@@ -1480,12 +1983,12 @@ function needsQuotes(name) {
1480
1983
  }
1481
1984
 
1482
1985
  // src/emit/emit-routes.ts
1483
- var import_promises8 = require("fs/promises");
1484
- var import_node_path9 = require("path");
1986
+ var import_promises10 = require("fs/promises");
1987
+ var import_node_path11 = require("path");
1485
1988
  async function emitRoutes(routes, outDir) {
1486
- await (0, import_promises8.mkdir)(outDir, { recursive: true });
1989
+ await (0, import_promises10.mkdir)(outDir, { recursive: true });
1487
1990
  const content = buildRoutesFile(routes);
1488
- await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, "routes.ts"), content, "utf8");
1991
+ await (0, import_promises10.writeFile)((0, import_node_path11.join)(outDir, "routes.ts"), content, "utf8");
1489
1992
  }
1490
1993
  function buildRoutesFile(routes) {
1491
1994
  if (routes.length === 0) {
@@ -1633,24 +2136,41 @@ async function generate(config, inputRoutes = []) {
1633
2136
  });
1634
2137
  }
1635
2138
  const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
2139
+ if (hasContracts && config.openapi.enabled) {
2140
+ await emitOpenApi(routes, config.codegen.outDir, {
2141
+ fileName: config.openapi.fileName,
2142
+ info: {
2143
+ title: config.openapi.title,
2144
+ version: config.openapi.version,
2145
+ ...config.openapi.description ? { description: config.openapi.description } : {}
2146
+ }
2147
+ });
2148
+ }
2149
+ if (hasContracts && config.mocks.enabled) {
2150
+ await emitMocks(routes, config.codegen.outDir, {
2151
+ fileName: config.mocks.fileName,
2152
+ seed: config.mocks.seed,
2153
+ baseUrl: config.mocks.baseUrl
2154
+ });
2155
+ }
1636
2156
  await emitIndex(config.codegen.outDir, hasContracts, hasForms);
1637
2157
  if (extensions.length > 0) {
1638
2158
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1639
2159
  for (const file of extraFiles) {
1640
- const dest = (0, import_node_path10.join)(config.codegen.outDir, file.path);
1641
- await (0, import_promises9.mkdir)((0, import_node_path10.dirname)(dest), { recursive: true });
1642
- await (0, import_promises9.writeFile)(dest, file.contents, "utf8");
2160
+ const dest = (0, import_node_path12.join)(config.codegen.outDir, file.path);
2161
+ await (0, import_promises11.mkdir)((0, import_node_path12.dirname)(dest), { recursive: true });
2162
+ await (0, import_promises11.writeFile)(dest, file.contents, "utf8");
1643
2163
  }
1644
2164
  }
1645
2165
  }
1646
2166
 
1647
2167
  // src/watch/watcher.ts
1648
- var import_promises12 = require("fs/promises");
1649
- var import_node_path14 = require("path");
2168
+ var import_promises14 = require("fs/promises");
2169
+ var import_node_path16 = require("path");
1650
2170
  var import_chokidar = __toESM(require("chokidar"), 1);
1651
2171
 
1652
2172
  // src/discovery/contracts-fast.ts
1653
- var import_node_path12 = require("path");
2173
+ var import_node_path14 = require("path");
1654
2174
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
1655
2175
  var import_ts_morph9 = require("ts-morph");
1656
2176
 
@@ -1662,7 +2182,7 @@ var import_ts_morph4 = require("ts-morph");
1662
2182
 
1663
2183
  // src/discovery/type-ref-resolution.ts
1664
2184
  var import_node_fs = require("fs");
1665
- var import_node_path11 = require("path");
2185
+ var import_node_path13 = require("path");
1666
2186
  var import_ts_morph3 = require("ts-morph");
1667
2187
  var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1668
2188
  var _ctxByProject = /* @__PURE__ */ new WeakMap();
@@ -1714,12 +2234,12 @@ function findTypeInFile(name, file) {
1714
2234
  }
1715
2235
  function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1716
2236
  if (moduleSpecifier.startsWith(".")) {
1717
- const dir = (0, import_node_path11.dirname)(sourceFile.getFilePath());
2237
+ const dir = (0, import_node_path13.dirname)(sourceFile.getFilePath());
1718
2238
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
1719
2239
  return [
1720
- (0, import_node_path11.resolve)(dir, `${noExt}.ts`),
1721
- (0, import_node_path11.resolve)(dir, `${moduleSpecifier}.ts`),
1722
- (0, import_node_path11.resolve)(dir, moduleSpecifier, "index.ts")
2240
+ (0, import_node_path13.resolve)(dir, `${noExt}.ts`),
2241
+ (0, import_node_path13.resolve)(dir, `${moduleSpecifier}.ts`),
2242
+ (0, import_node_path13.resolve)(dir, moduleSpecifier, "index.ts")
1723
2243
  ];
1724
2244
  }
1725
2245
  const ctx = _ctxFor(project);
@@ -1740,8 +2260,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1740
2260
  const rest = moduleSpecifier.slice(prefix.length);
1741
2261
  const candidates = [];
1742
2262
  for (const mapping of mappings) {
1743
- const resolved = (0, import_node_path11.resolve)(baseUrl, mapping.replace("*", rest));
1744
- candidates.push(`${resolved}.ts`, (0, import_node_path11.resolve)(resolved, "index.ts"));
2263
+ const resolved = (0, import_node_path13.resolve)(baseUrl, mapping.replace("*", rest));
2264
+ candidates.push(`${resolved}.ts`, (0, import_node_path13.resolve)(resolved, "index.ts"));
1745
2265
  }
1746
2266
  dbg(" resolved candidates:", candidates);
1747
2267
  return candidates;
@@ -1822,7 +2342,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
1822
2342
  }
1823
2343
  return null;
1824
2344
  }
2345
+ function resolveImportedVariable(name, sourceFile, project) {
2346
+ const local = sourceFile.getVariableDeclaration(name);
2347
+ if (local) return { decl: local, file: sourceFile };
2348
+ return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
2349
+ }
2350
+ function resolveVariableViaImports(name, sourceFile, project, seen) {
2351
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2352
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
2353
+ if (!namedImport) continue;
2354
+ const sourceName = namedImport.getName();
2355
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
2356
+ const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
2357
+ if (found) return found;
2358
+ }
2359
+ return null;
2360
+ }
2361
+ function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
2362
+ const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
2363
+ for (const candidate of candidates) {
2364
+ let importedFile = project.getSourceFile(candidate);
2365
+ if (!importedFile) {
2366
+ try {
2367
+ importedFile = project.addSourceFileAtPath(candidate);
2368
+ } catch {
2369
+ continue;
2370
+ }
2371
+ }
2372
+ const found = resolveVariableInFile(name, importedFile, project, seen);
2373
+ if (found) return found;
2374
+ }
2375
+ return null;
2376
+ }
2377
+ function resolveVariableInFile(name, file, project, seen) {
2378
+ const filePath = file.getFilePath();
2379
+ if (seen.has(filePath)) return null;
2380
+ seen.add(filePath);
2381
+ const local = file.getVariableDeclaration(name);
2382
+ if (local) return { decl: local, file };
2383
+ for (const exportDecl of file.getExportDeclarations()) {
2384
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue();
2385
+ const namedExports = exportDecl.getNamedExports();
2386
+ if (moduleSpecifier) {
2387
+ const hasStar = namedExports.length === 0;
2388
+ const reExport2 = namedExports.find(
2389
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2390
+ );
2391
+ if (!hasStar && !reExport2) continue;
2392
+ const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
2393
+ const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
2394
+ if (found) return found;
2395
+ continue;
2396
+ }
2397
+ const reExport = namedExports.find(
2398
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2399
+ );
2400
+ if (!reExport) continue;
2401
+ const sourceName = reExport.getName();
2402
+ const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
2403
+ if (viaImports) return viaImports;
2404
+ }
2405
+ return null;
2406
+ }
1825
2407
  var _findTypeCache = /* @__PURE__ */ new WeakMap();
2408
+ function clearTypeResolutionCaches(project) {
2409
+ _findTypeCache.delete(project);
2410
+ _resolveNamedRefCache.delete(project);
2411
+ }
1826
2412
  function findType(name, sourceFile, project) {
1827
2413
  let byKey = _findTypeCache.get(project);
1828
2414
  if (byKey === void 0) {
@@ -1965,7 +2551,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1965
2551
  emittedClasses: /* @__PURE__ */ new Map(),
1966
2552
  visiting: /* @__PURE__ */ new Set(),
1967
2553
  recursiveSchemas: /* @__PURE__ */ new Set(),
1968
- depth: 0
2554
+ depth: 0,
2555
+ typeBindings: /* @__PURE__ */ new Map()
1969
2556
  };
1970
2557
  const root = buildObject(classDecl, sourceFile, ctx);
1971
2558
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -1989,11 +2576,34 @@ function buildProperty(prop, classFile, ctx) {
1989
2576
  const typeNode = prop.getTypeNode();
1990
2577
  const typeText = typeNode?.getText() ?? "unknown";
1991
2578
  const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
2579
+ const discriminator = resolveDiscriminator(dec("Type"));
2580
+ if (discriminator) {
2581
+ const options = discriminator.subTypes.map(
2582
+ (name) => buildNestedReference(name, classFile, ctx)
2583
+ );
2584
+ const unionNode = {
2585
+ kind: "union",
2586
+ options,
2587
+ discriminator: discriminator.property
2588
+ };
2589
+ const wrapArray = has("IsArray") || isArrayType;
2590
+ const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
2591
+ return applyPresence(node2, decorators);
2592
+ }
2593
+ const propTypeParam = singularClassName(typeText);
2594
+ if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
2595
+ const bound = ctx.typeBindings.get(propTypeParam);
2596
+ const childNode = buildNestedReference(bound, classFile, ctx);
2597
+ const wrapArray = has("IsArray") || isArrayType;
2598
+ const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
2599
+ return applyPresence(node2, decorators);
2600
+ }
1992
2601
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1993
2602
  if (has("ValidateNested") || typeRefName) {
2603
+ const typeArgs = genericTypeArgNames(typeNode);
1994
2604
  const childName = typeRefName ?? singularClassName(typeText);
1995
2605
  if (childName) {
1996
- const childNode = buildNestedReference(childName, classFile, ctx);
2606
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
1997
2607
  const wrapArray = has("IsArray") || isArrayType;
1998
2608
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
1999
2609
  return applyPresence(node2, decorators);
@@ -2118,10 +2728,12 @@ function baseFromType(typeText, isArrayType) {
2118
2728
  return { kind: "unknown" };
2119
2729
  }
2120
2730
  }
2121
- function buildNestedReference(className, fromFile, ctx) {
2122
- if (ctx.visiting.has(className)) {
2123
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2124
- ctx.emittedClasses.set(className, reserved);
2731
+ function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
2732
+ const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
2733
+ const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
2734
+ if (ctx.visiting.has(cacheKey)) {
2735
+ const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
2736
+ ctx.emittedClasses.set(cacheKey, reserved);
2125
2737
  ctx.recursiveSchemas.add(reserved);
2126
2738
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2127
2739
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -2140,19 +2752,27 @@ function buildNestedReference(className, fromFile, ctx) {
2140
2752
  }
2141
2753
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2142
2754
  }
2143
- const existing = ctx.emittedClasses.get(className);
2755
+ const existing = ctx.emittedClasses.get(cacheKey);
2144
2756
  if (existing) return { kind: "ref", name: existing };
2145
- const schemaName = aliasFor(className, ctx);
2757
+ const schemaName = aliasFor(schemaBase, ctx);
2146
2758
  const resolved = findType(className, fromFile, ctx.project);
2147
2759
  if (!resolved || resolved.kind !== "class") {
2148
2760
  return { kind: "object", fields: [], passthrough: true };
2149
2761
  }
2150
- ctx.emittedClasses.set(className, schemaName);
2151
- ctx.visiting.add(className);
2762
+ const params = resolved.decl.getTypeParameters().map((p) => p.getName());
2763
+ const newBindings = [];
2764
+ params.forEach((param, i) => {
2765
+ const arg = typeArgs[i];
2766
+ if (arg) newBindings.push([param, arg]);
2767
+ });
2768
+ for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
2769
+ ctx.emittedClasses.set(cacheKey, schemaName);
2770
+ ctx.visiting.add(cacheKey);
2152
2771
  ctx.depth += 1;
2153
2772
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
2154
2773
  ctx.depth -= 1;
2155
- ctx.visiting.delete(className);
2774
+ ctx.visiting.delete(cacheKey);
2775
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
2156
2776
  ctx.named.set(schemaName, childNode);
2157
2777
  return { kind: "ref", name: schemaName };
2158
2778
  }
@@ -2199,6 +2819,39 @@ function messageRaw(decorator) {
2199
2819
  }
2200
2820
  return void 0;
2201
2821
  }
2822
+ function resolveDiscriminator(decorator) {
2823
+ const optsArg = decorator?.getArguments()[1];
2824
+ if (!optsArg || !import_ts_morph4.Node.isObjectLiteralExpression(optsArg)) return null;
2825
+ let discProp;
2826
+ for (const prop of optsArg.getProperties()) {
2827
+ if (import_ts_morph4.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
2828
+ discProp = prop.getInitializer();
2829
+ }
2830
+ }
2831
+ if (!discProp || !import_ts_morph4.Node.isObjectLiteralExpression(discProp)) return null;
2832
+ let property = null;
2833
+ const subTypes = [];
2834
+ for (const prop of discProp.getProperties()) {
2835
+ if (!import_ts_morph4.Node.isPropertyAssignment(prop)) continue;
2836
+ const name = prop.getName();
2837
+ const init = prop.getInitializer();
2838
+ if (!init) continue;
2839
+ if (name === "property" && import_ts_morph4.Node.isStringLiteral(init)) {
2840
+ property = init.getLiteralValue();
2841
+ } else if (name === "subTypes" && import_ts_morph4.Node.isArrayLiteralExpression(init)) {
2842
+ for (const el of init.getElements()) {
2843
+ if (!import_ts_morph4.Node.isObjectLiteralExpression(el)) continue;
2844
+ for (const p of el.getProperties()) {
2845
+ if (!import_ts_morph4.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
2846
+ const nameInit = p.getInitializer();
2847
+ if (nameInit && import_ts_morph4.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
2848
+ }
2849
+ }
2850
+ }
2851
+ }
2852
+ if (!property || subTypes.length === 0) return null;
2853
+ return { property, subTypes };
2854
+ }
2202
2855
  function resolveTypeFactoryName(decorator) {
2203
2856
  const arg = firstArg(decorator);
2204
2857
  if (!arg) return null;
@@ -2212,6 +2865,17 @@ function singularClassName(typeText) {
2212
2865
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2213
2866
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2214
2867
  }
2868
+ function genericTypeArgNames(typeNode) {
2869
+ if (!typeNode || !import_ts_morph4.Node.isTypeReference(typeNode)) return [];
2870
+ const names = [];
2871
+ for (const arg of typeNode.getTypeArguments()) {
2872
+ if (!import_ts_morph4.Node.isTypeReference(arg)) return [];
2873
+ const tn = arg.getTypeName();
2874
+ if (!import_ts_morph4.Node.isIdentifier(tn)) return [];
2875
+ names.push(tn.getText());
2876
+ }
2877
+ return names;
2878
+ }
2215
2879
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2216
2880
  const arg = firstArg(decorator);
2217
2881
  if (!arg) return null;
@@ -2266,6 +2930,9 @@ var import_ts_morph5 = require("ts-morph");
2266
2930
 
2267
2931
  // src/discovery/enum-resolution.ts
2268
2932
  var _enumCache = /* @__PURE__ */ new WeakMap();
2933
+ function clearEnumCache(project) {
2934
+ _enumCache.delete(project);
2935
+ }
2269
2936
  function resolveEnumValues(name, sourceFile, project) {
2270
2937
  let byKey = _enumCache.get(project);
2271
2938
  if (byKey === void 0) {
@@ -2734,24 +3401,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2734
3401
  "Map",
2735
3402
  "Set"
2736
3403
  ]);
2737
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3404
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2738
3405
  if (depth <= 0) return "unknown";
2739
3406
  if (import_ts_morph7.Node.isArrayTypeNode(typeNode)) {
2740
3407
  const elementType = typeNode.getElementTypeNode();
2741
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3408
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
2742
3409
  }
2743
3410
  if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
2744
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3411
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
2745
3412
  }
2746
3413
  if (import_ts_morph7.Node.isIntersectionTypeNode(typeNode)) {
2747
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3414
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
2748
3415
  }
2749
3416
  if (import_ts_morph7.Node.isParenthesizedTypeNode(typeNode)) {
2750
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3417
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
2751
3418
  }
2752
3419
  if (import_ts_morph7.Node.isTypeReference(typeNode)) {
2753
3420
  const typeName = typeNode.getTypeName();
2754
3421
  const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3422
+ const bound = subst.get(name);
3423
+ if (bound !== void 0) return bound;
2755
3424
  if (name === "string" || name === "number" || name === "boolean") return name;
2756
3425
  if (name === "Date") return "string";
2757
3426
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -2759,14 +3428,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2759
3428
  return "unknown";
2760
3429
  const wrapperMode = WRAPPER_TYPES[name];
2761
3430
  if (wrapperMode) {
2762
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3431
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
2763
3432
  }
2764
3433
  if (PASSTHROUGH_UTILITY.has(name)) {
2765
3434
  return typeNode.getText();
2766
3435
  }
2767
3436
  const resolved = findType(name, sourceFile, project);
2768
3437
  if (resolved) {
2769
- return expandTypeDecl(resolved, project, depth - 1);
3438
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
3439
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
2770
3440
  }
2771
3441
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
2772
3442
  return "unknown";
@@ -2779,32 +3449,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2779
3449
  if (kind === import_ts_morph7.SyntaxKind.AnyKeyword) return "unknown";
2780
3450
  return typeNode.getText();
2781
3451
  }
2782
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
3452
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
2783
3453
  const typeArgs = typeNode.getTypeArguments();
2784
3454
  const firstTypeArg = typeArgs[0];
2785
3455
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2786
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3456
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
2787
3457
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
2788
3458
  }
2789
3459
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2790
3460
  }
2791
- function expandTypeDecl(result, project, depth) {
3461
+ function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
3462
+ if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
3463
+ const params = result.decl.getTypeParameters().map((p) => p.getName());
3464
+ if (params.length === 0) return /* @__PURE__ */ new Map();
3465
+ const args = typeNode.getTypeArguments();
3466
+ const subst = /* @__PURE__ */ new Map();
3467
+ params.forEach((param, i) => {
3468
+ const arg = args[i];
3469
+ if (arg)
3470
+ subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
3471
+ });
3472
+ return subst;
3473
+ }
3474
+ function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
2792
3475
  if (depth < 0) return "unknown";
2793
3476
  switch (result.kind) {
2794
3477
  case "class":
2795
- return resolvePropertied(result.decl, result.file, project, depth);
3478
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2796
3479
  case "interface":
2797
- return resolvePropertied(result.decl, result.file, project, depth);
3480
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2798
3481
  case "typeAlias":
2799
3482
  if (result.typeNode) {
2800
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
3483
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
2801
3484
  }
2802
3485
  return result.text;
2803
3486
  case "enum":
2804
3487
  return result.members.join(" | ");
2805
3488
  }
2806
3489
  }
2807
- function resolvePropertied(decl, sourceFile, project, depth) {
3490
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2808
3491
  if (depth < 0) return "unknown";
2809
3492
  const lines = [];
2810
3493
  for (const prop of decl.getProperties()) {
@@ -2813,7 +3496,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
2813
3496
  const propTypeNode = prop.getTypeNode();
2814
3497
  let propType = "unknown";
2815
3498
  if (propTypeNode) {
2816
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
3499
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2817
3500
  }
2818
3501
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
2819
3502
  }
@@ -2862,7 +3545,7 @@ function extractParamsType(method, sourceFile, project) {
2862
3545
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
2863
3546
  }
2864
3547
  function extractResponseType(method, sourceFile, project) {
2865
- const apiResponseDecorator = method.getDecorator("ApiResponse");
3548
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2866
3549
  if (apiResponseDecorator) {
2867
3550
  const args = apiResponseDecorator.getArguments();
2868
3551
  const optsArg = args[0];
@@ -2891,6 +3574,59 @@ function extractResponseType(method, sourceFile, project) {
2891
3574
  }
2892
3575
  return "unknown";
2893
3576
  }
3577
+ function apiResponseStatus(decorator) {
3578
+ const optsArg = decorator.getArguments()[0];
3579
+ if (!optsArg || !import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) return null;
3580
+ for (const prop of optsArg.getProperties()) {
3581
+ if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
3582
+ if (prop.getName() !== "status") continue;
3583
+ const val = prop.getInitializer();
3584
+ if (val && import_ts_morph7.Node.isNumericLiteral(val)) return Number(val.getLiteralValue());
3585
+ }
3586
+ return null;
3587
+ }
3588
+ function apiResponseTypeNode(decorator) {
3589
+ const optsArg = decorator.getArguments()[0];
3590
+ if (!optsArg || !import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) return null;
3591
+ for (const prop of optsArg.getProperties()) {
3592
+ if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
3593
+ if (prop.getName() !== "type") continue;
3594
+ const val = prop.getInitializer();
3595
+ if (!val) return null;
3596
+ if (import_ts_morph7.Node.isArrayLiteralExpression(val)) {
3597
+ const first = val.getElements()[0];
3598
+ return first ? { node: first, isArray: true } : null;
3599
+ }
3600
+ return { node: val, isArray: false };
3601
+ }
3602
+ return null;
3603
+ }
3604
+ function extractErrorType(method, sourceFile, project) {
3605
+ for (const decorator of method.getDecorators()) {
3606
+ if (decorator.getName() !== "ApiResponse") continue;
3607
+ const status = apiResponseStatus(decorator);
3608
+ if (status === null || status < 400) continue;
3609
+ const typeInfo = apiResponseTypeNode(decorator);
3610
+ if (!typeInfo) continue;
3611
+ const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
3612
+ const type = typeInfo.isArray ? `Array<${inner}>` : inner;
3613
+ let ref = null;
3614
+ if (import_ts_morph7.Node.isIdentifier(typeInfo.node)) {
3615
+ const name = typeInfo.node.getText();
3616
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3617
+ if (localDecl?.isExported()) {
3618
+ ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
3619
+ } else {
3620
+ const resolved = resolveImportedType(name, sourceFile, project);
3621
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
3622
+ ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
3623
+ }
3624
+ }
3625
+ }
3626
+ return { type, ref };
3627
+ }
3628
+ return null;
3629
+ }
2894
3630
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
2895
3631
  if (!import_ts_morph7.Node.isIdentifier(node)) return "unknown";
2896
3632
  const name = node.getText();
@@ -2906,17 +3642,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
2906
3642
  unwrapContainers: true
2907
3643
  });
2908
3644
  }
3645
+ var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
3646
+ var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
3647
+ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
3648
+ function detectStreamElement(method) {
3649
+ const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
3650
+ let node = method.getReturnTypeNode();
3651
+ node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
3652
+ const containerEl = streamContainerElement(node);
3653
+ if (containerEl) {
3654
+ return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
3655
+ }
3656
+ if (hasSse) return node ?? null;
3657
+ return null;
3658
+ }
3659
+ function streamContainerElement(node) {
3660
+ if (!node || !import_ts_morph7.Node.isTypeReference(node)) return null;
3661
+ const typeName = node.getTypeName();
3662
+ const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : "";
3663
+ if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
3664
+ return node.getTypeArguments()[0] ?? null;
3665
+ }
3666
+ return null;
3667
+ }
3668
+ function unwrapNamedContainer(node, names) {
3669
+ if (!node || !import_ts_morph7.Node.isTypeReference(node)) return node;
3670
+ const typeName = node.getTypeName();
3671
+ const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : "";
3672
+ if (names.has(name)) {
3673
+ return node.getTypeArguments()[0] ?? node;
3674
+ }
3675
+ return node;
3676
+ }
2909
3677
  function extractDtoContract(method, sourceFile, project) {
2910
3678
  let body = extractBodyType(method, sourceFile, project);
2911
3679
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
2912
3680
  const query = extractQueryType(method, sourceFile, project);
3681
+ const streamElement = detectStreamElement(method);
3682
+ const isStream = streamElement !== null;
2913
3683
  if (filterInfo && filterInfo.source === "body") {
2914
3684
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
2915
3685
  body = body ?? bodyType;
2916
3686
  }
2917
3687
  const paramsType = extractParamsType(method, sourceFile, project);
2918
- const response = extractResponseType(method, sourceFile, project);
2919
- if (body === null && query === null && paramsType === null && response === "unknown" && filterInfo === null) {
3688
+ const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3689
+ const errorInfo = extractErrorType(method, sourceFile, project);
3690
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
2920
3691
  return null;
2921
3692
  }
2922
3693
  let bodyRef = null;
@@ -2930,12 +3701,12 @@ function extractDtoContract(method, sourceFile, project) {
2930
3701
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2931
3702
  }
2932
3703
  }
2933
- const returnTypeNode = method.getReturnTypeNode();
3704
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
2934
3705
  if (returnTypeNode) {
2935
3706
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
2936
3707
  }
2937
- if (!responseRef) {
2938
- const apiResp = method.getDecorator("ApiResponse");
3708
+ if (!responseRef && !isStream) {
3709
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2939
3710
  if (apiResp) {
2940
3711
  const args = apiResp.getArguments();
2941
3712
  const optsArg = args[0];
@@ -2977,16 +3748,19 @@ function extractDtoContract(method, sourceFile, project) {
2977
3748
  query,
2978
3749
  body,
2979
3750
  response,
3751
+ error: errorInfo?.type ?? null,
2980
3752
  params: paramsType,
2981
3753
  queryRef,
2982
3754
  bodyRef,
2983
3755
  responseRef,
3756
+ errorRef: errorInfo?.ref ?? null,
2984
3757
  filterFields: filterInfo?.fieldNames ?? null,
2985
3758
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
2986
3759
  filterSource: filterInfo?.source ?? null,
2987
3760
  formWarnings,
2988
3761
  bodySchema,
2989
- querySchema
3762
+ querySchema,
3763
+ stream: isStream
2990
3764
  };
2991
3765
  }
2992
3766
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -3104,6 +3878,7 @@ function parseDefineContractCall(callExpr) {
3104
3878
  let query = null;
3105
3879
  let body = null;
3106
3880
  let response = "unknown";
3881
+ let error = null;
3107
3882
  let bodyZodText = null;
3108
3883
  let queryZodText = null;
3109
3884
  for (const prop of optsArg.getProperties()) {
@@ -3119,25 +3894,38 @@ function parseDefineContractCall(callExpr) {
3119
3894
  bodyZodText = val.getText();
3120
3895
  } else if (propName === "response") {
3121
3896
  response = zodAstToTs(val);
3897
+ } else if (propName === "error") {
3898
+ error = zodAstToTs(val);
3122
3899
  }
3123
3900
  }
3124
- return { query, body, response, bodyZodText, queryZodText };
3901
+ return { query, body, response, error, bodyZodText, queryZodText };
3125
3902
  }
3126
3903
 
3127
3904
  // src/discovery/contracts-fast.ts
3128
3905
  async function discoverContractsFast(opts) {
3129
3906
  const { cwd, glob, tsconfig } = opts;
3130
- const tsconfigPath = tsconfig ? (0, import_node_path12.resolve)(tsconfig) : (0, import_node_path12.join)(cwd, "tsconfig.json");
3131
- let project;
3907
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3908
+ const project = createDiscoveryProject(tsconfigPath);
3909
+ const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3910
+ for (const f of files) {
3911
+ project.addSourceFileAtPath(f);
3912
+ }
3913
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3914
+ return extractAllRoutes(project);
3915
+ }
3916
+ function resolveTsconfigPath(cwd, tsconfig) {
3917
+ return tsconfig ? (0, import_node_path14.resolve)(tsconfig) : (0, import_node_path14.join)(cwd, "tsconfig.json");
3918
+ }
3919
+ function createDiscoveryProject(tsconfigPath) {
3132
3920
  try {
3133
- project = new import_ts_morph9.Project({
3921
+ return new import_ts_morph9.Project({
3134
3922
  tsConfigFilePath: tsconfigPath,
3135
3923
  skipAddingFilesFromTsConfig: true,
3136
3924
  skipLoadingLibFiles: true,
3137
3925
  skipFileDependencyResolution: true
3138
3926
  });
3139
3927
  } catch {
3140
- project = new import_ts_morph9.Project({
3928
+ return new import_ts_morph9.Project({
3141
3929
  skipAddingFilesFromTsConfig: true,
3142
3930
  skipLoadingLibFiles: true,
3143
3931
  skipFileDependencyResolution: true,
@@ -3148,20 +3936,105 @@ async function discoverContractsFast(opts) {
3148
3936
  }
3149
3937
  });
3150
3938
  }
3151
- const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3152
- for (const f of files) {
3153
- project.addSourceFileAtPath(f);
3154
- }
3155
- const routes = [];
3939
+ }
3940
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
3156
3941
  setDiscoveryContext(project, {
3157
3942
  projectRoot: cwd,
3158
3943
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3159
3944
  });
3945
+ }
3946
+ function extractRoutesFrom(project, controllerPaths) {
3947
+ const routes = [];
3948
+ for (const path of controllerPaths) {
3949
+ const sourceFile = project.getSourceFile(path);
3950
+ if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
3951
+ }
3952
+ return routes;
3953
+ }
3954
+ function extractAllRoutes(project) {
3955
+ const routes = [];
3160
3956
  for (const sourceFile of project.getSourceFiles()) {
3161
3957
  routes.push(...extractFromSourceFile(sourceFile, project));
3162
3958
  }
3163
3959
  return routes;
3164
3960
  }
3961
+ var PersistentDiscovery = class _PersistentDiscovery {
3962
+ project;
3963
+ cwd;
3964
+ glob;
3965
+ /** Absolute paths of the controllers currently loaded as extraction roots. */
3966
+ controllerPaths = /* @__PURE__ */ new Set();
3967
+ constructor(project, cwd, glob) {
3968
+ this.project = project;
3969
+ this.cwd = cwd;
3970
+ this.glob = glob;
3971
+ }
3972
+ /**
3973
+ * Build the initial persistent Project: create it, glob + add all controllers,
3974
+ * bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
3975
+ */
3976
+ static async create(opts) {
3977
+ const { cwd, glob, tsconfig } = opts;
3978
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3979
+ const project = createDiscoveryProject(tsconfigPath);
3980
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3981
+ const instance = new _PersistentDiscovery(project, cwd, glob);
3982
+ const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3983
+ for (const f of files) {
3984
+ project.addSourceFileAtPath(f);
3985
+ instance.controllerPaths.add(f);
3986
+ }
3987
+ return instance;
3988
+ }
3989
+ /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3990
+ discover() {
3991
+ return this.runExtraction();
3992
+ }
3993
+ /**
3994
+ * Re-discover after one or more files changed. Refreshes the changed file(s)
3995
+ * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3996
+ * to pick up added/removed controllers, clears the per-Project caches, then
3997
+ * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3998
+ * being exhaustive because re-globbing + refresh-on-presence covers the set.
3999
+ */
4000
+ async rediscover(changedPaths) {
4001
+ if (changedPaths) {
4002
+ for (const p of changedPaths) {
4003
+ const abs = (0, import_node_path14.resolve)(p);
4004
+ const sf = this.project.getSourceFile(abs);
4005
+ if (sf) {
4006
+ await sf.refreshFromFileSystem();
4007
+ }
4008
+ }
4009
+ }
4010
+ const globbed = new Set(
4011
+ await (0, import_fast_glob2.default)(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
4012
+ );
4013
+ for (const f of globbed) {
4014
+ if (!this.controllerPaths.has(f)) {
4015
+ try {
4016
+ this.project.addSourceFileAtPath(f);
4017
+ this.controllerPaths.add(f);
4018
+ } catch {
4019
+ }
4020
+ }
4021
+ }
4022
+ for (const f of this.controllerPaths) {
4023
+ if (!globbed.has(f)) {
4024
+ const sf = this.project.getSourceFile(f);
4025
+ if (sf) this.project.removeSourceFile(sf);
4026
+ this.controllerPaths.delete(f);
4027
+ }
4028
+ }
4029
+ return this.runExtraction();
4030
+ }
4031
+ /** Clear stale per-Project caches, then extract over the controller set. */
4032
+ runExtraction() {
4033
+ clearTypeResolutionCaches(this.project);
4034
+ clearEnumCache(this.project);
4035
+ return extractRoutesFrom(this.project, this.controllerPaths);
4036
+ }
4037
+ };
3165
4038
  function decoratorStringArg(decoratorExpr) {
3166
4039
  if (!decoratorExpr) return void 0;
3167
4040
  if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -3217,6 +4090,11 @@ function resolveVerb(method) {
3217
4090
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3218
4091
  }
3219
4092
  }
4093
+ const sseDecorator = method.getDecorator("Sse");
4094
+ if (sseDecorator) {
4095
+ const pathArg = sseDecorator.getArguments()[0];
4096
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
4097
+ }
3220
4098
  return null;
3221
4099
  }
3222
4100
  function readAsDecorator(node, label) {
@@ -3259,7 +4137,17 @@ function buildRoute(args) {
3259
4137
  };
3260
4138
  }
3261
4139
  function extractContractRoute(args) {
3262
- const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
4140
+ const {
4141
+ cls,
4142
+ method,
4143
+ applyContractDecorator,
4144
+ verb,
4145
+ prefix,
4146
+ className,
4147
+ sourceFile,
4148
+ project,
4149
+ seenNames
4150
+ } = args;
3263
4151
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3264
4152
  if (!firstDecoratorArg) return null;
3265
4153
  let contractDef = null;
@@ -3269,18 +4157,19 @@ function extractContractRoute(args) {
3269
4157
  contractDef = parseDefineContractCall(firstDecoratorArg);
3270
4158
  } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
3271
4159
  const identName = firstDecoratorArg.getText();
3272
- const varDecl = sourceFile.getVariableDeclaration(identName);
3273
- if (!varDecl) {
4160
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4161
+ if (!resolvedVar) {
3274
4162
  console.warn(
3275
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
4163
+ `[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
3276
4164
  );
3277
4165
  return null;
3278
4166
  }
4167
+ const { decl: varDecl, file: declFile } = resolvedVar;
3279
4168
  const initializer = varDecl.getInitializer();
3280
4169
  if (!initializer) return null;
3281
4170
  contractDef = parseDefineContractCall(initializer);
3282
4171
  if (contractDef && varDecl.isExported()) {
3283
- const filePath = sourceFile.getFilePath();
4172
+ const filePath = declFile.getFilePath();
3284
4173
  if (contractDef.body !== null) {
3285
4174
  bodyZodRef = { name: `${identName}.body`, filePath };
3286
4175
  }
@@ -3313,6 +4202,7 @@ function extractContractRoute(args) {
3313
4202
  query: contractDef.query,
3314
4203
  body: contractDef.body,
3315
4204
  response: contractDef.response,
4205
+ error: contractDef.error,
3316
4206
  // Path A: capture both the importable ref and the raw text. The emitter
3317
4207
  // prefers inlining the text (client-safe — re-exporting from a controller
3318
4208
  // would drag server-only deps into the client bundle).
@@ -3344,15 +4234,18 @@ function extractDtoRoute(args) {
3344
4234
  query: dtoContract?.query ?? null,
3345
4235
  body: dtoContract?.body ?? null,
3346
4236
  response: dtoContract?.response ?? "unknown",
4237
+ error: dtoContract?.error ?? null,
3347
4238
  queryRef: dtoContract?.queryRef ?? null,
3348
4239
  bodyRef: dtoContract?.bodyRef ?? null,
3349
4240
  responseRef: dtoContract?.responseRef ?? null,
4241
+ errorRef: dtoContract?.errorRef ?? null,
3350
4242
  filterFields: dtoContract?.filterFields ?? null,
3351
4243
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3352
4244
  filterSource: dtoContract?.filterSource ?? null,
3353
4245
  formWarnings: dtoContract?.formWarnings ?? [],
3354
4246
  bodySchema: dtoContract?.bodySchema ?? null,
3355
- querySchema: dtoContract?.querySchema ?? null
4247
+ querySchema: dtoContract?.querySchema ?? null,
4248
+ stream: dtoContract?.stream ?? false
3356
4249
  }
3357
4250
  });
3358
4251
  }
@@ -3376,6 +4269,7 @@ function extractFromSourceFile(sourceFile, project) {
3376
4269
  prefix,
3377
4270
  className,
3378
4271
  sourceFile,
4272
+ project,
3379
4273
  seenNames
3380
4274
  }) : extractDtoRoute({
3381
4275
  cls,
@@ -3394,9 +4288,9 @@ function extractFromSourceFile(sourceFile, project) {
3394
4288
  }
3395
4289
 
3396
4290
  // src/watch/lock-file.ts
3397
- var import_promises10 = require("fs/promises");
3398
- var import_promises11 = require("fs/promises");
3399
- var import_node_path13 = require("path");
4291
+ var import_promises12 = require("fs/promises");
4292
+ var import_promises13 = require("fs/promises");
4293
+ var import_node_path15 = require("path");
3400
4294
  var LOCK_FILE = ".watcher.lock";
3401
4295
  function isProcessAlive(pid) {
3402
4296
  try {
@@ -3407,21 +4301,21 @@ function isProcessAlive(pid) {
3407
4301
  }
3408
4302
  }
3409
4303
  async function acquireLock(outDir) {
3410
- await (0, import_promises11.mkdir)(outDir, { recursive: true });
3411
- const lockPath = (0, import_node_path13.join)(outDir, LOCK_FILE);
4304
+ await (0, import_promises13.mkdir)(outDir, { recursive: true });
4305
+ const lockPath = (0, import_node_path15.join)(outDir, LOCK_FILE);
3412
4306
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3413
4307
  try {
3414
- const fd = await (0, import_promises10.open)(lockPath, "wx");
4308
+ const fd = await (0, import_promises12.open)(lockPath, "wx");
3415
4309
  await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
3416
4310
  `, "utf8");
3417
4311
  await fd.close();
3418
4312
  } catch (err) {
3419
4313
  if (err.code === "EEXIST") {
3420
4314
  try {
3421
- const raw = await (0, import_promises11.readFile)(lockPath, "utf8");
4315
+ const raw = await (0, import_promises13.readFile)(lockPath, "utf8");
3422
4316
  const existing = JSON.parse(raw);
3423
4317
  if (isProcessAlive(existing.pid)) return null;
3424
- await (0, import_promises11.unlink)(lockPath);
4318
+ await (0, import_promises13.unlink)(lockPath);
3425
4319
  return acquireLock(outDir);
3426
4320
  } catch {
3427
4321
  return null;
@@ -3432,7 +4326,7 @@ async function acquireLock(outDir) {
3432
4326
  return {
3433
4327
  release: async () => {
3434
4328
  try {
3435
- await (0, import_promises11.unlink)(lockPath);
4329
+ await (0, import_promises13.unlink)(lockPath);
3436
4330
  } catch {
3437
4331
  }
3438
4332
  }
@@ -3448,7 +4342,7 @@ async function watch(config, onChange) {
3448
4342
  if (lock === null) {
3449
4343
  let holderPid = "unknown";
3450
4344
  try {
3451
- const raw = await (0, import_promises12.readFile)((0, import_node_path14.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
4345
+ const raw = await (0, import_promises14.readFile)((0, import_node_path16.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3452
4346
  const data = JSON.parse(raw);
3453
4347
  if (data.pid !== void 0) holderPid = String(data.pid);
3454
4348
  } catch {
@@ -3458,12 +4352,20 @@ async function watch(config, onChange) {
3458
4352
  );
3459
4353
  return NO_OP_WATCHER;
3460
4354
  }
4355
+ let discovery = null;
4356
+ async function getDiscovery() {
4357
+ if (discovery === null) {
4358
+ discovery = await PersistentDiscovery.create({
4359
+ cwd: config.codegen.cwd,
4360
+ glob: config.contracts.glob,
4361
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4362
+ });
4363
+ return discovery;
4364
+ }
4365
+ return discovery;
4366
+ }
3461
4367
  try {
3462
- const initialRoutes = await discoverContractsFast({
3463
- cwd: config.codegen.cwd,
3464
- glob: config.contracts.glob,
3465
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3466
- });
4368
+ const initialRoutes = (await getDiscovery()).discover();
3467
4369
  await generate(config, initialRoutes);
3468
4370
  } catch (err) {
3469
4371
  console.warn(
@@ -3476,7 +4378,7 @@ async function watch(config, onChange) {
3476
4378
  }
3477
4379
  let pagesDebounceTimer;
3478
4380
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3479
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, pagesGlob), {
4381
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, pagesGlob), {
3480
4382
  ignoreInitial: true,
3481
4383
  persistent: true,
3482
4384
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3502,23 +4404,23 @@ async function watch(config, onChange) {
3502
4404
  pagesWatcher.on("change", schedulePagesRegenerate);
3503
4405
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3504
4406
  let contractsDebounceTimer;
3505
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.contracts.glob), {
4407
+ const pendingChangedPaths = /* @__PURE__ */ new Set();
4408
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.contracts.glob), {
3506
4409
  ignoreInitial: true,
3507
4410
  persistent: true,
3508
4411
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3509
4412
  });
3510
- function scheduleContractsRegenerate() {
4413
+ function scheduleContractsRegenerate(changedPath) {
4414
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3511
4415
  if (contractsDebounceTimer !== void 0) {
3512
4416
  clearTimeout(contractsDebounceTimer);
3513
4417
  }
3514
4418
  contractsDebounceTimer = setTimeout(async () => {
3515
4419
  contractsDebounceTimer = void 0;
4420
+ const changed = [...pendingChangedPaths];
4421
+ pendingChangedPaths.clear();
3516
4422
  try {
3517
- const routes = await discoverContractsFast({
3518
- cwd: config.codegen.cwd,
3519
- glob: config.contracts.glob,
3520
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3521
- });
4423
+ const routes = await (await getDiscovery()).rediscover(changed);
3522
4424
  await generate(config, routes);
3523
4425
  } catch (err) {
3524
4426
  console.error(
@@ -3529,17 +4431,17 @@ async function watch(config, onChange) {
3529
4431
  onChange?.();
3530
4432
  }, config.contracts.debounceMs);
3531
4433
  }
3532
- contractsWatcher.on("add", scheduleContractsRegenerate);
3533
- contractsWatcher.on("change", scheduleContractsRegenerate);
3534
- contractsWatcher.on("unlink", scheduleContractsRegenerate);
3535
- const formsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.forms.watch), {
4434
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4435
+ contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4436
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4437
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.forms.watch), {
3536
4438
  ignoreInitial: true,
3537
4439
  persistent: true,
3538
4440
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3539
4441
  });
3540
- formsWatcher.on("add", scheduleContractsRegenerate);
3541
- formsWatcher.on("change", scheduleContractsRegenerate);
3542
- formsWatcher.on("unlink", scheduleContractsRegenerate);
4442
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4443
+ formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4444
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
3543
4445
  return {
3544
4446
  close: async () => {
3545
4447
  if (pagesDebounceTimer !== void 0) {
@@ -3608,17 +4510,21 @@ function renderTsType(node, ctx) {
3608
4510
  }
3609
4511
 
3610
4512
  // src/index.ts
3611
- var VERSION = "0.4.1";
4513
+ var VERSION = "0.5.0";
3612
4514
  // Annotate the CommonJS export names for ESM import in node:
3613
4515
  0 && (module.exports = {
3614
4516
  CodegenError,
3615
4517
  ConfigError,
3616
4518
  VERSION,
3617
4519
  acquireLock,
4520
+ buildMocksFile,
4521
+ buildOpenApiSpec,
3618
4522
  defineConfig,
3619
4523
  discoverContractsFast,
3620
4524
  emitApi,
3621
4525
  emitForms,
4526
+ emitMocks,
4527
+ emitOpenApi,
3622
4528
  emitRoutes,
3623
4529
  extractSchemaFromDto,
3624
4530
  generate,
@@ -3626,6 +4532,8 @@ var VERSION = "0.4.1";
3626
4532
  renderTsType,
3627
4533
  resolveAdapter,
3628
4534
  resolveConfig,
4535
+ schemaModuleToJsonSchema,
4536
+ schemaNodeToJsonSchema,
3629
4537
  watch
3630
4538
  });
3631
4539
  //# sourceMappingURL=index.cjs.map