@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.js CHANGED
@@ -131,6 +131,19 @@ function applyDefaults(userConfig, cwd) {
131
131
  enabled: userConfig.forms?.enabled ?? true,
132
132
  watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
133
133
  zodImport: userConfig.forms?.zodImport ?? "zod"
134
+ },
135
+ openapi: {
136
+ enabled: userConfig.openapi?.enabled ?? false,
137
+ fileName: userConfig.openapi?.fileName ?? "openapi.json",
138
+ title: userConfig.openapi?.title ?? "NestJS API",
139
+ version: userConfig.openapi?.version ?? "1.0.0",
140
+ description: userConfig.openapi?.description ?? null
141
+ },
142
+ mocks: {
143
+ enabled: userConfig.mocks?.enabled ?? false,
144
+ fileName: userConfig.mocks?.fileName ?? "mocks.ts",
145
+ seed: userConfig.mocks?.seed ?? 1,
146
+ baseUrl: userConfig.mocks?.baseUrl ?? ""
134
147
  }
135
148
  };
136
149
  }
@@ -168,8 +181,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
168
181
  }
169
182
 
170
183
  // src/generate.ts
171
- import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
172
- import { dirname as dirname2, join as join10 } from "path";
184
+ import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
185
+ import { dirname as dirname2, join as join12 } from "path";
173
186
 
174
187
  // src/discovery/pages.ts
175
188
  import { readFile } from "fs/promises";
@@ -698,17 +711,28 @@ function emitFilterQueryType(c) {
698
711
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
699
712
  }
700
713
  function buildResponseType(c, outDir) {
714
+ const respRef = c.contractSource.responseRef;
715
+ if (c.contractSource.stream) {
716
+ if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
717
+ return c.contractSource.response;
718
+ }
701
719
  if (c.controllerRef) {
702
720
  let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
703
721
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
704
722
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
705
723
  }
706
- const respRef = c.contractSource.responseRef;
707
724
  if (respRef) {
708
725
  return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
709
726
  }
710
727
  return c.contractSource.response;
711
728
  }
729
+ function buildErrorType(c) {
730
+ const errRef = c.contractSource.errorRef;
731
+ if (errRef) {
732
+ return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
733
+ }
734
+ return c.contractSource.error ?? "unknown";
735
+ }
712
736
  function emitRouterTypeBlock(tree, indent, outDir) {
713
737
  const pad = " ".repeat(indent);
714
738
  const lines = [];
@@ -723,12 +747,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
723
747
  const bodyRef = c.contractSource.bodyRef;
724
748
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
725
749
  const response = buildResponseType(c, outDir);
750
+ const error = buildErrorType(c);
726
751
  const params = buildParamsType(c.params);
727
752
  const safeMethod = JSON.stringify(method);
728
753
  const safeUrl = JSON.stringify(c.path);
729
754
  const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
755
+ const stream = c.contractSource.stream ? "true" : "false";
730
756
  lines.push(
731
- `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
757
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
732
758
  );
733
759
  } else {
734
760
  lines.push(`${pad}${objKey}: {`);
@@ -800,15 +826,21 @@ function emitReqHelper() {
800
826
  ""
801
827
  ];
802
828
  }
803
- function renderLeaf(pad, objKey, req, requestExpr, members) {
829
+ function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
804
830
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
805
831
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
832
+ if (streamExpr) {
833
+ lines.push(`${pad} stream: () => ${streamExpr},`);
834
+ }
806
835
  for (const [name, value] of Object.entries(members)) {
807
836
  lines.push(`${pad} ${name}: ${value},`);
808
837
  }
809
838
  lines.push(`${pad}}),`);
810
839
  return lines;
811
840
  }
841
+ function renderStreamExpr(req) {
842
+ return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
843
+ }
812
844
  function emitApiObjectBlock(tree, indent, p) {
813
845
  const pad = " ".repeat(indent);
814
846
  const lines = [];
@@ -843,7 +875,8 @@ function emitApiObjectBlock(tree, indent, p) {
843
875
  }
844
876
  const members = {};
845
877
  for (const [name, { value }] of owned) members[name] = value;
846
- lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
878
+ const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
879
+ lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
847
880
  }
848
881
  return lines;
849
882
  }
@@ -881,6 +914,8 @@ var ROUTE_NAMESPACE = [
881
914
  ' export type Params<K extends string> = ResolveByName<K, "params">;',
882
915
  ' export type Error<K extends string> = ResolveByName<K, "error">;',
883
916
  ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
917
+ " /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
918
+ ' export type Stream<K extends string> = ResolveByName<K, "response">;',
884
919
  " export type Request<K extends string> = {",
885
920
  " body: Body<K>;",
886
921
  " query: Query<K>;",
@@ -897,6 +932,7 @@ var PATH_NAMESPACE = [
897
932
  ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
898
933
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
899
934
  ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
935
+ ' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
900
936
  "}",
901
937
  ""
902
938
  ];
@@ -908,6 +944,7 @@ var EMPTY_ROUTE_NAMESPACE = [
908
944
  " export type Params<K extends string> = never;",
909
945
  " export type Error<K extends string> = never;",
910
946
  " export type FilterFields<K extends string> = never;",
947
+ " export type Stream<K extends string> = never;",
911
948
  " export type Request<K extends string> = { body: never; query: never; params: never };",
912
949
  "}",
913
950
  ""
@@ -920,6 +957,7 @@ var EMPTY_PATH_NAMESPACE = [
920
957
  " export type Params<M extends string, U extends string> = never;",
921
958
  " export type Error<M extends string, U extends string> = never;",
922
959
  " export type FilterFields<M extends string, U extends string> = never;",
960
+ " export type Stream<M extends string, U extends string> = never;",
923
961
  "}",
924
962
  ""
925
963
  ];
@@ -943,7 +981,7 @@ function buildApiFile(routes, outDir, opts = {}) {
943
981
  for (const r of contracted) {
944
982
  const cs = r.contract?.contractSource;
945
983
  if (!cs) continue;
946
- const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
984
+ const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
947
985
  for (const ref of refs) {
948
986
  if (!ref) continue;
949
987
  let names = importsByFile.get(ref.filePath);
@@ -1376,11 +1414,470 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1376
1414
  await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
1377
1415
  }
1378
1416
 
1379
- // src/emit/emit-pages.ts
1417
+ // src/emit/emit-mocks.ts
1380
1418
  import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
1381
- import { join as join8, relative as relative5 } from "path";
1382
- async function emitPages(pages, outDir, _options = {}) {
1419
+ import { join as join8 } from "path";
1420
+
1421
+ // src/ir/schema-node-to-json-schema.ts
1422
+ var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
1423
+ function parseLiteral(raw) {
1424
+ const t = raw.trim();
1425
+ if (t === "true") return true;
1426
+ if (t === "false") return false;
1427
+ if (t === "null") return null;
1428
+ const q = t[0];
1429
+ if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
1430
+ return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
1431
+ }
1432
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
1433
+ return Number(t);
1434
+ }
1435
+ return t;
1436
+ }
1437
+ function literalsType(values) {
1438
+ const types = new Set(values.map((v) => v === null ? "null" : typeof v));
1439
+ if (types.size === 1) {
1440
+ const only = [...types][0];
1441
+ if (only === "string") return "string";
1442
+ if (only === "number") return "number";
1443
+ if (only === "boolean") return "boolean";
1444
+ }
1445
+ return void 0;
1446
+ }
1447
+ function convert(node, ctx) {
1448
+ switch (node.kind) {
1449
+ case "string": {
1450
+ const out = { type: "string" };
1451
+ for (const c of node.checks) {
1452
+ if (c.check === "email") out.format = "email";
1453
+ else if (c.check === "url") out.format = "uri";
1454
+ else if (c.check === "uuid") out.format = "uuid";
1455
+ else if (c.check === "min") out.minLength = Number(c.value);
1456
+ else if (c.check === "max") out.maxLength = Number(c.value);
1457
+ else if (c.check === "regex") {
1458
+ const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
1459
+ out.pattern = m ? m[1] : c.pattern;
1460
+ }
1461
+ }
1462
+ return out;
1463
+ }
1464
+ case "number": {
1465
+ const out = { type: "number" };
1466
+ for (const c of node.checks) {
1467
+ if (c.check === "int") out.type = "integer";
1468
+ else if (c.check === "min") out.minimum = Number(c.value);
1469
+ else if (c.check === "max") out.maximum = Number(c.value);
1470
+ else if (c.check === "positive") out.exclusiveMinimum = 0;
1471
+ else if (c.check === "negative") out.exclusiveMaximum = 0;
1472
+ }
1473
+ return out;
1474
+ }
1475
+ case "boolean":
1476
+ return { type: "boolean" };
1477
+ case "date":
1478
+ return { type: "string", format: "date-time" };
1479
+ case "unknown":
1480
+ return node.note ? { description: node.note } : {};
1481
+ case "instanceof":
1482
+ return { type: "object", description: `instanceof ${node.ctor}` };
1483
+ case "enum": {
1484
+ const values = node.literals.map(parseLiteral);
1485
+ const t = literalsType(values);
1486
+ const out = { enum: values };
1487
+ if (t) out.type = t;
1488
+ return out;
1489
+ }
1490
+ case "literal": {
1491
+ const value = parseLiteral(node.raw);
1492
+ const out = { const: value };
1493
+ const t = literalsType([value]);
1494
+ if (t) out.type = t;
1495
+ return out;
1496
+ }
1497
+ case "union": {
1498
+ const options = node.options.map((o) => convert(o, ctx));
1499
+ const out = { oneOf: options };
1500
+ if (node.discriminator) {
1501
+ out.discriminator = { propertyName: node.discriminator };
1502
+ }
1503
+ return out;
1504
+ }
1505
+ case "object": {
1506
+ const properties = {};
1507
+ const required = [];
1508
+ for (const f of node.fields) {
1509
+ if (f.value.kind === "optional") {
1510
+ properties[f.key] = convert(f.value.inner, ctx);
1511
+ } else {
1512
+ properties[f.key] = convert(f.value, ctx);
1513
+ required.push(f.key);
1514
+ }
1515
+ }
1516
+ const out = { type: "object", properties };
1517
+ if (required.length > 0) out.required = required;
1518
+ out.additionalProperties = node.passthrough;
1519
+ return out;
1520
+ }
1521
+ case "array":
1522
+ return { type: "array", items: convert(node.element, ctx) };
1523
+ case "optional":
1524
+ return widenNullable(convert(node.inner, ctx));
1525
+ case "ref":
1526
+ case "lazyRef":
1527
+ return { $ref: `${ctx.refPrefix}${node.name}` };
1528
+ case "annotated":
1529
+ return convert(node.inner, ctx);
1530
+ }
1531
+ }
1532
+ function widenNullable(schema) {
1533
+ if (schema.$ref) {
1534
+ return { anyOf: [schema, { type: "null" }] };
1535
+ }
1536
+ if (typeof schema.type === "string") {
1537
+ return { ...schema, type: [schema.type, "null"] };
1538
+ }
1539
+ if (Array.isArray(schema.type)) {
1540
+ return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
1541
+ }
1542
+ return { anyOf: [schema, { type: "null" }] };
1543
+ }
1544
+ function schemaNodeToJsonSchema(node, ctx = DEFAULT_CTX) {
1545
+ return convert(node, ctx);
1546
+ }
1547
+ function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
1548
+ const named = {};
1549
+ for (const [name, node] of mod.named) {
1550
+ named[name] = convert(node, ctx);
1551
+ }
1552
+ return { root: convert(mod.root, ctx), named };
1553
+ }
1554
+
1555
+ // src/emit/mock-gen-runtime.ts
1556
+ var MOCK_GEN_RUNTIME = `
1557
+ /** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
1558
+ function makeRng(seed) {
1559
+ let a = seed >>> 0;
1560
+ return {
1561
+ next() {
1562
+ a |= 0;
1563
+ a = (a + 0x6d2b79f5) | 0;
1564
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
1565
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
1566
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
1567
+ },
1568
+ };
1569
+ }
1570
+
1571
+ function __pick(rng, items) {
1572
+ return items[Math.floor(rng.next() * items.length)];
1573
+ }
1574
+
1575
+ function __intBetween(rng, min, max) {
1576
+ return Math.floor(rng.next() * (max - min + 1)) + min;
1577
+ }
1578
+
1579
+ const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
1580
+ const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
1581
+ const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
1582
+
1583
+ function __fakeWords(rng, count) {
1584
+ let out = [];
1585
+ for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
1586
+ return out.join(' ');
1587
+ }
1588
+
1589
+ function __hex(rng, len) {
1590
+ let s = '';
1591
+ for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
1592
+ return s;
1593
+ }
1594
+
1595
+ function __fakeUuid(rng) {
1596
+ return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
1597
+ }
1598
+
1599
+ function __fakeString(rng, schema) {
1600
+ switch (schema.format) {
1601
+ case 'email':
1602
+ return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
1603
+ case 'uri':
1604
+ case 'url':
1605
+ return 'https://example.com/' + __pick(rng, __WORDS);
1606
+ case 'uuid':
1607
+ return __fakeUuid(rng);
1608
+ case 'date-time':
1609
+ return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
1610
+ default:
1611
+ return __fakeWords(rng, __intBetween(rng, 1, 3));
1612
+ }
1613
+ }
1614
+
1615
+ /** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
1616
+ function generateMock(schema, rng, defs, depth) {
1617
+ defs = defs || {};
1618
+ depth = depth || 0;
1619
+ if (schema.$ref) {
1620
+ const name = schema.$ref.replace('#/components/schemas/', '');
1621
+ const target = defs[name];
1622
+ if (!target || depth > 4) return null;
1623
+ return generateMock(target, rng, defs, depth + 1);
1624
+ }
1625
+ if ('const' in schema) return schema.const;
1626
+ if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
1627
+ if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
1628
+ if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
1629
+ let type = Array.isArray(schema.type)
1630
+ ? (schema.type.filter((t) => t !== 'null')[0] || 'null')
1631
+ : schema.type;
1632
+ switch (type) {
1633
+ case 'string':
1634
+ return __fakeString(rng, schema);
1635
+ case 'integer':
1636
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
1637
+ case 'number':
1638
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
1639
+ case 'boolean':
1640
+ return rng.next() < 0.5;
1641
+ case 'null':
1642
+ return null;
1643
+ case 'array': {
1644
+ const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
1645
+ const items = schema.items || {};
1646
+ let arr = [];
1647
+ for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
1648
+ return arr;
1649
+ }
1650
+ case 'object': {
1651
+ const out = {};
1652
+ const props = schema.properties || {};
1653
+ for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
1654
+ return out;
1655
+ }
1656
+ default:
1657
+ return {};
1658
+ }
1659
+ }
1660
+ `.trim();
1661
+
1662
+ // src/emit/emit-mocks.ts
1663
+ var REF_PREFIX = "#/components/schemas/";
1664
+ function toMswPath(path, baseUrl) {
1665
+ return `${baseUrl}${path}`;
1666
+ }
1667
+ function responseSchemaFor(route, defs) {
1668
+ const cs = route.contract.contractSource;
1669
+ if (cs.responseSchema) {
1670
+ const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
1671
+ for (const [name, node] of Object.entries(named)) {
1672
+ if (!(name in defs)) defs[name] = node;
1673
+ }
1674
+ return root;
1675
+ }
1676
+ return {};
1677
+ }
1678
+ function buildMocksFile(routes, opts = {}) {
1679
+ const seed = opts.seed ?? 1;
1680
+ const baseUrl = opts.baseUrl ?? "";
1681
+ const contracted = routes.filter((r) => r.contract);
1682
+ const defs = {};
1683
+ const handlers = [];
1684
+ for (const r of contracted) {
1685
+ const schema = responseSchemaFor(r, defs);
1686
+ const method = r.method.toLowerCase();
1687
+ const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
1688
+ const path = toMswPath(r.path, baseUrl);
1689
+ const cs = r.contract.contractSource;
1690
+ const schemaLiteral = JSON.stringify(schema);
1691
+ const pathLit = JSON.stringify(path);
1692
+ if (cs.stream) {
1693
+ handlers.push(
1694
+ [
1695
+ ` // ${r.name} (stream)`,
1696
+ ` http.${mswMethod}(${pathLit}, () => {`,
1697
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1698
+ " const body = `data: ${JSON.stringify(value)}\\n\\n`;",
1699
+ " return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
1700
+ " }),"
1701
+ ].join("\n")
1702
+ );
1703
+ } else {
1704
+ handlers.push(
1705
+ [
1706
+ ` // ${r.name}`,
1707
+ ` http.${mswMethod}(${pathLit}, () => {`,
1708
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
1709
+ " return HttpResponse.json(value);",
1710
+ " }),"
1711
+ ].join("\n")
1712
+ );
1713
+ }
1714
+ }
1715
+ const lines = [
1716
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1717
+ "// MSW handlers returning deterministic, schema-shaped mock data.",
1718
+ "/* eslint-disable */",
1719
+ "// @ts-nocheck",
1720
+ "",
1721
+ "import { http, HttpResponse } from 'msw';",
1722
+ "",
1723
+ `const SEED = ${seed};`,
1724
+ "",
1725
+ "// ---------------------------------------------------------------------------",
1726
+ "// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
1727
+ "// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
1728
+ "// ---------------------------------------------------------------------------",
1729
+ MOCK_GEN_RUNTIME,
1730
+ "",
1731
+ "// Shared component schemas referenced by $ref.",
1732
+ `const DEFS = ${JSON.stringify(defs, null, 2)};`,
1733
+ "",
1734
+ "/** MSW request handlers, one per contracted route. */",
1735
+ "export const handlers = [",
1736
+ ...handlers,
1737
+ "];",
1738
+ ""
1739
+ ];
1740
+ return lines.join("\n");
1741
+ }
1742
+ async function emitMocks(routes, outDir, opts = {}) {
1383
1743
  await mkdir5(outDir, { recursive: true });
1744
+ const content = buildMocksFile(routes, opts);
1745
+ const fileName = opts.fileName ?? "mocks.ts";
1746
+ await writeFile5(join8(outDir, fileName), content, "utf8");
1747
+ }
1748
+
1749
+ // src/emit/emit-openapi.ts
1750
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1751
+ import { join as join9 } from "path";
1752
+ var REF_PREFIX2 = "#/components/schemas/";
1753
+ function toOpenApiPath(path) {
1754
+ return path.replace(/:([^/]+)/g, "{$1}");
1755
+ }
1756
+ function positionSchema(schema, tsType, components) {
1757
+ if (schema) {
1758
+ const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
1759
+ for (const [name, node] of Object.entries(named)) {
1760
+ if (!(name in components)) components[name] = node;
1761
+ }
1762
+ return root;
1763
+ }
1764
+ return tsType ? { description: tsType } : {};
1765
+ }
1766
+ function buildParameters(route) {
1767
+ const params = [];
1768
+ for (const p of route.params) {
1769
+ if (p.source === "path") {
1770
+ params.push({
1771
+ name: p.name,
1772
+ in: "path",
1773
+ required: true,
1774
+ schema: { type: "string" }
1775
+ });
1776
+ } else if (p.source === "query") {
1777
+ params.push({
1778
+ name: p.name,
1779
+ in: "query",
1780
+ required: false,
1781
+ schema: { type: "string" }
1782
+ });
1783
+ } else if (p.source === "header") {
1784
+ params.push({
1785
+ name: p.name,
1786
+ in: "header",
1787
+ required: false,
1788
+ schema: { type: "string" }
1789
+ });
1790
+ }
1791
+ }
1792
+ return params;
1793
+ }
1794
+ function buildResponses(cs, components) {
1795
+ const responses = {};
1796
+ const successSchema = positionSchema(
1797
+ // Prefer rich response IR when present; otherwise fall back to the TS type.
1798
+ cs.responseSchema ?? null,
1799
+ cs.response,
1800
+ components
1801
+ );
1802
+ const successContentType = cs.stream ? "text/event-stream" : "application/json";
1803
+ responses["200"] = {
1804
+ description: cs.stream ? "Server-sent event stream" : "Successful response",
1805
+ content: { [successContentType]: { schema: successSchema } }
1806
+ };
1807
+ const errorSchema = positionSchema(null, cs.error ?? null, components);
1808
+ const errorBody = {
1809
+ description: "Error response",
1810
+ content: { "application/json": { schema: errorSchema } }
1811
+ };
1812
+ if (cs.error || cs.errorRef) {
1813
+ responses["400"] = errorBody;
1814
+ responses.default = errorBody;
1815
+ } else {
1816
+ responses.default = {
1817
+ description: "Error response",
1818
+ content: { "application/json": { schema: {} } }
1819
+ };
1820
+ }
1821
+ return responses;
1822
+ }
1823
+ function buildOperation(route, components) {
1824
+ const cs = route.contract.contractSource;
1825
+ const op = {
1826
+ operationId: route.name,
1827
+ parameters: buildParameters(route),
1828
+ responses: buildResponses(cs, components)
1829
+ };
1830
+ const method = route.method.toUpperCase();
1831
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
1832
+ if (hasBody && (cs.bodySchema || cs.body)) {
1833
+ const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
1834
+ op.requestBody = {
1835
+ required: true,
1836
+ content: { "application/json": { schema: bodySchema } }
1837
+ };
1838
+ }
1839
+ return op;
1840
+ }
1841
+ function buildOpenApiSpec(routes, opts = {}) {
1842
+ const components = {};
1843
+ const paths = {};
1844
+ for (const route of routes) {
1845
+ if (!route.contract) continue;
1846
+ const oaPath = toOpenApiPath(route.path);
1847
+ const method = route.method.toLowerCase();
1848
+ let pathItem = paths[oaPath];
1849
+ if (!pathItem) {
1850
+ pathItem = {};
1851
+ paths[oaPath] = pathItem;
1852
+ }
1853
+ pathItem[method] = buildOperation(route, components);
1854
+ }
1855
+ const info = opts.info ?? {};
1856
+ const doc = {
1857
+ openapi: "3.1.0",
1858
+ info: {
1859
+ title: info.title ?? "NestJS API",
1860
+ version: info.version ?? "1.0.0",
1861
+ ...info.description ? { description: info.description } : {}
1862
+ },
1863
+ paths,
1864
+ components: { schemas: components }
1865
+ };
1866
+ return doc;
1867
+ }
1868
+ async function emitOpenApi(routes, outDir, opts = {}) {
1869
+ await mkdir6(outDir, { recursive: true });
1870
+ const doc = buildOpenApiSpec(routes, opts);
1871
+ const fileName = opts.fileName ?? "openapi.json";
1872
+ await writeFile6(join9(outDir, fileName), `${JSON.stringify(doc, null, 2)}
1873
+ `, "utf8");
1874
+ }
1875
+
1876
+ // src/emit/emit-pages.ts
1877
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1878
+ import { join as join10, relative as relative5 } from "path";
1879
+ async function emitPages(pages, outDir, _options = {}) {
1880
+ await mkdir7(outDir, { recursive: true });
1384
1881
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1385
1882
  const augBody = pages.map((p) => {
1386
1883
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
@@ -1399,7 +1896,7 @@ ${augBody}
1399
1896
  }
1400
1897
  ${sharedPropsBlock}}
1401
1898
  `;
1402
- await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
1899
+ await writeFile7(join10(outDir, "pages.d.ts"), content, "utf8");
1403
1900
  }
1404
1901
  function buildSharedPropsBlock(sharedProps) {
1405
1902
  if (!sharedProps) return "";
@@ -1429,12 +1926,12 @@ function needsQuotes(name) {
1429
1926
  }
1430
1927
 
1431
1928
  // src/emit/emit-routes.ts
1432
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1433
- import { join as join9 } from "path";
1929
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
1930
+ import { join as join11 } from "path";
1434
1931
  async function emitRoutes(routes, outDir) {
1435
- await mkdir6(outDir, { recursive: true });
1932
+ await mkdir8(outDir, { recursive: true });
1436
1933
  const content = buildRoutesFile(routes);
1437
- await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
1934
+ await writeFile8(join11(outDir, "routes.ts"), content, "utf8");
1438
1935
  }
1439
1936
  function buildRoutesFile(routes) {
1440
1937
  if (routes.length === 0) {
@@ -1582,24 +2079,41 @@ async function generate(config, inputRoutes = []) {
1582
2079
  });
1583
2080
  }
1584
2081
  const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
2082
+ if (hasContracts && config.openapi.enabled) {
2083
+ await emitOpenApi(routes, config.codegen.outDir, {
2084
+ fileName: config.openapi.fileName,
2085
+ info: {
2086
+ title: config.openapi.title,
2087
+ version: config.openapi.version,
2088
+ ...config.openapi.description ? { description: config.openapi.description } : {}
2089
+ }
2090
+ });
2091
+ }
2092
+ if (hasContracts && config.mocks.enabled) {
2093
+ await emitMocks(routes, config.codegen.outDir, {
2094
+ fileName: config.mocks.fileName,
2095
+ seed: config.mocks.seed,
2096
+ baseUrl: config.mocks.baseUrl
2097
+ });
2098
+ }
1585
2099
  await emitIndex(config.codegen.outDir, hasContracts, hasForms);
1586
2100
  if (extensions.length > 0) {
1587
2101
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1588
2102
  for (const file of extraFiles) {
1589
- const dest = join10(config.codegen.outDir, file.path);
1590
- await mkdir7(dirname2(dest), { recursive: true });
1591
- await writeFile7(dest, file.contents, "utf8");
2103
+ const dest = join12(config.codegen.outDir, file.path);
2104
+ await mkdir9(dirname2(dest), { recursive: true });
2105
+ await writeFile9(dest, file.contents, "utf8");
1592
2106
  }
1593
2107
  }
1594
2108
  }
1595
2109
 
1596
2110
  // src/watch/watcher.ts
1597
2111
  import { readFile as readFile3 } from "fs/promises";
1598
- import { join as join13 } from "path";
2112
+ import { join as join15 } from "path";
1599
2113
  import chokidar from "chokidar";
1600
2114
 
1601
2115
  // src/discovery/contracts-fast.ts
1602
- import { join as join11, resolve as resolve3 } from "path";
2116
+ import { join as join13, resolve as resolve3 } from "path";
1603
2117
  import fg2 from "fast-glob";
1604
2118
  import {
1605
2119
  Node as Node8,
@@ -1781,7 +2295,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
1781
2295
  }
1782
2296
  return null;
1783
2297
  }
2298
+ function resolveImportedVariable(name, sourceFile, project) {
2299
+ const local = sourceFile.getVariableDeclaration(name);
2300
+ if (local) return { decl: local, file: sourceFile };
2301
+ return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
2302
+ }
2303
+ function resolveVariableViaImports(name, sourceFile, project, seen) {
2304
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2305
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
2306
+ if (!namedImport) continue;
2307
+ const sourceName = namedImport.getName();
2308
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
2309
+ const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
2310
+ if (found) return found;
2311
+ }
2312
+ return null;
2313
+ }
2314
+ function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
2315
+ const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
2316
+ for (const candidate of candidates) {
2317
+ let importedFile = project.getSourceFile(candidate);
2318
+ if (!importedFile) {
2319
+ try {
2320
+ importedFile = project.addSourceFileAtPath(candidate);
2321
+ } catch {
2322
+ continue;
2323
+ }
2324
+ }
2325
+ const found = resolveVariableInFile(name, importedFile, project, seen);
2326
+ if (found) return found;
2327
+ }
2328
+ return null;
2329
+ }
2330
+ function resolveVariableInFile(name, file, project, seen) {
2331
+ const filePath = file.getFilePath();
2332
+ if (seen.has(filePath)) return null;
2333
+ seen.add(filePath);
2334
+ const local = file.getVariableDeclaration(name);
2335
+ if (local) return { decl: local, file };
2336
+ for (const exportDecl of file.getExportDeclarations()) {
2337
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue();
2338
+ const namedExports = exportDecl.getNamedExports();
2339
+ if (moduleSpecifier) {
2340
+ const hasStar = namedExports.length === 0;
2341
+ const reExport2 = namedExports.find(
2342
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2343
+ );
2344
+ if (!hasStar && !reExport2) continue;
2345
+ const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
2346
+ const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
2347
+ if (found) return found;
2348
+ continue;
2349
+ }
2350
+ const reExport = namedExports.find(
2351
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
2352
+ );
2353
+ if (!reExport) continue;
2354
+ const sourceName = reExport.getName();
2355
+ const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
2356
+ if (viaImports) return viaImports;
2357
+ }
2358
+ return null;
2359
+ }
1784
2360
  var _findTypeCache = /* @__PURE__ */ new WeakMap();
2361
+ function clearTypeResolutionCaches(project) {
2362
+ _findTypeCache.delete(project);
2363
+ _resolveNamedRefCache.delete(project);
2364
+ }
1785
2365
  function findType(name, sourceFile, project) {
1786
2366
  let byKey = _findTypeCache.get(project);
1787
2367
  if (byKey === void 0) {
@@ -1924,7 +2504,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
1924
2504
  emittedClasses: /* @__PURE__ */ new Map(),
1925
2505
  visiting: /* @__PURE__ */ new Set(),
1926
2506
  recursiveSchemas: /* @__PURE__ */ new Set(),
1927
- depth: 0
2507
+ depth: 0,
2508
+ typeBindings: /* @__PURE__ */ new Map()
1928
2509
  };
1929
2510
  const root = buildObject(classDecl, sourceFile, ctx);
1930
2511
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -1948,11 +2529,34 @@ function buildProperty(prop, classFile, ctx) {
1948
2529
  const typeNode = prop.getTypeNode();
1949
2530
  const typeText = typeNode?.getText() ?? "unknown";
1950
2531
  const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
2532
+ const discriminator = resolveDiscriminator(dec("Type"));
2533
+ if (discriminator) {
2534
+ const options = discriminator.subTypes.map(
2535
+ (name) => buildNestedReference(name, classFile, ctx)
2536
+ );
2537
+ const unionNode = {
2538
+ kind: "union",
2539
+ options,
2540
+ discriminator: discriminator.property
2541
+ };
2542
+ const wrapArray = has("IsArray") || isArrayType;
2543
+ const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
2544
+ return applyPresence(node2, decorators);
2545
+ }
2546
+ const propTypeParam = singularClassName(typeText);
2547
+ if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
2548
+ const bound = ctx.typeBindings.get(propTypeParam);
2549
+ const childNode = buildNestedReference(bound, classFile, ctx);
2550
+ const wrapArray = has("IsArray") || isArrayType;
2551
+ const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
2552
+ return applyPresence(node2, decorators);
2553
+ }
1951
2554
  const typeRefName = resolveTypeFactoryName(dec("Type"));
1952
2555
  if (has("ValidateNested") || typeRefName) {
2556
+ const typeArgs = genericTypeArgNames(typeNode);
1953
2557
  const childName = typeRefName ?? singularClassName(typeText);
1954
2558
  if (childName) {
1955
- const childNode = buildNestedReference(childName, classFile, ctx);
2559
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
1956
2560
  const wrapArray = has("IsArray") || isArrayType;
1957
2561
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
1958
2562
  return applyPresence(node2, decorators);
@@ -2077,10 +2681,12 @@ function baseFromType(typeText, isArrayType) {
2077
2681
  return { kind: "unknown" };
2078
2682
  }
2079
2683
  }
2080
- function buildNestedReference(className, fromFile, ctx) {
2081
- if (ctx.visiting.has(className)) {
2082
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2083
- ctx.emittedClasses.set(className, reserved);
2684
+ function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
2685
+ const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
2686
+ const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
2687
+ if (ctx.visiting.has(cacheKey)) {
2688
+ const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
2689
+ ctx.emittedClasses.set(cacheKey, reserved);
2084
2690
  ctx.recursiveSchemas.add(reserved);
2085
2691
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2086
2692
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -2099,19 +2705,27 @@ function buildNestedReference(className, fromFile, ctx) {
2099
2705
  }
2100
2706
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2101
2707
  }
2102
- const existing = ctx.emittedClasses.get(className);
2708
+ const existing = ctx.emittedClasses.get(cacheKey);
2103
2709
  if (existing) return { kind: "ref", name: existing };
2104
- const schemaName = aliasFor(className, ctx);
2710
+ const schemaName = aliasFor(schemaBase, ctx);
2105
2711
  const resolved = findType(className, fromFile, ctx.project);
2106
2712
  if (!resolved || resolved.kind !== "class") {
2107
2713
  return { kind: "object", fields: [], passthrough: true };
2108
2714
  }
2109
- ctx.emittedClasses.set(className, schemaName);
2110
- ctx.visiting.add(className);
2715
+ const params = resolved.decl.getTypeParameters().map((p) => p.getName());
2716
+ const newBindings = [];
2717
+ params.forEach((param, i) => {
2718
+ const arg = typeArgs[i];
2719
+ if (arg) newBindings.push([param, arg]);
2720
+ });
2721
+ for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
2722
+ ctx.emittedClasses.set(cacheKey, schemaName);
2723
+ ctx.visiting.add(cacheKey);
2111
2724
  ctx.depth += 1;
2112
2725
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
2113
2726
  ctx.depth -= 1;
2114
- ctx.visiting.delete(className);
2727
+ ctx.visiting.delete(cacheKey);
2728
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
2115
2729
  ctx.named.set(schemaName, childNode);
2116
2730
  return { kind: "ref", name: schemaName };
2117
2731
  }
@@ -2158,6 +2772,39 @@ function messageRaw(decorator) {
2158
2772
  }
2159
2773
  return void 0;
2160
2774
  }
2775
+ function resolveDiscriminator(decorator) {
2776
+ const optsArg = decorator?.getArguments()[1];
2777
+ if (!optsArg || !Node3.isObjectLiteralExpression(optsArg)) return null;
2778
+ let discProp;
2779
+ for (const prop of optsArg.getProperties()) {
2780
+ if (Node3.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
2781
+ discProp = prop.getInitializer();
2782
+ }
2783
+ }
2784
+ if (!discProp || !Node3.isObjectLiteralExpression(discProp)) return null;
2785
+ let property = null;
2786
+ const subTypes = [];
2787
+ for (const prop of discProp.getProperties()) {
2788
+ if (!Node3.isPropertyAssignment(prop)) continue;
2789
+ const name = prop.getName();
2790
+ const init = prop.getInitializer();
2791
+ if (!init) continue;
2792
+ if (name === "property" && Node3.isStringLiteral(init)) {
2793
+ property = init.getLiteralValue();
2794
+ } else if (name === "subTypes" && Node3.isArrayLiteralExpression(init)) {
2795
+ for (const el of init.getElements()) {
2796
+ if (!Node3.isObjectLiteralExpression(el)) continue;
2797
+ for (const p of el.getProperties()) {
2798
+ if (!Node3.isPropertyAssignment(p) || p.getName() !== "name") continue;
2799
+ const nameInit = p.getInitializer();
2800
+ if (nameInit && Node3.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
2801
+ }
2802
+ }
2803
+ }
2804
+ }
2805
+ if (!property || subTypes.length === 0) return null;
2806
+ return { property, subTypes };
2807
+ }
2161
2808
  function resolveTypeFactoryName(decorator) {
2162
2809
  const arg = firstArg(decorator);
2163
2810
  if (!arg) return null;
@@ -2171,6 +2818,17 @@ function singularClassName(typeText) {
2171
2818
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2172
2819
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2173
2820
  }
2821
+ function genericTypeArgNames(typeNode) {
2822
+ if (!typeNode || !Node3.isTypeReference(typeNode)) return [];
2823
+ const names = [];
2824
+ for (const arg of typeNode.getTypeArguments()) {
2825
+ if (!Node3.isTypeReference(arg)) return [];
2826
+ const tn = arg.getTypeName();
2827
+ if (!Node3.isIdentifier(tn)) return [];
2828
+ names.push(tn.getText());
2829
+ }
2830
+ return names;
2831
+ }
2174
2832
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2175
2833
  const arg = firstArg(decorator);
2176
2834
  if (!arg) return null;
@@ -2230,6 +2888,9 @@ import {
2230
2888
 
2231
2889
  // src/discovery/enum-resolution.ts
2232
2890
  var _enumCache = /* @__PURE__ */ new WeakMap();
2891
+ function clearEnumCache(project) {
2892
+ _enumCache.delete(project);
2893
+ }
2233
2894
  function resolveEnumValues(name, sourceFile, project) {
2234
2895
  let byKey = _enumCache.get(project);
2235
2896
  if (byKey === void 0) {
@@ -2698,24 +3359,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2698
3359
  "Map",
2699
3360
  "Set"
2700
3361
  ]);
2701
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3362
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2702
3363
  if (depth <= 0) return "unknown";
2703
3364
  if (Node6.isArrayTypeNode(typeNode)) {
2704
3365
  const elementType = typeNode.getElementTypeNode();
2705
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3366
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
2706
3367
  }
2707
3368
  if (Node6.isUnionTypeNode(typeNode)) {
2708
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3369
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
2709
3370
  }
2710
3371
  if (Node6.isIntersectionTypeNode(typeNode)) {
2711
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3372
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
2712
3373
  }
2713
3374
  if (Node6.isParenthesizedTypeNode(typeNode)) {
2714
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3375
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
2715
3376
  }
2716
3377
  if (Node6.isTypeReference(typeNode)) {
2717
3378
  const typeName = typeNode.getTypeName();
2718
3379
  const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3380
+ const bound = subst.get(name);
3381
+ if (bound !== void 0) return bound;
2719
3382
  if (name === "string" || name === "number" || name === "boolean") return name;
2720
3383
  if (name === "Date") return "string";
2721
3384
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -2723,14 +3386,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2723
3386
  return "unknown";
2724
3387
  const wrapperMode = WRAPPER_TYPES[name];
2725
3388
  if (wrapperMode) {
2726
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3389
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
2727
3390
  }
2728
3391
  if (PASSTHROUGH_UTILITY.has(name)) {
2729
3392
  return typeNode.getText();
2730
3393
  }
2731
3394
  const resolved = findType(name, sourceFile, project);
2732
3395
  if (resolved) {
2733
- return expandTypeDecl(resolved, project, depth - 1);
3396
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
3397
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
2734
3398
  }
2735
3399
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
2736
3400
  return "unknown";
@@ -2743,32 +3407,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2743
3407
  if (kind === SyntaxKind3.AnyKeyword) return "unknown";
2744
3408
  return typeNode.getText();
2745
3409
  }
2746
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
3410
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
2747
3411
  const typeArgs = typeNode.getTypeArguments();
2748
3412
  const firstTypeArg = typeArgs[0];
2749
3413
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2750
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3414
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
2751
3415
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
2752
3416
  }
2753
3417
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2754
3418
  }
2755
- function expandTypeDecl(result, project, depth) {
3419
+ function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
3420
+ if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
3421
+ const params = result.decl.getTypeParameters().map((p) => p.getName());
3422
+ if (params.length === 0) return /* @__PURE__ */ new Map();
3423
+ const args = typeNode.getTypeArguments();
3424
+ const subst = /* @__PURE__ */ new Map();
3425
+ params.forEach((param, i) => {
3426
+ const arg = args[i];
3427
+ if (arg)
3428
+ subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
3429
+ });
3430
+ return subst;
3431
+ }
3432
+ function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
2756
3433
  if (depth < 0) return "unknown";
2757
3434
  switch (result.kind) {
2758
3435
  case "class":
2759
- return resolvePropertied(result.decl, result.file, project, depth);
3436
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2760
3437
  case "interface":
2761
- return resolvePropertied(result.decl, result.file, project, depth);
3438
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
2762
3439
  case "typeAlias":
2763
3440
  if (result.typeNode) {
2764
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
3441
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
2765
3442
  }
2766
3443
  return result.text;
2767
3444
  case "enum":
2768
3445
  return result.members.join(" | ");
2769
3446
  }
2770
3447
  }
2771
- function resolvePropertied(decl, sourceFile, project, depth) {
3448
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2772
3449
  if (depth < 0) return "unknown";
2773
3450
  const lines = [];
2774
3451
  for (const prop of decl.getProperties()) {
@@ -2777,7 +3454,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
2777
3454
  const propTypeNode = prop.getTypeNode();
2778
3455
  let propType = "unknown";
2779
3456
  if (propTypeNode) {
2780
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
3457
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2781
3458
  }
2782
3459
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
2783
3460
  }
@@ -2826,7 +3503,7 @@ function extractParamsType(method, sourceFile, project) {
2826
3503
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
2827
3504
  }
2828
3505
  function extractResponseType(method, sourceFile, project) {
2829
- const apiResponseDecorator = method.getDecorator("ApiResponse");
3506
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2830
3507
  if (apiResponseDecorator) {
2831
3508
  const args = apiResponseDecorator.getArguments();
2832
3509
  const optsArg = args[0];
@@ -2855,6 +3532,59 @@ function extractResponseType(method, sourceFile, project) {
2855
3532
  }
2856
3533
  return "unknown";
2857
3534
  }
3535
+ function apiResponseStatus(decorator) {
3536
+ const optsArg = decorator.getArguments()[0];
3537
+ if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
3538
+ for (const prop of optsArg.getProperties()) {
3539
+ if (!Node6.isPropertyAssignment(prop)) continue;
3540
+ if (prop.getName() !== "status") continue;
3541
+ const val = prop.getInitializer();
3542
+ if (val && Node6.isNumericLiteral(val)) return Number(val.getLiteralValue());
3543
+ }
3544
+ return null;
3545
+ }
3546
+ function apiResponseTypeNode(decorator) {
3547
+ const optsArg = decorator.getArguments()[0];
3548
+ if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
3549
+ for (const prop of optsArg.getProperties()) {
3550
+ if (!Node6.isPropertyAssignment(prop)) continue;
3551
+ if (prop.getName() !== "type") continue;
3552
+ const val = prop.getInitializer();
3553
+ if (!val) return null;
3554
+ if (Node6.isArrayLiteralExpression(val)) {
3555
+ const first = val.getElements()[0];
3556
+ return first ? { node: first, isArray: true } : null;
3557
+ }
3558
+ return { node: val, isArray: false };
3559
+ }
3560
+ return null;
3561
+ }
3562
+ function extractErrorType(method, sourceFile, project) {
3563
+ for (const decorator of method.getDecorators()) {
3564
+ if (decorator.getName() !== "ApiResponse") continue;
3565
+ const status = apiResponseStatus(decorator);
3566
+ if (status === null || status < 400) continue;
3567
+ const typeInfo = apiResponseTypeNode(decorator);
3568
+ if (!typeInfo) continue;
3569
+ const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
3570
+ const type = typeInfo.isArray ? `Array<${inner}>` : inner;
3571
+ let ref = null;
3572
+ if (Node6.isIdentifier(typeInfo.node)) {
3573
+ const name = typeInfo.node.getText();
3574
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3575
+ if (localDecl?.isExported()) {
3576
+ ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
3577
+ } else {
3578
+ const resolved = resolveImportedType(name, sourceFile, project);
3579
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
3580
+ ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
3581
+ }
3582
+ }
3583
+ }
3584
+ return { type, ref };
3585
+ }
3586
+ return null;
3587
+ }
2858
3588
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
2859
3589
  if (!Node6.isIdentifier(node)) return "unknown";
2860
3590
  const name = node.getText();
@@ -2870,17 +3600,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
2870
3600
  unwrapContainers: true
2871
3601
  });
2872
3602
  }
3603
+ var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
3604
+ var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
3605
+ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
3606
+ function detectStreamElement(method) {
3607
+ const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
3608
+ let node = method.getReturnTypeNode();
3609
+ node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
3610
+ const containerEl = streamContainerElement(node);
3611
+ if (containerEl) {
3612
+ return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
3613
+ }
3614
+ if (hasSse) return node ?? null;
3615
+ return null;
3616
+ }
3617
+ function streamContainerElement(node) {
3618
+ if (!node || !Node6.isTypeReference(node)) return null;
3619
+ const typeName = node.getTypeName();
3620
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
3621
+ if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
3622
+ return node.getTypeArguments()[0] ?? null;
3623
+ }
3624
+ return null;
3625
+ }
3626
+ function unwrapNamedContainer(node, names) {
3627
+ if (!node || !Node6.isTypeReference(node)) return node;
3628
+ const typeName = node.getTypeName();
3629
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
3630
+ if (names.has(name)) {
3631
+ return node.getTypeArguments()[0] ?? node;
3632
+ }
3633
+ return node;
3634
+ }
2873
3635
  function extractDtoContract(method, sourceFile, project) {
2874
3636
  let body = extractBodyType(method, sourceFile, project);
2875
3637
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
2876
3638
  const query = extractQueryType(method, sourceFile, project);
3639
+ const streamElement = detectStreamElement(method);
3640
+ const isStream = streamElement !== null;
2877
3641
  if (filterInfo && filterInfo.source === "body") {
2878
3642
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
2879
3643
  body = body ?? bodyType;
2880
3644
  }
2881
3645
  const paramsType = extractParamsType(method, sourceFile, project);
2882
- const response = extractResponseType(method, sourceFile, project);
2883
- if (body === null && query === null && paramsType === null && response === "unknown" && filterInfo === null) {
3646
+ const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3647
+ const errorInfo = extractErrorType(method, sourceFile, project);
3648
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
2884
3649
  return null;
2885
3650
  }
2886
3651
  let bodyRef = null;
@@ -2894,12 +3659,12 @@ function extractDtoContract(method, sourceFile, project) {
2894
3659
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2895
3660
  }
2896
3661
  }
2897
- const returnTypeNode = method.getReturnTypeNode();
3662
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
2898
3663
  if (returnTypeNode) {
2899
3664
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
2900
3665
  }
2901
- if (!responseRef) {
2902
- const apiResp = method.getDecorator("ApiResponse");
3666
+ if (!responseRef && !isStream) {
3667
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
2903
3668
  if (apiResp) {
2904
3669
  const args = apiResp.getArguments();
2905
3670
  const optsArg = args[0];
@@ -2941,16 +3706,19 @@ function extractDtoContract(method, sourceFile, project) {
2941
3706
  query,
2942
3707
  body,
2943
3708
  response,
3709
+ error: errorInfo?.type ?? null,
2944
3710
  params: paramsType,
2945
3711
  queryRef,
2946
3712
  bodyRef,
2947
3713
  responseRef,
3714
+ errorRef: errorInfo?.ref ?? null,
2948
3715
  filterFields: filterInfo?.fieldNames ?? null,
2949
3716
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
2950
3717
  filterSource: filterInfo?.source ?? null,
2951
3718
  formWarnings,
2952
3719
  bodySchema,
2953
- querySchema
3720
+ querySchema,
3721
+ stream: isStream
2954
3722
  };
2955
3723
  }
2956
3724
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -3068,6 +3836,7 @@ function parseDefineContractCall(callExpr) {
3068
3836
  let query = null;
3069
3837
  let body = null;
3070
3838
  let response = "unknown";
3839
+ let error = null;
3071
3840
  let bodyZodText = null;
3072
3841
  let queryZodText = null;
3073
3842
  for (const prop of optsArg.getProperties()) {
@@ -3083,25 +3852,38 @@ function parseDefineContractCall(callExpr) {
3083
3852
  bodyZodText = val.getText();
3084
3853
  } else if (propName === "response") {
3085
3854
  response = zodAstToTs(val);
3855
+ } else if (propName === "error") {
3856
+ error = zodAstToTs(val);
3086
3857
  }
3087
3858
  }
3088
- return { query, body, response, bodyZodText, queryZodText };
3859
+ return { query, body, response, error, bodyZodText, queryZodText };
3089
3860
  }
3090
3861
 
3091
3862
  // src/discovery/contracts-fast.ts
3092
3863
  async function discoverContractsFast(opts) {
3093
3864
  const { cwd, glob, tsconfig } = opts;
3094
- const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
3095
- let project;
3865
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3866
+ const project = createDiscoveryProject(tsconfigPath);
3867
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3868
+ for (const f of files) {
3869
+ project.addSourceFileAtPath(f);
3870
+ }
3871
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3872
+ return extractAllRoutes(project);
3873
+ }
3874
+ function resolveTsconfigPath(cwd, tsconfig) {
3875
+ return tsconfig ? resolve3(tsconfig) : join13(cwd, "tsconfig.json");
3876
+ }
3877
+ function createDiscoveryProject(tsconfigPath) {
3096
3878
  try {
3097
- project = new Project3({
3879
+ return new Project3({
3098
3880
  tsConfigFilePath: tsconfigPath,
3099
3881
  skipAddingFilesFromTsConfig: true,
3100
3882
  skipLoadingLibFiles: true,
3101
3883
  skipFileDependencyResolution: true
3102
3884
  });
3103
3885
  } catch {
3104
- project = new Project3({
3886
+ return new Project3({
3105
3887
  skipAddingFilesFromTsConfig: true,
3106
3888
  skipLoadingLibFiles: true,
3107
3889
  skipFileDependencyResolution: true,
@@ -3112,20 +3894,105 @@ async function discoverContractsFast(opts) {
3112
3894
  }
3113
3895
  });
3114
3896
  }
3115
- const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3116
- for (const f of files) {
3117
- project.addSourceFileAtPath(f);
3118
- }
3119
- const routes = [];
3897
+ }
3898
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
3120
3899
  setDiscoveryContext(project, {
3121
3900
  projectRoot: cwd,
3122
3901
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3123
3902
  });
3903
+ }
3904
+ function extractRoutesFrom(project, controllerPaths) {
3905
+ const routes = [];
3906
+ for (const path of controllerPaths) {
3907
+ const sourceFile = project.getSourceFile(path);
3908
+ if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
3909
+ }
3910
+ return routes;
3911
+ }
3912
+ function extractAllRoutes(project) {
3913
+ const routes = [];
3124
3914
  for (const sourceFile of project.getSourceFiles()) {
3125
3915
  routes.push(...extractFromSourceFile(sourceFile, project));
3126
3916
  }
3127
3917
  return routes;
3128
3918
  }
3919
+ var PersistentDiscovery = class _PersistentDiscovery {
3920
+ project;
3921
+ cwd;
3922
+ glob;
3923
+ /** Absolute paths of the controllers currently loaded as extraction roots. */
3924
+ controllerPaths = /* @__PURE__ */ new Set();
3925
+ constructor(project, cwd, glob) {
3926
+ this.project = project;
3927
+ this.cwd = cwd;
3928
+ this.glob = glob;
3929
+ }
3930
+ /**
3931
+ * Build the initial persistent Project: create it, glob + add all controllers,
3932
+ * bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
3933
+ */
3934
+ static async create(opts) {
3935
+ const { cwd, glob, tsconfig } = opts;
3936
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
3937
+ const project = createDiscoveryProject(tsconfigPath);
3938
+ bindDiscoveryContext(project, cwd, tsconfigPath);
3939
+ const instance = new _PersistentDiscovery(project, cwd, glob);
3940
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3941
+ for (const f of files) {
3942
+ project.addSourceFileAtPath(f);
3943
+ instance.controllerPaths.add(f);
3944
+ }
3945
+ return instance;
3946
+ }
3947
+ /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
3948
+ discover() {
3949
+ return this.runExtraction();
3950
+ }
3951
+ /**
3952
+ * Re-discover after one or more files changed. Refreshes the changed file(s)
3953
+ * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
3954
+ * to pick up added/removed controllers, clears the per-Project caches, then
3955
+ * re-extracts. `changedPaths` is a hint; correctness does not depend on it
3956
+ * being exhaustive because re-globbing + refresh-on-presence covers the set.
3957
+ */
3958
+ async rediscover(changedPaths) {
3959
+ if (changedPaths) {
3960
+ for (const p of changedPaths) {
3961
+ const abs = resolve3(p);
3962
+ const sf = this.project.getSourceFile(abs);
3963
+ if (sf) {
3964
+ await sf.refreshFromFileSystem();
3965
+ }
3966
+ }
3967
+ }
3968
+ const globbed = new Set(
3969
+ await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
3970
+ );
3971
+ for (const f of globbed) {
3972
+ if (!this.controllerPaths.has(f)) {
3973
+ try {
3974
+ this.project.addSourceFileAtPath(f);
3975
+ this.controllerPaths.add(f);
3976
+ } catch {
3977
+ }
3978
+ }
3979
+ }
3980
+ for (const f of this.controllerPaths) {
3981
+ if (!globbed.has(f)) {
3982
+ const sf = this.project.getSourceFile(f);
3983
+ if (sf) this.project.removeSourceFile(sf);
3984
+ this.controllerPaths.delete(f);
3985
+ }
3986
+ }
3987
+ return this.runExtraction();
3988
+ }
3989
+ /** Clear stale per-Project caches, then extract over the controller set. */
3990
+ runExtraction() {
3991
+ clearTypeResolutionCaches(this.project);
3992
+ clearEnumCache(this.project);
3993
+ return extractRoutesFrom(this.project, this.controllerPaths);
3994
+ }
3995
+ };
3129
3996
  function decoratorStringArg(decoratorExpr) {
3130
3997
  if (!decoratorExpr) return void 0;
3131
3998
  if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -3181,6 +4048,11 @@ function resolveVerb(method) {
3181
4048
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3182
4049
  }
3183
4050
  }
4051
+ const sseDecorator = method.getDecorator("Sse");
4052
+ if (sseDecorator) {
4053
+ const pathArg = sseDecorator.getArguments()[0];
4054
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
4055
+ }
3184
4056
  return null;
3185
4057
  }
3186
4058
  function readAsDecorator(node, label) {
@@ -3223,7 +4095,17 @@ function buildRoute(args) {
3223
4095
  };
3224
4096
  }
3225
4097
  function extractContractRoute(args) {
3226
- const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
4098
+ const {
4099
+ cls,
4100
+ method,
4101
+ applyContractDecorator,
4102
+ verb,
4103
+ prefix,
4104
+ className,
4105
+ sourceFile,
4106
+ project,
4107
+ seenNames
4108
+ } = args;
3227
4109
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3228
4110
  if (!firstDecoratorArg) return null;
3229
4111
  let contractDef = null;
@@ -3233,18 +4115,19 @@ function extractContractRoute(args) {
3233
4115
  contractDef = parseDefineContractCall(firstDecoratorArg);
3234
4116
  } else if (Node8.isIdentifier(firstDecoratorArg)) {
3235
4117
  const identName = firstDecoratorArg.getText();
3236
- const varDecl = sourceFile.getVariableDeclaration(identName);
3237
- if (!varDecl) {
4118
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4119
+ if (!resolvedVar) {
3238
4120
  console.warn(
3239
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
4121
+ `[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
3240
4122
  );
3241
4123
  return null;
3242
4124
  }
4125
+ const { decl: varDecl, file: declFile } = resolvedVar;
3243
4126
  const initializer = varDecl.getInitializer();
3244
4127
  if (!initializer) return null;
3245
4128
  contractDef = parseDefineContractCall(initializer);
3246
4129
  if (contractDef && varDecl.isExported()) {
3247
- const filePath = sourceFile.getFilePath();
4130
+ const filePath = declFile.getFilePath();
3248
4131
  if (contractDef.body !== null) {
3249
4132
  bodyZodRef = { name: `${identName}.body`, filePath };
3250
4133
  }
@@ -3277,6 +4160,7 @@ function extractContractRoute(args) {
3277
4160
  query: contractDef.query,
3278
4161
  body: contractDef.body,
3279
4162
  response: contractDef.response,
4163
+ error: contractDef.error,
3280
4164
  // Path A: capture both the importable ref and the raw text. The emitter
3281
4165
  // prefers inlining the text (client-safe — re-exporting from a controller
3282
4166
  // would drag server-only deps into the client bundle).
@@ -3308,15 +4192,18 @@ function extractDtoRoute(args) {
3308
4192
  query: dtoContract?.query ?? null,
3309
4193
  body: dtoContract?.body ?? null,
3310
4194
  response: dtoContract?.response ?? "unknown",
4195
+ error: dtoContract?.error ?? null,
3311
4196
  queryRef: dtoContract?.queryRef ?? null,
3312
4197
  bodyRef: dtoContract?.bodyRef ?? null,
3313
4198
  responseRef: dtoContract?.responseRef ?? null,
4199
+ errorRef: dtoContract?.errorRef ?? null,
3314
4200
  filterFields: dtoContract?.filterFields ?? null,
3315
4201
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3316
4202
  filterSource: dtoContract?.filterSource ?? null,
3317
4203
  formWarnings: dtoContract?.formWarnings ?? [],
3318
4204
  bodySchema: dtoContract?.bodySchema ?? null,
3319
- querySchema: dtoContract?.querySchema ?? null
4205
+ querySchema: dtoContract?.querySchema ?? null,
4206
+ stream: dtoContract?.stream ?? false
3320
4207
  }
3321
4208
  });
3322
4209
  }
@@ -3340,6 +4227,7 @@ function extractFromSourceFile(sourceFile, project) {
3340
4227
  prefix,
3341
4228
  className,
3342
4229
  sourceFile,
4230
+ project,
3343
4231
  seenNames
3344
4232
  }) : extractDtoRoute({
3345
4233
  cls,
@@ -3359,8 +4247,8 @@ function extractFromSourceFile(sourceFile, project) {
3359
4247
 
3360
4248
  // src/watch/lock-file.ts
3361
4249
  import { open } from "fs/promises";
3362
- import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3363
- import { join as join12 } from "path";
4250
+ import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
4251
+ import { join as join14 } from "path";
3364
4252
  var LOCK_FILE = ".watcher.lock";
3365
4253
  function isProcessAlive(pid) {
3366
4254
  try {
@@ -3371,8 +4259,8 @@ function isProcessAlive(pid) {
3371
4259
  }
3372
4260
  }
3373
4261
  async function acquireLock(outDir) {
3374
- await mkdir8(outDir, { recursive: true });
3375
- const lockPath = join12(outDir, LOCK_FILE);
4262
+ await mkdir10(outDir, { recursive: true });
4263
+ const lockPath = join14(outDir, LOCK_FILE);
3376
4264
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3377
4265
  try {
3378
4266
  const fd = await open(lockPath, "wx");
@@ -3412,7 +4300,7 @@ async function watch(config, onChange) {
3412
4300
  if (lock === null) {
3413
4301
  let holderPid = "unknown";
3414
4302
  try {
3415
- const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
4303
+ const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
3416
4304
  const data = JSON.parse(raw);
3417
4305
  if (data.pid !== void 0) holderPid = String(data.pid);
3418
4306
  } catch {
@@ -3422,12 +4310,20 @@ async function watch(config, onChange) {
3422
4310
  );
3423
4311
  return NO_OP_WATCHER;
3424
4312
  }
4313
+ let discovery = null;
4314
+ async function getDiscovery() {
4315
+ if (discovery === null) {
4316
+ discovery = await PersistentDiscovery.create({
4317
+ cwd: config.codegen.cwd,
4318
+ glob: config.contracts.glob,
4319
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4320
+ });
4321
+ return discovery;
4322
+ }
4323
+ return discovery;
4324
+ }
3425
4325
  try {
3426
- const initialRoutes = await discoverContractsFast({
3427
- cwd: config.codegen.cwd,
3428
- glob: config.contracts.glob,
3429
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3430
- });
4326
+ const initialRoutes = (await getDiscovery()).discover();
3431
4327
  await generate(config, initialRoutes);
3432
4328
  } catch (err) {
3433
4329
  console.warn(
@@ -3440,7 +4336,7 @@ async function watch(config, onChange) {
3440
4336
  }
3441
4337
  let pagesDebounceTimer;
3442
4338
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3443
- const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
4339
+ const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
3444
4340
  ignoreInitial: true,
3445
4341
  persistent: true,
3446
4342
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3466,23 +4362,23 @@ async function watch(config, onChange) {
3466
4362
  pagesWatcher.on("change", schedulePagesRegenerate);
3467
4363
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3468
4364
  let contractsDebounceTimer;
3469
- const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
4365
+ const pendingChangedPaths = /* @__PURE__ */ new Set();
4366
+ const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
3470
4367
  ignoreInitial: true,
3471
4368
  persistent: true,
3472
4369
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3473
4370
  });
3474
- function scheduleContractsRegenerate() {
4371
+ function scheduleContractsRegenerate(changedPath) {
4372
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3475
4373
  if (contractsDebounceTimer !== void 0) {
3476
4374
  clearTimeout(contractsDebounceTimer);
3477
4375
  }
3478
4376
  contractsDebounceTimer = setTimeout(async () => {
3479
4377
  contractsDebounceTimer = void 0;
4378
+ const changed = [...pendingChangedPaths];
4379
+ pendingChangedPaths.clear();
3480
4380
  try {
3481
- const routes = await discoverContractsFast({
3482
- cwd: config.codegen.cwd,
3483
- glob: config.contracts.glob,
3484
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3485
- });
4381
+ const routes = await (await getDiscovery()).rediscover(changed);
3486
4382
  await generate(config, routes);
3487
4383
  } catch (err) {
3488
4384
  console.error(
@@ -3493,17 +4389,17 @@ async function watch(config, onChange) {
3493
4389
  onChange?.();
3494
4390
  }, config.contracts.debounceMs);
3495
4391
  }
3496
- contractsWatcher.on("add", scheduleContractsRegenerate);
3497
- contractsWatcher.on("change", scheduleContractsRegenerate);
3498
- contractsWatcher.on("unlink", scheduleContractsRegenerate);
3499
- const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
4392
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4393
+ contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4394
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4395
+ const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
3500
4396
  ignoreInitial: true,
3501
4397
  persistent: true,
3502
4398
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3503
4399
  });
3504
- formsWatcher.on("add", scheduleContractsRegenerate);
3505
- formsWatcher.on("change", scheduleContractsRegenerate);
3506
- formsWatcher.on("unlink", scheduleContractsRegenerate);
4400
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4401
+ formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4402
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
3507
4403
  return {
3508
4404
  close: async () => {
3509
4405
  if (pagesDebounceTimer !== void 0) {
@@ -3572,16 +4468,20 @@ function renderTsType(node, ctx) {
3572
4468
  }
3573
4469
 
3574
4470
  // src/index.ts
3575
- var VERSION = "0.4.1";
4471
+ var VERSION = "0.5.0";
3576
4472
  export {
3577
4473
  CodegenError,
3578
4474
  ConfigError,
3579
4475
  VERSION,
3580
4476
  acquireLock,
4477
+ buildMocksFile,
4478
+ buildOpenApiSpec,
3581
4479
  defineConfig,
3582
4480
  discoverContractsFast,
3583
4481
  emitApi,
3584
4482
  emitForms,
4483
+ emitMocks,
4484
+ emitOpenApi,
3585
4485
  emitRoutes,
3586
4486
  extractSchemaFromDto,
3587
4487
  generate,
@@ -3589,6 +4489,8 @@ export {
3589
4489
  renderTsType,
3590
4490
  resolveAdapter,
3591
4491
  resolveConfig,
4492
+ schemaModuleToJsonSchema,
4493
+ schemaNodeToJsonSchema,
3592
4494
  watch
3593
4495
  };
3594
4496
  //# sourceMappingURL=index.js.map