@dudousxd/nestjs-codegen 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -118,13 +118,26 @@ function applyDefaults(userConfig, cwd) {
118
118
  enabled: userConfig.forms?.enabled ?? true,
119
119
  watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
120
120
  zodImport: userConfig.forms?.zodImport ?? "zod"
121
+ },
122
+ openapi: {
123
+ enabled: userConfig.openapi?.enabled ?? false,
124
+ fileName: userConfig.openapi?.fileName ?? "openapi.json",
125
+ title: userConfig.openapi?.title ?? "NestJS API",
126
+ version: userConfig.openapi?.version ?? "1.0.0",
127
+ description: userConfig.openapi?.description ?? null
128
+ },
129
+ mocks: {
130
+ enabled: userConfig.mocks?.enabled ?? false,
131
+ fileName: userConfig.mocks?.fileName ?? "mocks.ts",
132
+ seed: userConfig.mocks?.seed ?? 1,
133
+ baseUrl: userConfig.mocks?.baseUrl ?? ""
121
134
  }
122
135
  };
123
136
  }
124
137
 
125
138
  // src/watch/watcher.ts
126
139
  import { readFile as readFile3 } from "fs/promises";
127
- import { join as join13 } from "path";
140
+ import { join as join15 } from "path";
128
141
  import chokidar from "chokidar";
129
142
 
130
143
  // src/discovery/contracts-fast.ts
@@ -310,7 +323,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
310
323
  }
311
324
  return null;
312
325
  }
326
+ function resolveImportedVariable(name, sourceFile, project) {
327
+ const local = sourceFile.getVariableDeclaration(name);
328
+ if (local) return { decl: local, file: sourceFile };
329
+ return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
330
+ }
331
+ function resolveVariableViaImports(name, sourceFile, project, seen) {
332
+ for (const importDecl of sourceFile.getImportDeclarations()) {
333
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
334
+ if (!namedImport) continue;
335
+ const sourceName = namedImport.getName();
336
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
337
+ const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
338
+ if (found) return found;
339
+ }
340
+ return null;
341
+ }
342
+ function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
343
+ const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
344
+ for (const candidate of candidates) {
345
+ let importedFile = project.getSourceFile(candidate);
346
+ if (!importedFile) {
347
+ try {
348
+ importedFile = project.addSourceFileAtPath(candidate);
349
+ } catch {
350
+ continue;
351
+ }
352
+ }
353
+ const found = resolveVariableInFile(name, importedFile, project, seen);
354
+ if (found) return found;
355
+ }
356
+ return null;
357
+ }
358
+ function resolveVariableInFile(name, file, project, seen) {
359
+ const filePath = file.getFilePath();
360
+ if (seen.has(filePath)) return null;
361
+ seen.add(filePath);
362
+ const local = file.getVariableDeclaration(name);
363
+ if (local) return { decl: local, file };
364
+ for (const exportDecl of file.getExportDeclarations()) {
365
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue();
366
+ const namedExports = exportDecl.getNamedExports();
367
+ if (moduleSpecifier) {
368
+ const hasStar = namedExports.length === 0;
369
+ const reExport2 = namedExports.find(
370
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
371
+ );
372
+ if (!hasStar && !reExport2) continue;
373
+ const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
374
+ const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
375
+ if (found) return found;
376
+ continue;
377
+ }
378
+ const reExport = namedExports.find(
379
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
380
+ );
381
+ if (!reExport) continue;
382
+ const sourceName = reExport.getName();
383
+ const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
384
+ if (viaImports) return viaImports;
385
+ }
386
+ return null;
387
+ }
313
388
  var _findTypeCache = /* @__PURE__ */ new WeakMap();
389
+ function clearTypeResolutionCaches(project) {
390
+ _findTypeCache.delete(project);
391
+ _resolveNamedRefCache.delete(project);
392
+ }
314
393
  function findType(name, sourceFile, project) {
315
394
  let byKey = _findTypeCache.get(project);
316
395
  if (byKey === void 0) {
@@ -451,9 +530,11 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
451
530
  warnings: [],
452
531
  warnedDecorators: /* @__PURE__ */ new Set(),
453
532
  emittedClasses: /* @__PURE__ */ new Map(),
533
+ usedSchemaNames: /* @__PURE__ */ new Set(),
454
534
  visiting: /* @__PURE__ */ new Set(),
455
535
  recursiveSchemas: /* @__PURE__ */ new Set(),
456
- depth: 0
536
+ depth: 0,
537
+ typeBindings: /* @__PURE__ */ new Map()
457
538
  };
458
539
  const root = buildObject(classDecl, sourceFile, ctx);
459
540
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -477,11 +558,34 @@ function buildProperty(prop, classFile, ctx) {
477
558
  const typeNode = prop.getTypeNode();
478
559
  const typeText = typeNode?.getText() ?? "unknown";
479
560
  const isArrayType = !!typeNode && Node2.isArrayTypeNode(typeNode);
561
+ const discriminator = resolveDiscriminator(dec("Type"));
562
+ if (discriminator) {
563
+ const options = discriminator.subTypes.map(
564
+ (name) => buildNestedReference(name, classFile, ctx)
565
+ );
566
+ const unionNode = {
567
+ kind: "union",
568
+ options,
569
+ discriminator: discriminator.property
570
+ };
571
+ const wrapArray = has("IsArray") || isArrayType;
572
+ const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
573
+ return applyPresence(node2, decorators);
574
+ }
575
+ const propTypeParam = singularClassName(typeText);
576
+ if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
577
+ const bound = ctx.typeBindings.get(propTypeParam);
578
+ const childNode = buildNestedReference(bound, classFile, ctx);
579
+ const wrapArray = has("IsArray") || isArrayType;
580
+ const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
581
+ return applyPresence(node2, decorators);
582
+ }
480
583
  const typeRefName = resolveTypeFactoryName(dec("Type"));
481
584
  if (has("ValidateNested") || typeRefName) {
585
+ const typeArgs = genericTypeArgNames(typeNode);
482
586
  const childName = typeRefName ?? singularClassName(typeText);
483
587
  if (childName) {
484
- const childNode = buildNestedReference(childName, classFile, ctx);
588
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
485
589
  const wrapArray = has("IsArray") || isArrayType;
486
590
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
487
591
  return applyPresence(node2, decorators);
@@ -606,10 +710,13 @@ function baseFromType(typeText, isArrayType) {
606
710
  return { kind: "unknown" };
607
711
  }
608
712
  }
609
- function buildNestedReference(className, fromFile, ctx) {
610
- if (ctx.visiting.has(className)) {
611
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
612
- ctx.emittedClasses.set(className, reserved);
713
+ function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
714
+ const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
715
+ const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
716
+ if (ctx.visiting.has(cacheKey)) {
717
+ const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
718
+ ctx.emittedClasses.set(cacheKey, reserved);
719
+ ctx.usedSchemaNames.add(reserved);
613
720
  ctx.recursiveSchemas.add(reserved);
614
721
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
615
722
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -628,29 +735,37 @@ function buildNestedReference(className, fromFile, ctx) {
628
735
  }
629
736
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
630
737
  }
631
- const existing = ctx.emittedClasses.get(className);
738
+ const existing = ctx.emittedClasses.get(cacheKey);
632
739
  if (existing) return { kind: "ref", name: existing };
633
- const schemaName = aliasFor(className, ctx);
740
+ const schemaName = aliasFor(schemaBase, ctx);
634
741
  const resolved = findType(className, fromFile, ctx.project);
635
742
  if (!resolved || resolved.kind !== "class") {
636
743
  return { kind: "object", fields: [], passthrough: true };
637
744
  }
638
- ctx.emittedClasses.set(className, schemaName);
639
- ctx.visiting.add(className);
745
+ const params = resolved.decl.getTypeParameters().map((p) => p.getName());
746
+ const newBindings = [];
747
+ params.forEach((param, i) => {
748
+ const arg = typeArgs[i];
749
+ if (arg) newBindings.push([param, arg]);
750
+ });
751
+ for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
752
+ ctx.emittedClasses.set(cacheKey, schemaName);
753
+ ctx.usedSchemaNames.add(schemaName);
754
+ ctx.visiting.add(cacheKey);
640
755
  ctx.depth += 1;
641
756
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
642
757
  ctx.depth -= 1;
643
- ctx.visiting.delete(className);
758
+ ctx.visiting.delete(cacheKey);
759
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
644
760
  ctx.named.set(schemaName, childNode);
761
+ ctx.usedSchemaNames.add(schemaName);
645
762
  return { kind: "ref", name: schemaName };
646
763
  }
647
764
  function aliasFor(className, ctx) {
648
765
  const baseName = `${className}Schema`;
649
766
  let candidate = baseName;
650
767
  let i = 1;
651
- const used = new Set(ctx.named.keys());
652
- for (const v of ctx.emittedClasses.values()) used.add(v);
653
- while (used.has(candidate)) {
768
+ while (ctx.usedSchemaNames.has(candidate)) {
654
769
  candidate = `${baseName}_${i}`;
655
770
  i += 1;
656
771
  }
@@ -687,6 +802,39 @@ function messageRaw(decorator) {
687
802
  }
688
803
  return void 0;
689
804
  }
805
+ function resolveDiscriminator(decorator) {
806
+ const optsArg = decorator?.getArguments()[1];
807
+ if (!optsArg || !Node2.isObjectLiteralExpression(optsArg)) return null;
808
+ let discProp;
809
+ for (const prop of optsArg.getProperties()) {
810
+ if (Node2.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
811
+ discProp = prop.getInitializer();
812
+ }
813
+ }
814
+ if (!discProp || !Node2.isObjectLiteralExpression(discProp)) return null;
815
+ let property = null;
816
+ const subTypes = [];
817
+ for (const prop of discProp.getProperties()) {
818
+ if (!Node2.isPropertyAssignment(prop)) continue;
819
+ const name = prop.getName();
820
+ const init = prop.getInitializer();
821
+ if (!init) continue;
822
+ if (name === "property" && Node2.isStringLiteral(init)) {
823
+ property = init.getLiteralValue();
824
+ } else if (name === "subTypes" && Node2.isArrayLiteralExpression(init)) {
825
+ for (const el of init.getElements()) {
826
+ if (!Node2.isObjectLiteralExpression(el)) continue;
827
+ for (const p of el.getProperties()) {
828
+ if (!Node2.isPropertyAssignment(p) || p.getName() !== "name") continue;
829
+ const nameInit = p.getInitializer();
830
+ if (nameInit && Node2.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
831
+ }
832
+ }
833
+ }
834
+ }
835
+ if (!property || subTypes.length === 0) return null;
836
+ return { property, subTypes };
837
+ }
690
838
  function resolveTypeFactoryName(decorator) {
691
839
  const arg = firstArg(decorator);
692
840
  if (!arg) return null;
@@ -700,6 +848,17 @@ function singularClassName(typeText) {
700
848
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
701
849
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
702
850
  }
851
+ function genericTypeArgNames(typeNode) {
852
+ if (!typeNode || !Node2.isTypeReference(typeNode)) return [];
853
+ const names = [];
854
+ for (const arg of typeNode.getTypeArguments()) {
855
+ if (!Node2.isTypeReference(arg)) return [];
856
+ const tn = arg.getTypeName();
857
+ if (!Node2.isIdentifier(tn)) return [];
858
+ names.push(tn.getText());
859
+ }
860
+ return names;
861
+ }
703
862
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
704
863
  const arg = firstArg(decorator);
705
864
  if (!arg) return null;
@@ -759,6 +918,9 @@ import {
759
918
 
760
919
  // src/discovery/enum-resolution.ts
761
920
  var _enumCache = /* @__PURE__ */ new WeakMap();
921
+ function clearEnumCache(project) {
922
+ _enumCache.delete(project);
923
+ }
762
924
  function resolveEnumValues(name, sourceFile, project) {
763
925
  let byKey = _enumCache.get(project);
764
926
  if (byKey === void 0) {
@@ -1227,24 +1389,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
1227
1389
  "Map",
1228
1390
  "Set"
1229
1391
  ]);
1230
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1392
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
1231
1393
  if (depth <= 0) return "unknown";
1232
1394
  if (Node5.isArrayTypeNode(typeNode)) {
1233
1395
  const elementType = typeNode.getElementTypeNode();
1234
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
1396
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
1235
1397
  }
1236
1398
  if (Node5.isUnionTypeNode(typeNode)) {
1237
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
1399
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
1238
1400
  }
1239
1401
  if (Node5.isIntersectionTypeNode(typeNode)) {
1240
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
1402
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
1241
1403
  }
1242
1404
  if (Node5.isParenthesizedTypeNode(typeNode)) {
1243
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
1405
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
1244
1406
  }
1245
1407
  if (Node5.isTypeReference(typeNode)) {
1246
1408
  const typeName = typeNode.getTypeName();
1247
1409
  const name = Node5.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
1410
+ const bound = subst.get(name);
1411
+ if (bound !== void 0) return bound;
1248
1412
  if (name === "string" || name === "number" || name === "boolean") return name;
1249
1413
  if (name === "Date") return "string";
1250
1414
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -1252,14 +1416,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1252
1416
  return "unknown";
1253
1417
  const wrapperMode = WRAPPER_TYPES[name];
1254
1418
  if (wrapperMode) {
1255
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
1419
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
1256
1420
  }
1257
1421
  if (PASSTHROUGH_UTILITY.has(name)) {
1258
1422
  return typeNode.getText();
1259
1423
  }
1260
1424
  const resolved = findType(name, sourceFile, project);
1261
1425
  if (resolved) {
1262
- return expandTypeDecl(resolved, project, depth - 1);
1426
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
1427
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
1263
1428
  }
1264
1429
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1265
1430
  return "unknown";
@@ -1272,32 +1437,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1272
1437
  if (kind === SyntaxKind2.AnyKeyword) return "unknown";
1273
1438
  return typeNode.getText();
1274
1439
  }
1275
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
1440
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
1276
1441
  const typeArgs = typeNode.getTypeArguments();
1277
1442
  const firstTypeArg = typeArgs[0];
1278
1443
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1279
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
1444
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
1280
1445
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
1281
1446
  }
1282
1447
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
1283
1448
  }
1284
- function expandTypeDecl(result, project, depth) {
1449
+ function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
1450
+ if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
1451
+ const params = result.decl.getTypeParameters().map((p) => p.getName());
1452
+ if (params.length === 0) return /* @__PURE__ */ new Map();
1453
+ const args = typeNode.getTypeArguments();
1454
+ const subst = /* @__PURE__ */ new Map();
1455
+ params.forEach((param, i) => {
1456
+ const arg = args[i];
1457
+ if (arg)
1458
+ subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
1459
+ });
1460
+ return subst;
1461
+ }
1462
+ function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
1285
1463
  if (depth < 0) return "unknown";
1286
1464
  switch (result.kind) {
1287
1465
  case "class":
1288
- return resolvePropertied(result.decl, result.file, project, depth);
1466
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
1289
1467
  case "interface":
1290
- return resolvePropertied(result.decl, result.file, project, depth);
1468
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
1291
1469
  case "typeAlias":
1292
1470
  if (result.typeNode) {
1293
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
1471
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
1294
1472
  }
1295
1473
  return result.text;
1296
1474
  case "enum":
1297
1475
  return result.members.join(" | ");
1298
1476
  }
1299
1477
  }
1300
- function resolvePropertied(decl, sourceFile, project, depth) {
1478
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
1301
1479
  if (depth < 0) return "unknown";
1302
1480
  const lines = [];
1303
1481
  for (const prop of decl.getProperties()) {
@@ -1306,7 +1484,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
1306
1484
  const propTypeNode = prop.getTypeNode();
1307
1485
  let propType = "unknown";
1308
1486
  if (propTypeNode) {
1309
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
1487
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
1310
1488
  }
1311
1489
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
1312
1490
  }
@@ -1355,7 +1533,7 @@ function extractParamsType(method, sourceFile, project) {
1355
1533
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
1356
1534
  }
1357
1535
  function extractResponseType(method, sourceFile, project) {
1358
- const apiResponseDecorator = method.getDecorator("ApiResponse");
1536
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
1359
1537
  if (apiResponseDecorator) {
1360
1538
  const args = apiResponseDecorator.getArguments();
1361
1539
  const optsArg = args[0];
@@ -1384,6 +1562,59 @@ function extractResponseType(method, sourceFile, project) {
1384
1562
  }
1385
1563
  return "unknown";
1386
1564
  }
1565
+ function apiResponseStatus(decorator) {
1566
+ const optsArg = decorator.getArguments()[0];
1567
+ if (!optsArg || !Node5.isObjectLiteralExpression(optsArg)) return null;
1568
+ for (const prop of optsArg.getProperties()) {
1569
+ if (!Node5.isPropertyAssignment(prop)) continue;
1570
+ if (prop.getName() !== "status") continue;
1571
+ const val = prop.getInitializer();
1572
+ if (val && Node5.isNumericLiteral(val)) return Number(val.getLiteralValue());
1573
+ }
1574
+ return null;
1575
+ }
1576
+ function apiResponseTypeNode(decorator) {
1577
+ const optsArg = decorator.getArguments()[0];
1578
+ if (!optsArg || !Node5.isObjectLiteralExpression(optsArg)) return null;
1579
+ for (const prop of optsArg.getProperties()) {
1580
+ if (!Node5.isPropertyAssignment(prop)) continue;
1581
+ if (prop.getName() !== "type") continue;
1582
+ const val = prop.getInitializer();
1583
+ if (!val) return null;
1584
+ if (Node5.isArrayLiteralExpression(val)) {
1585
+ const first = val.getElements()[0];
1586
+ return first ? { node: first, isArray: true } : null;
1587
+ }
1588
+ return { node: val, isArray: false };
1589
+ }
1590
+ return null;
1591
+ }
1592
+ function extractErrorType(method, sourceFile, project) {
1593
+ for (const decorator of method.getDecorators()) {
1594
+ if (decorator.getName() !== "ApiResponse") continue;
1595
+ const status = apiResponseStatus(decorator);
1596
+ if (status === null || status < 400) continue;
1597
+ const typeInfo = apiResponseTypeNode(decorator);
1598
+ if (!typeInfo) continue;
1599
+ const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
1600
+ const type = typeInfo.isArray ? `Array<${inner}>` : inner;
1601
+ let ref = null;
1602
+ if (Node5.isIdentifier(typeInfo.node)) {
1603
+ const name = typeInfo.node.getText();
1604
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1605
+ if (localDecl?.isExported()) {
1606
+ ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
1607
+ } else {
1608
+ const resolved = resolveImportedType(name, sourceFile, project);
1609
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
1610
+ ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
1611
+ }
1612
+ }
1613
+ }
1614
+ return { type, ref };
1615
+ }
1616
+ return null;
1617
+ }
1387
1618
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
1388
1619
  if (!Node5.isIdentifier(node)) return "unknown";
1389
1620
  const name = node.getText();
@@ -1399,17 +1630,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
1399
1630
  unwrapContainers: true
1400
1631
  });
1401
1632
  }
1633
+ var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
1634
+ var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
1635
+ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
1636
+ function detectStreamElement(method) {
1637
+ const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
1638
+ let node = method.getReturnTypeNode();
1639
+ node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
1640
+ const containerEl = streamContainerElement(node);
1641
+ if (containerEl) {
1642
+ return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
1643
+ }
1644
+ if (hasSse) return node ?? null;
1645
+ return null;
1646
+ }
1647
+ function streamContainerElement(node) {
1648
+ if (!node || !Node5.isTypeReference(node)) return null;
1649
+ const typeName = node.getTypeName();
1650
+ const name = Node5.isIdentifier(typeName) ? typeName.getText() : "";
1651
+ if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
1652
+ return node.getTypeArguments()[0] ?? null;
1653
+ }
1654
+ return null;
1655
+ }
1656
+ function unwrapNamedContainer(node, names) {
1657
+ if (!node || !Node5.isTypeReference(node)) return node;
1658
+ const typeName = node.getTypeName();
1659
+ const name = Node5.isIdentifier(typeName) ? typeName.getText() : "";
1660
+ if (names.has(name)) {
1661
+ return node.getTypeArguments()[0] ?? node;
1662
+ }
1663
+ return node;
1664
+ }
1402
1665
  function extractDtoContract(method, sourceFile, project) {
1403
1666
  let body = extractBodyType(method, sourceFile, project);
1404
1667
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
1405
1668
  const query = extractQueryType(method, sourceFile, project);
1669
+ const streamElement = detectStreamElement(method);
1670
+ const isStream = streamElement !== null;
1406
1671
  if (filterInfo && filterInfo.source === "body") {
1407
1672
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
1408
1673
  body = body ?? bodyType;
1409
1674
  }
1410
1675
  const paramsType = extractParamsType(method, sourceFile, project);
1411
- const response = extractResponseType(method, sourceFile, project);
1412
- if (body === null && query === null && paramsType === null && response === "unknown" && filterInfo === null) {
1676
+ const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
1677
+ const errorInfo = extractErrorType(method, sourceFile, project);
1678
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
1413
1679
  return null;
1414
1680
  }
1415
1681
  let bodyRef = null;
@@ -1423,12 +1689,12 @@ function extractDtoContract(method, sourceFile, project) {
1423
1689
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
1424
1690
  }
1425
1691
  }
1426
- const returnTypeNode = method.getReturnTypeNode();
1692
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
1427
1693
  if (returnTypeNode) {
1428
1694
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
1429
1695
  }
1430
- if (!responseRef) {
1431
- const apiResp = method.getDecorator("ApiResponse");
1696
+ if (!responseRef && !isStream) {
1697
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
1432
1698
  if (apiResp) {
1433
1699
  const args = apiResp.getArguments();
1434
1700
  const optsArg = args[0];
@@ -1470,16 +1736,19 @@ function extractDtoContract(method, sourceFile, project) {
1470
1736
  query,
1471
1737
  body,
1472
1738
  response,
1739
+ error: errorInfo?.type ?? null,
1473
1740
  params: paramsType,
1474
1741
  queryRef,
1475
1742
  bodyRef,
1476
1743
  responseRef,
1744
+ errorRef: errorInfo?.ref ?? null,
1477
1745
  filterFields: filterInfo?.fieldNames ?? null,
1478
1746
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
1479
1747
  filterSource: filterInfo?.source ?? null,
1480
1748
  formWarnings,
1481
1749
  bodySchema,
1482
- querySchema
1750
+ querySchema,
1751
+ stream: isStream
1483
1752
  };
1484
1753
  }
1485
1754
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -1597,6 +1866,7 @@ function parseDefineContractCall(callExpr) {
1597
1866
  let query = null;
1598
1867
  let body = null;
1599
1868
  let response = "unknown";
1869
+ let error = null;
1600
1870
  let bodyZodText = null;
1601
1871
  let queryZodText = null;
1602
1872
  for (const prop of optsArg.getProperties()) {
@@ -1612,25 +1882,27 @@ function parseDefineContractCall(callExpr) {
1612
1882
  bodyZodText = val.getText();
1613
1883
  } else if (propName === "response") {
1614
1884
  response = zodAstToTs(val);
1885
+ } else if (propName === "error") {
1886
+ error = zodAstToTs(val);
1615
1887
  }
1616
1888
  }
1617
- return { query, body, response, bodyZodText, queryZodText };
1889
+ return { query, body, response, error, bodyZodText, queryZodText };
1618
1890
  }
1619
1891
 
1620
1892
  // src/discovery/contracts-fast.ts
1621
- async function discoverContractsFast(opts) {
1622
- const { cwd, glob, tsconfig } = opts;
1623
- const tsconfigPath = tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
1624
- let project;
1893
+ function resolveTsconfigPath(cwd, tsconfig) {
1894
+ return tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
1895
+ }
1896
+ function createDiscoveryProject(tsconfigPath) {
1625
1897
  try {
1626
- project = new Project({
1898
+ return new Project({
1627
1899
  tsConfigFilePath: tsconfigPath,
1628
1900
  skipAddingFilesFromTsConfig: true,
1629
1901
  skipLoadingLibFiles: true,
1630
1902
  skipFileDependencyResolution: true
1631
1903
  });
1632
1904
  } catch {
1633
- project = new Project({
1905
+ return new Project({
1634
1906
  skipAddingFilesFromTsConfig: true,
1635
1907
  skipLoadingLibFiles: true,
1636
1908
  skipFileDependencyResolution: true,
@@ -1641,20 +1913,98 @@ async function discoverContractsFast(opts) {
1641
1913
  }
1642
1914
  });
1643
1915
  }
1644
- const files = await fg(glob, { cwd, absolute: true, onlyFiles: true });
1645
- for (const f of files) {
1646
- project.addSourceFileAtPath(f);
1647
- }
1648
- const routes = [];
1916
+ }
1917
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
1649
1918
  setDiscoveryContext(project, {
1650
1919
  projectRoot: cwd,
1651
1920
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
1652
1921
  });
1653
- for (const sourceFile of project.getSourceFiles()) {
1654
- routes.push(...extractFromSourceFile(sourceFile, project));
1922
+ }
1923
+ function extractRoutesFrom(project, controllerPaths) {
1924
+ const routes = [];
1925
+ for (const path of controllerPaths) {
1926
+ const sourceFile = project.getSourceFile(path);
1927
+ if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
1655
1928
  }
1656
1929
  return routes;
1657
1930
  }
1931
+ var PersistentDiscovery = class _PersistentDiscovery {
1932
+ project;
1933
+ cwd;
1934
+ glob;
1935
+ /** Absolute paths of the controllers currently loaded as extraction roots. */
1936
+ controllerPaths = /* @__PURE__ */ new Set();
1937
+ constructor(project, cwd, glob) {
1938
+ this.project = project;
1939
+ this.cwd = cwd;
1940
+ this.glob = glob;
1941
+ }
1942
+ /**
1943
+ * Build the initial persistent Project: create it, glob + add all controllers,
1944
+ * bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
1945
+ */
1946
+ static async create(opts) {
1947
+ const { cwd, glob, tsconfig } = opts;
1948
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
1949
+ const project = createDiscoveryProject(tsconfigPath);
1950
+ bindDiscoveryContext(project, cwd, tsconfigPath);
1951
+ const instance = new _PersistentDiscovery(project, cwd, glob);
1952
+ const files = await fg(glob, { cwd, absolute: true, onlyFiles: true });
1953
+ for (const f of files) {
1954
+ project.addSourceFileAtPath(f);
1955
+ instance.controllerPaths.add(f);
1956
+ }
1957
+ return instance;
1958
+ }
1959
+ /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
1960
+ discover() {
1961
+ return this.runExtraction();
1962
+ }
1963
+ /**
1964
+ * Re-discover after one or more files changed. Refreshes the changed file(s)
1965
+ * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
1966
+ * to pick up added/removed controllers, clears the per-Project caches, then
1967
+ * re-extracts. `changedPaths` is a hint; correctness does not depend on it
1968
+ * being exhaustive because re-globbing + refresh-on-presence covers the set.
1969
+ */
1970
+ async rediscover(changedPaths) {
1971
+ if (changedPaths) {
1972
+ for (const p of changedPaths) {
1973
+ const abs = resolve3(p);
1974
+ const sf = this.project.getSourceFile(abs);
1975
+ if (sf) {
1976
+ await sf.refreshFromFileSystem();
1977
+ }
1978
+ }
1979
+ }
1980
+ const globbed = new Set(
1981
+ await fg(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
1982
+ );
1983
+ for (const f of globbed) {
1984
+ if (!this.controllerPaths.has(f)) {
1985
+ try {
1986
+ this.project.addSourceFileAtPath(f);
1987
+ this.controllerPaths.add(f);
1988
+ } catch {
1989
+ }
1990
+ }
1991
+ }
1992
+ for (const f of this.controllerPaths) {
1993
+ if (!globbed.has(f)) {
1994
+ const sf = this.project.getSourceFile(f);
1995
+ if (sf) this.project.removeSourceFile(sf);
1996
+ this.controllerPaths.delete(f);
1997
+ }
1998
+ }
1999
+ return this.runExtraction();
2000
+ }
2001
+ /** Clear stale per-Project caches, then extract over the controller set. */
2002
+ runExtraction() {
2003
+ clearTypeResolutionCaches(this.project);
2004
+ clearEnumCache(this.project);
2005
+ return extractRoutesFrom(this.project, this.controllerPaths);
2006
+ }
2007
+ };
1658
2008
  function decoratorStringArg(decoratorExpr) {
1659
2009
  if (!decoratorExpr) return void 0;
1660
2010
  if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -1710,6 +2060,11 @@ function resolveVerb(method) {
1710
2060
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
1711
2061
  }
1712
2062
  }
2063
+ const sseDecorator = method.getDecorator("Sse");
2064
+ if (sseDecorator) {
2065
+ const pathArg = sseDecorator.getArguments()[0];
2066
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
2067
+ }
1713
2068
  return null;
1714
2069
  }
1715
2070
  function readAsDecorator(node, label) {
@@ -1752,7 +2107,17 @@ function buildRoute(args) {
1752
2107
  };
1753
2108
  }
1754
2109
  function extractContractRoute(args) {
1755
- const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
2110
+ const {
2111
+ cls,
2112
+ method,
2113
+ applyContractDecorator,
2114
+ verb,
2115
+ prefix,
2116
+ className,
2117
+ sourceFile,
2118
+ project,
2119
+ seenNames
2120
+ } = args;
1756
2121
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
1757
2122
  if (!firstDecoratorArg) return null;
1758
2123
  let contractDef = null;
@@ -1762,18 +2127,19 @@ function extractContractRoute(args) {
1762
2127
  contractDef = parseDefineContractCall(firstDecoratorArg);
1763
2128
  } else if (Node7.isIdentifier(firstDecoratorArg)) {
1764
2129
  const identName = firstDecoratorArg.getText();
1765
- const varDecl = sourceFile.getVariableDeclaration(identName);
1766
- if (!varDecl) {
2130
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
2131
+ if (!resolvedVar) {
1767
2132
  console.warn(
1768
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
2133
+ `[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
1769
2134
  );
1770
2135
  return null;
1771
2136
  }
2137
+ const { decl: varDecl, file: declFile } = resolvedVar;
1772
2138
  const initializer = varDecl.getInitializer();
1773
2139
  if (!initializer) return null;
1774
2140
  contractDef = parseDefineContractCall(initializer);
1775
2141
  if (contractDef && varDecl.isExported()) {
1776
- const filePath = sourceFile.getFilePath();
2142
+ const filePath = declFile.getFilePath();
1777
2143
  if (contractDef.body !== null) {
1778
2144
  bodyZodRef = { name: `${identName}.body`, filePath };
1779
2145
  }
@@ -1806,6 +2172,7 @@ function extractContractRoute(args) {
1806
2172
  query: contractDef.query,
1807
2173
  body: contractDef.body,
1808
2174
  response: contractDef.response,
2175
+ error: contractDef.error,
1809
2176
  // Path A: capture both the importable ref and the raw text. The emitter
1810
2177
  // prefers inlining the text (client-safe — re-exporting from a controller
1811
2178
  // would drag server-only deps into the client bundle).
@@ -1837,15 +2204,18 @@ function extractDtoRoute(args) {
1837
2204
  query: dtoContract?.query ?? null,
1838
2205
  body: dtoContract?.body ?? null,
1839
2206
  response: dtoContract?.response ?? "unknown",
2207
+ error: dtoContract?.error ?? null,
1840
2208
  queryRef: dtoContract?.queryRef ?? null,
1841
2209
  bodyRef: dtoContract?.bodyRef ?? null,
1842
2210
  responseRef: dtoContract?.responseRef ?? null,
2211
+ errorRef: dtoContract?.errorRef ?? null,
1843
2212
  filterFields: dtoContract?.filterFields ?? null,
1844
2213
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
1845
2214
  filterSource: dtoContract?.filterSource ?? null,
1846
2215
  formWarnings: dtoContract?.formWarnings ?? [],
1847
2216
  bodySchema: dtoContract?.bodySchema ?? null,
1848
- querySchema: dtoContract?.querySchema ?? null
2217
+ querySchema: dtoContract?.querySchema ?? null,
2218
+ stream: dtoContract?.stream ?? false
1849
2219
  }
1850
2220
  });
1851
2221
  }
@@ -1869,6 +2239,7 @@ function extractFromSourceFile(sourceFile, project) {
1869
2239
  prefix,
1870
2240
  className,
1871
2241
  sourceFile,
2242
+ project,
1872
2243
  seenNames
1873
2244
  }) : extractDtoRoute({
1874
2245
  cls,
@@ -1887,8 +2258,8 @@ function extractFromSourceFile(sourceFile, project) {
1887
2258
  }
1888
2259
 
1889
2260
  // src/generate.ts
1890
- import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1891
- import { dirname as dirname3, join as join11 } from "path";
2261
+ import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
2262
+ import { dirname as dirname3, join as join13 } from "path";
1892
2263
 
1893
2264
  // src/discovery/pages.ts
1894
2265
  import { readFile } from "fs/promises";
@@ -2417,17 +2788,28 @@ function emitFilterQueryType(c) {
2417
2788
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
2418
2789
  }
2419
2790
  function buildResponseType(c, outDir) {
2791
+ const respRef = c.contractSource.responseRef;
2792
+ if (c.contractSource.stream) {
2793
+ if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
2794
+ return c.contractSource.response;
2795
+ }
2420
2796
  if (c.controllerRef) {
2421
2797
  let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
2422
2798
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
2423
2799
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
2424
2800
  }
2425
- const respRef = c.contractSource.responseRef;
2426
2801
  if (respRef) {
2427
2802
  return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
2428
2803
  }
2429
2804
  return c.contractSource.response;
2430
2805
  }
2806
+ function buildErrorType(c) {
2807
+ const errRef = c.contractSource.errorRef;
2808
+ if (errRef) {
2809
+ return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
2810
+ }
2811
+ return c.contractSource.error ?? "unknown";
2812
+ }
2431
2813
  function emitRouterTypeBlock(tree, indent, outDir) {
2432
2814
  const pad = " ".repeat(indent);
2433
2815
  const lines = [];
@@ -2442,12 +2824,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
2442
2824
  const bodyRef = c.contractSource.bodyRef;
2443
2825
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
2444
2826
  const response = buildResponseType(c, outDir);
2827
+ const error = buildErrorType(c);
2445
2828
  const params = buildParamsType(c.params);
2446
2829
  const safeMethod = JSON.stringify(method);
2447
2830
  const safeUrl = JSON.stringify(c.path);
2448
2831
  const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
2832
+ const stream = c.contractSource.stream ? "true" : "false";
2449
2833
  lines.push(
2450
- `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
2834
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
2451
2835
  );
2452
2836
  } else {
2453
2837
  lines.push(`${pad}${objKey}: {`);
@@ -2519,15 +2903,21 @@ function emitReqHelper() {
2519
2903
  ""
2520
2904
  ];
2521
2905
  }
2522
- function renderLeaf(pad, objKey, req, requestExpr, members) {
2906
+ function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
2523
2907
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
2524
2908
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
2909
+ if (streamExpr) {
2910
+ lines.push(`${pad} stream: () => ${streamExpr},`);
2911
+ }
2525
2912
  for (const [name, value] of Object.entries(members)) {
2526
2913
  lines.push(`${pad} ${name}: ${value},`);
2527
2914
  }
2528
2915
  lines.push(`${pad}}),`);
2529
2916
  return lines;
2530
2917
  }
2918
+ function renderStreamExpr(req) {
2919
+ return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
2920
+ }
2531
2921
  function emitApiObjectBlock(tree, indent, p) {
2532
2922
  const pad = " ".repeat(indent);
2533
2923
  const lines = [];
@@ -2562,7 +2952,8 @@ function emitApiObjectBlock(tree, indent, p) {
2562
2952
  }
2563
2953
  const members = {};
2564
2954
  for (const [name, { value }] of owned) members[name] = value;
2565
- lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
2955
+ const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
2956
+ lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
2566
2957
  }
2567
2958
  return lines;
2568
2959
  }
@@ -2600,6 +2991,8 @@ var ROUTE_NAMESPACE = [
2600
2991
  ' export type Params<K extends string> = ResolveByName<K, "params">;',
2601
2992
  ' export type Error<K extends string> = ResolveByName<K, "error">;',
2602
2993
  ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
2994
+ " /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
2995
+ ' export type Stream<K extends string> = ResolveByName<K, "response">;',
2603
2996
  " export type Request<K extends string> = {",
2604
2997
  " body: Body<K>;",
2605
2998
  " query: Query<K>;",
@@ -2616,6 +3009,7 @@ var PATH_NAMESPACE = [
2616
3009
  ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
2617
3010
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
2618
3011
  ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
3012
+ ' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
2619
3013
  "}",
2620
3014
  ""
2621
3015
  ];
@@ -2627,6 +3021,7 @@ var EMPTY_ROUTE_NAMESPACE = [
2627
3021
  " export type Params<K extends string> = never;",
2628
3022
  " export type Error<K extends string> = never;",
2629
3023
  " export type FilterFields<K extends string> = never;",
3024
+ " export type Stream<K extends string> = never;",
2630
3025
  " export type Request<K extends string> = { body: never; query: never; params: never };",
2631
3026
  "}",
2632
3027
  ""
@@ -2639,6 +3034,7 @@ var EMPTY_PATH_NAMESPACE = [
2639
3034
  " export type Params<M extends string, U extends string> = never;",
2640
3035
  " export type Error<M extends string, U extends string> = never;",
2641
3036
  " export type FilterFields<M extends string, U extends string> = never;",
3037
+ " export type Stream<M extends string, U extends string> = never;",
2642
3038
  "}",
2643
3039
  ""
2644
3040
  ];
@@ -2662,7 +3058,7 @@ function buildApiFile(routes, outDir, opts = {}) {
2662
3058
  for (const r of contracted) {
2663
3059
  const cs = r.contract?.contractSource;
2664
3060
  if (!cs) continue;
2665
- const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
3061
+ const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
2666
3062
  for (const ref of refs) {
2667
3063
  if (!ref) continue;
2668
3064
  let names = importsByFile.get(ref.filePath);
@@ -2840,18 +3236,27 @@ function refRootIdentifier(refName) {
2840
3236
  function hasSource(src) {
2841
3237
  return !!(src.schema || src.zodText || src.zodRef);
2842
3238
  }
3239
+ function escapeRegExp(s) {
3240
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3241
+ }
3242
+ var wordBoundaryRegexCache = /* @__PURE__ */ new Map();
3243
+ function wordBoundaryRegex(token) {
3244
+ let re = wordBoundaryRegexCache.get(token);
3245
+ if (re === void 0) {
3246
+ re = new RegExp(`\\b${escapeRegExp(token)}\\b`, "g");
3247
+ wordBoundaryRegexCache.set(token, re);
3248
+ }
3249
+ return re;
3250
+ }
2843
3251
  function applyRenames(text, renames) {
2844
3252
  if (!renames || renames.size === 0) return text;
2845
3253
  let out = text;
2846
3254
  for (const [from, to] of renames) {
2847
3255
  if (from === to) continue;
2848
- out = out.replace(new RegExp(`\\b${escapeRegExp(from)}\\b`, "g"), to);
3256
+ out = out.replace(wordBoundaryRegex(from), to);
2849
3257
  }
2850
3258
  return out;
2851
3259
  }
2852
- function escapeRegExp(s) {
2853
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2854
- }
2855
3260
  function isSelfReferential(name, text) {
2856
3261
  return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
2857
3262
  }
@@ -2863,7 +3268,23 @@ function planNestedSchemas(entries) {
2863
3268
  const local = Object.entries(entry.nestedSchemas);
2864
3269
  if (local.length === 0) continue;
2865
3270
  const rename = /* @__PURE__ */ new Map();
2866
- for (const [name] of local) rename.set(name, name);
3271
+ const renameValues = /* @__PURE__ */ new Set();
3272
+ const setRename = (key, value) => {
3273
+ const prev = rename.get(key);
3274
+ rename.set(key, value);
3275
+ if (prev !== void 0 && prev !== value) {
3276
+ let stillUsed = false;
3277
+ for (const v of rename.values()) {
3278
+ if (v === prev) {
3279
+ stillUsed = true;
3280
+ break;
3281
+ }
3282
+ }
3283
+ if (!stillUsed) renameValues.delete(prev);
3284
+ }
3285
+ renameValues.add(value);
3286
+ };
3287
+ for (const [name] of local) setRename(name, name);
2867
3288
  const textFor = (name) => {
2868
3289
  const raw = entry.nestedSchemas?.[name] ?? "";
2869
3290
  return applyRenames(raw, rename);
@@ -2881,11 +3302,11 @@ function planNestedSchemas(entries) {
2881
3302
  if (existing === text) continue;
2882
3303
  let i = 2;
2883
3304
  let candidate = `${name}_${i}`;
2884
- while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || [...rename.values()].includes(candidate)) {
3305
+ while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || renameValues.has(candidate)) {
2885
3306
  i += 1;
2886
3307
  candidate = `${name}_${i}`;
2887
3308
  }
2888
- rename.set(name, candidate);
3309
+ setRename(name, candidate);
2889
3310
  changed = true;
2890
3311
  }
2891
3312
  }
@@ -3095,11 +3516,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
3095
3516
  await writeFile4(join8(outDir, "index.d.ts"), content, "utf8");
3096
3517
  }
3097
3518
 
3098
- // src/emit/emit-pages.ts
3519
+ // src/emit/emit-mocks.ts
3099
3520
  import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
3100
- import { join as join9, relative as relative5 } from "path";
3101
- async function emitPages(pages, outDir, _options = {}) {
3521
+ import { join as join9 } from "path";
3522
+
3523
+ // src/ir/schema-node-to-json-schema.ts
3524
+ var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
3525
+ function parseLiteral(raw) {
3526
+ const t = raw.trim();
3527
+ if (t === "true") return true;
3528
+ if (t === "false") return false;
3529
+ if (t === "null") return null;
3530
+ const q = t[0];
3531
+ if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
3532
+ return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
3533
+ }
3534
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
3535
+ return Number(t);
3536
+ }
3537
+ return t;
3538
+ }
3539
+ function literalsType(values) {
3540
+ const types = new Set(values.map((v) => v === null ? "null" : typeof v));
3541
+ if (types.size === 1) {
3542
+ const only = [...types][0];
3543
+ if (only === "string") return "string";
3544
+ if (only === "number") return "number";
3545
+ if (only === "boolean") return "boolean";
3546
+ }
3547
+ return void 0;
3548
+ }
3549
+ function convert(node, ctx) {
3550
+ switch (node.kind) {
3551
+ case "string": {
3552
+ const out = { type: "string" };
3553
+ for (const c of node.checks) {
3554
+ if (c.check === "email") out.format = "email";
3555
+ else if (c.check === "url") out.format = "uri";
3556
+ else if (c.check === "uuid") out.format = "uuid";
3557
+ else if (c.check === "min") out.minLength = Number(c.value);
3558
+ else if (c.check === "max") out.maxLength = Number(c.value);
3559
+ else if (c.check === "regex") {
3560
+ const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
3561
+ out.pattern = m ? m[1] : c.pattern;
3562
+ }
3563
+ }
3564
+ return out;
3565
+ }
3566
+ case "number": {
3567
+ const out = { type: "number" };
3568
+ for (const c of node.checks) {
3569
+ if (c.check === "int") out.type = "integer";
3570
+ else if (c.check === "min") out.minimum = Number(c.value);
3571
+ else if (c.check === "max") out.maximum = Number(c.value);
3572
+ else if (c.check === "positive") out.exclusiveMinimum = 0;
3573
+ else if (c.check === "negative") out.exclusiveMaximum = 0;
3574
+ }
3575
+ return out;
3576
+ }
3577
+ case "boolean":
3578
+ return { type: "boolean" };
3579
+ case "date":
3580
+ return { type: "string", format: "date-time" };
3581
+ case "unknown":
3582
+ return node.note ? { description: node.note } : {};
3583
+ case "instanceof":
3584
+ return { type: "object", description: `instanceof ${node.ctor}` };
3585
+ case "enum": {
3586
+ const values = node.literals.map(parseLiteral);
3587
+ const t = literalsType(values);
3588
+ const out = { enum: values };
3589
+ if (t) out.type = t;
3590
+ return out;
3591
+ }
3592
+ case "literal": {
3593
+ const value = parseLiteral(node.raw);
3594
+ const out = { const: value };
3595
+ const t = literalsType([value]);
3596
+ if (t) out.type = t;
3597
+ return out;
3598
+ }
3599
+ case "union": {
3600
+ const options = node.options.map((o) => convert(o, ctx));
3601
+ const out = { oneOf: options };
3602
+ if (node.discriminator) {
3603
+ out.discriminator = { propertyName: node.discriminator };
3604
+ }
3605
+ return out;
3606
+ }
3607
+ case "object": {
3608
+ const properties = {};
3609
+ const required = [];
3610
+ for (const f of node.fields) {
3611
+ if (f.value.kind === "optional") {
3612
+ properties[f.key] = convert(f.value.inner, ctx);
3613
+ } else {
3614
+ properties[f.key] = convert(f.value, ctx);
3615
+ required.push(f.key);
3616
+ }
3617
+ }
3618
+ const out = { type: "object", properties };
3619
+ if (required.length > 0) out.required = required;
3620
+ out.additionalProperties = node.passthrough;
3621
+ return out;
3622
+ }
3623
+ case "array":
3624
+ return { type: "array", items: convert(node.element, ctx) };
3625
+ case "optional":
3626
+ return widenNullable(convert(node.inner, ctx));
3627
+ case "ref":
3628
+ case "lazyRef":
3629
+ return { $ref: `${ctx.refPrefix}${node.name}` };
3630
+ case "annotated":
3631
+ return convert(node.inner, ctx);
3632
+ }
3633
+ }
3634
+ function widenNullable(schema) {
3635
+ if (schema.$ref) {
3636
+ return { anyOf: [schema, { type: "null" }] };
3637
+ }
3638
+ if (typeof schema.type === "string") {
3639
+ return { ...schema, type: [schema.type, "null"] };
3640
+ }
3641
+ if (Array.isArray(schema.type)) {
3642
+ return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
3643
+ }
3644
+ return { anyOf: [schema, { type: "null" }] };
3645
+ }
3646
+ function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
3647
+ const named = {};
3648
+ for (const [name, node] of mod.named) {
3649
+ named[name] = convert(node, ctx);
3650
+ }
3651
+ return { root: convert(mod.root, ctx), named };
3652
+ }
3653
+
3654
+ // src/emit/mock-gen-runtime.ts
3655
+ var MOCK_GEN_RUNTIME = `
3656
+ /** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
3657
+ function makeRng(seed) {
3658
+ let a = seed >>> 0;
3659
+ return {
3660
+ next() {
3661
+ a |= 0;
3662
+ a = (a + 0x6d2b79f5) | 0;
3663
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
3664
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
3665
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
3666
+ },
3667
+ };
3668
+ }
3669
+
3670
+ function __pick(rng, items) {
3671
+ return items[Math.floor(rng.next() * items.length)];
3672
+ }
3673
+
3674
+ function __intBetween(rng, min, max) {
3675
+ return Math.floor(rng.next() * (max - min + 1)) + min;
3676
+ }
3677
+
3678
+ const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
3679
+ const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
3680
+ const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
3681
+
3682
+ function __fakeWords(rng, count) {
3683
+ let out = [];
3684
+ for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
3685
+ return out.join(' ');
3686
+ }
3687
+
3688
+ function __hex(rng, len) {
3689
+ let s = '';
3690
+ for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
3691
+ return s;
3692
+ }
3693
+
3694
+ function __fakeUuid(rng) {
3695
+ return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
3696
+ }
3697
+
3698
+ function __fakeString(rng, schema) {
3699
+ switch (schema.format) {
3700
+ case 'email':
3701
+ return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
3702
+ case 'uri':
3703
+ case 'url':
3704
+ return 'https://example.com/' + __pick(rng, __WORDS);
3705
+ case 'uuid':
3706
+ return __fakeUuid(rng);
3707
+ case 'date-time':
3708
+ return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
3709
+ default:
3710
+ return __fakeWords(rng, __intBetween(rng, 1, 3));
3711
+ }
3712
+ }
3713
+
3714
+ /** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
3715
+ function generateMock(schema, rng, defs, depth) {
3716
+ defs = defs || {};
3717
+ depth = depth || 0;
3718
+ if (schema.$ref) {
3719
+ const name = schema.$ref.replace('#/components/schemas/', '');
3720
+ const target = defs[name];
3721
+ if (!target || depth > 4) return null;
3722
+ return generateMock(target, rng, defs, depth + 1);
3723
+ }
3724
+ if ('const' in schema) return schema.const;
3725
+ if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
3726
+ if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
3727
+ if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
3728
+ let type = Array.isArray(schema.type)
3729
+ ? (schema.type.filter((t) => t !== 'null')[0] || 'null')
3730
+ : schema.type;
3731
+ switch (type) {
3732
+ case 'string':
3733
+ return __fakeString(rng, schema);
3734
+ case 'integer':
3735
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
3736
+ case 'number':
3737
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
3738
+ case 'boolean':
3739
+ return rng.next() < 0.5;
3740
+ case 'null':
3741
+ return null;
3742
+ case 'array': {
3743
+ const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
3744
+ const items = schema.items || {};
3745
+ let arr = [];
3746
+ for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
3747
+ return arr;
3748
+ }
3749
+ case 'object': {
3750
+ const out = {};
3751
+ const props = schema.properties || {};
3752
+ for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
3753
+ return out;
3754
+ }
3755
+ default:
3756
+ return {};
3757
+ }
3758
+ }
3759
+ `.trim();
3760
+
3761
+ // src/emit/emit-mocks.ts
3762
+ var REF_PREFIX = "#/components/schemas/";
3763
+ function toMswPath(path, baseUrl) {
3764
+ return `${baseUrl}${path}`;
3765
+ }
3766
+ function responseSchemaFor(route, defs) {
3767
+ const cs = route.contract.contractSource;
3768
+ if (cs.responseSchema) {
3769
+ const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
3770
+ for (const [name, node] of Object.entries(named)) {
3771
+ if (!(name in defs)) defs[name] = node;
3772
+ }
3773
+ return root;
3774
+ }
3775
+ return {};
3776
+ }
3777
+ function buildMocksFile(routes, opts = {}) {
3778
+ const seed = opts.seed ?? 1;
3779
+ const baseUrl = opts.baseUrl ?? "";
3780
+ const contracted = routes.filter((r) => r.contract);
3781
+ const defs = {};
3782
+ const handlers = [];
3783
+ for (const r of contracted) {
3784
+ const schema = responseSchemaFor(r, defs);
3785
+ const method = r.method.toLowerCase();
3786
+ const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
3787
+ const path = toMswPath(r.path, baseUrl);
3788
+ const cs = r.contract.contractSource;
3789
+ const schemaLiteral = JSON.stringify(schema);
3790
+ const pathLit = JSON.stringify(path);
3791
+ if (cs.stream) {
3792
+ handlers.push(
3793
+ [
3794
+ ` // ${r.name} (stream)`,
3795
+ ` http.${mswMethod}(${pathLit}, () => {`,
3796
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
3797
+ " const body = `data: ${JSON.stringify(value)}\\n\\n`;",
3798
+ " return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
3799
+ " }),"
3800
+ ].join("\n")
3801
+ );
3802
+ } else {
3803
+ handlers.push(
3804
+ [
3805
+ ` // ${r.name}`,
3806
+ ` http.${mswMethod}(${pathLit}, () => {`,
3807
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
3808
+ " return HttpResponse.json(value);",
3809
+ " }),"
3810
+ ].join("\n")
3811
+ );
3812
+ }
3813
+ }
3814
+ const lines = [
3815
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
3816
+ "// MSW handlers returning deterministic, schema-shaped mock data.",
3817
+ "/* eslint-disable */",
3818
+ "// @ts-nocheck",
3819
+ "",
3820
+ "import { http, HttpResponse } from 'msw';",
3821
+ "",
3822
+ `const SEED = ${seed};`,
3823
+ "",
3824
+ "// ---------------------------------------------------------------------------",
3825
+ "// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
3826
+ "// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
3827
+ "// ---------------------------------------------------------------------------",
3828
+ MOCK_GEN_RUNTIME,
3829
+ "",
3830
+ "// Shared component schemas referenced by $ref.",
3831
+ `const DEFS = ${JSON.stringify(defs, null, 2)};`,
3832
+ "",
3833
+ "/** MSW request handlers, one per contracted route. */",
3834
+ "export const handlers = [",
3835
+ ...handlers,
3836
+ "];",
3837
+ ""
3838
+ ];
3839
+ return lines.join("\n");
3840
+ }
3841
+ async function emitMocks(routes, outDir, opts = {}) {
3102
3842
  await mkdir5(outDir, { recursive: true });
3843
+ const content = buildMocksFile(routes, opts);
3844
+ const fileName = opts.fileName ?? "mocks.ts";
3845
+ await writeFile5(join9(outDir, fileName), content, "utf8");
3846
+ }
3847
+
3848
+ // src/emit/emit-openapi.ts
3849
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
3850
+ import { join as join10 } from "path";
3851
+ var REF_PREFIX2 = "#/components/schemas/";
3852
+ function toOpenApiPath(path) {
3853
+ return path.replace(/:([^/]+)/g, "{$1}");
3854
+ }
3855
+ function positionSchema(schema, tsType, components) {
3856
+ if (schema) {
3857
+ const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
3858
+ for (const [name, node] of Object.entries(named)) {
3859
+ if (!(name in components)) components[name] = node;
3860
+ }
3861
+ return root;
3862
+ }
3863
+ return tsType ? { description: tsType } : {};
3864
+ }
3865
+ function buildParameters(route) {
3866
+ const params = [];
3867
+ for (const p of route.params) {
3868
+ if (p.source === "path") {
3869
+ params.push({
3870
+ name: p.name,
3871
+ in: "path",
3872
+ required: true,
3873
+ schema: { type: "string" }
3874
+ });
3875
+ } else if (p.source === "query") {
3876
+ params.push({
3877
+ name: p.name,
3878
+ in: "query",
3879
+ required: false,
3880
+ schema: { type: "string" }
3881
+ });
3882
+ } else if (p.source === "header") {
3883
+ params.push({
3884
+ name: p.name,
3885
+ in: "header",
3886
+ required: false,
3887
+ schema: { type: "string" }
3888
+ });
3889
+ }
3890
+ }
3891
+ return params;
3892
+ }
3893
+ function buildResponses(cs, components) {
3894
+ const responses = {};
3895
+ const successSchema = positionSchema(
3896
+ // Prefer rich response IR when present; otherwise fall back to the TS type.
3897
+ cs.responseSchema ?? null,
3898
+ cs.response,
3899
+ components
3900
+ );
3901
+ const successContentType = cs.stream ? "text/event-stream" : "application/json";
3902
+ responses["200"] = {
3903
+ description: cs.stream ? "Server-sent event stream" : "Successful response",
3904
+ content: { [successContentType]: { schema: successSchema } }
3905
+ };
3906
+ const errorSchema = positionSchema(null, cs.error ?? null, components);
3907
+ const errorBody = {
3908
+ description: "Error response",
3909
+ content: { "application/json": { schema: errorSchema } }
3910
+ };
3911
+ if (cs.error || cs.errorRef) {
3912
+ responses["400"] = errorBody;
3913
+ responses.default = errorBody;
3914
+ } else {
3915
+ responses.default = {
3916
+ description: "Error response",
3917
+ content: { "application/json": { schema: {} } }
3918
+ };
3919
+ }
3920
+ return responses;
3921
+ }
3922
+ function buildOperation(route, components) {
3923
+ const cs = route.contract.contractSource;
3924
+ const op = {
3925
+ operationId: route.name,
3926
+ parameters: buildParameters(route),
3927
+ responses: buildResponses(cs, components)
3928
+ };
3929
+ const method = route.method.toUpperCase();
3930
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
3931
+ if (hasBody && (cs.bodySchema || cs.body)) {
3932
+ const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
3933
+ op.requestBody = {
3934
+ required: true,
3935
+ content: { "application/json": { schema: bodySchema } }
3936
+ };
3937
+ }
3938
+ return op;
3939
+ }
3940
+ function buildOpenApiSpec(routes, opts = {}) {
3941
+ const components = {};
3942
+ const paths = {};
3943
+ for (const route of routes) {
3944
+ if (!route.contract) continue;
3945
+ const oaPath = toOpenApiPath(route.path);
3946
+ const method = route.method.toLowerCase();
3947
+ let pathItem = paths[oaPath];
3948
+ if (!pathItem) {
3949
+ pathItem = {};
3950
+ paths[oaPath] = pathItem;
3951
+ }
3952
+ pathItem[method] = buildOperation(route, components);
3953
+ }
3954
+ const info = opts.info ?? {};
3955
+ const doc = {
3956
+ openapi: "3.1.0",
3957
+ info: {
3958
+ title: info.title ?? "NestJS API",
3959
+ version: info.version ?? "1.0.0",
3960
+ ...info.description ? { description: info.description } : {}
3961
+ },
3962
+ paths,
3963
+ components: { schemas: components }
3964
+ };
3965
+ return doc;
3966
+ }
3967
+ async function emitOpenApi(routes, outDir, opts = {}) {
3968
+ await mkdir6(outDir, { recursive: true });
3969
+ const doc = buildOpenApiSpec(routes, opts);
3970
+ const fileName = opts.fileName ?? "openapi.json";
3971
+ await writeFile6(join10(outDir, fileName), `${JSON.stringify(doc, null, 2)}
3972
+ `, "utf8");
3973
+ }
3974
+
3975
+ // src/emit/emit-pages.ts
3976
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
3977
+ import { join as join11, relative as relative5 } from "path";
3978
+ async function emitPages(pages, outDir, _options = {}) {
3979
+ await mkdir7(outDir, { recursive: true });
3103
3980
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
3104
3981
  const augBody = pages.map((p) => {
3105
3982
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
@@ -3118,7 +3995,7 @@ ${augBody}
3118
3995
  }
3119
3996
  ${sharedPropsBlock}}
3120
3997
  `;
3121
- await writeFile5(join9(outDir, "pages.d.ts"), content, "utf8");
3998
+ await writeFile7(join11(outDir, "pages.d.ts"), content, "utf8");
3122
3999
  }
3123
4000
  function buildSharedPropsBlock(sharedProps) {
3124
4001
  if (!sharedProps) return "";
@@ -3148,12 +4025,12 @@ function needsQuotes(name) {
3148
4025
  }
3149
4026
 
3150
4027
  // src/emit/emit-routes.ts
3151
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
3152
- import { join as join10 } from "path";
4028
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
4029
+ import { join as join12 } from "path";
3153
4030
  async function emitRoutes(routes, outDir) {
3154
- await mkdir6(outDir, { recursive: true });
4031
+ await mkdir8(outDir, { recursive: true });
3155
4032
  const content = buildRoutesFile(routes);
3156
- await writeFile6(join10(outDir, "routes.ts"), content, "utf8");
4033
+ await writeFile8(join12(outDir, "routes.ts"), content, "utf8");
3157
4034
  }
3158
4035
  function buildRoutesFile(routes) {
3159
4036
  if (routes.length === 0) {
@@ -3301,21 +4178,38 @@ async function generate(config, inputRoutes = []) {
3301
4178
  });
3302
4179
  }
3303
4180
  const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
4181
+ if (hasContracts && config.openapi.enabled) {
4182
+ await emitOpenApi(routes, config.codegen.outDir, {
4183
+ fileName: config.openapi.fileName,
4184
+ info: {
4185
+ title: config.openapi.title,
4186
+ version: config.openapi.version,
4187
+ ...config.openapi.description ? { description: config.openapi.description } : {}
4188
+ }
4189
+ });
4190
+ }
4191
+ if (hasContracts && config.mocks.enabled) {
4192
+ await emitMocks(routes, config.codegen.outDir, {
4193
+ fileName: config.mocks.fileName,
4194
+ seed: config.mocks.seed,
4195
+ baseUrl: config.mocks.baseUrl
4196
+ });
4197
+ }
3304
4198
  await emitIndex(config.codegen.outDir, hasContracts, hasForms);
3305
4199
  if (extensions.length > 0) {
3306
4200
  const extraFiles = await collectEmittedFiles(extensions, ctx);
3307
4201
  for (const file of extraFiles) {
3308
- const dest = join11(config.codegen.outDir, file.path);
3309
- await mkdir7(dirname3(dest), { recursive: true });
3310
- await writeFile7(dest, file.contents, "utf8");
4202
+ const dest = join13(config.codegen.outDir, file.path);
4203
+ await mkdir9(dirname3(dest), { recursive: true });
4204
+ await writeFile9(dest, file.contents, "utf8");
3311
4205
  }
3312
4206
  }
3313
4207
  }
3314
4208
 
3315
4209
  // src/watch/lock-file.ts
3316
4210
  import { open } from "fs/promises";
3317
- import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3318
- import { join as join12 } from "path";
4211
+ import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
4212
+ import { join as join14 } from "path";
3319
4213
  var LOCK_FILE = ".watcher.lock";
3320
4214
  function isProcessAlive(pid) {
3321
4215
  try {
@@ -3326,8 +4220,8 @@ function isProcessAlive(pid) {
3326
4220
  }
3327
4221
  }
3328
4222
  async function acquireLock(outDir) {
3329
- await mkdir8(outDir, { recursive: true });
3330
- const lockPath = join12(outDir, LOCK_FILE);
4223
+ await mkdir10(outDir, { recursive: true });
4224
+ const lockPath = join14(outDir, LOCK_FILE);
3331
4225
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3332
4226
  try {
3333
4227
  const fd = await open(lockPath, "wx");
@@ -3367,7 +4261,7 @@ async function watch(config, onChange) {
3367
4261
  if (lock === null) {
3368
4262
  let holderPid = "unknown";
3369
4263
  try {
3370
- const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
4264
+ const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
3371
4265
  const data = JSON.parse(raw);
3372
4266
  if (data.pid !== void 0) holderPid = String(data.pid);
3373
4267
  } catch {
@@ -3377,12 +4271,20 @@ async function watch(config, onChange) {
3377
4271
  );
3378
4272
  return NO_OP_WATCHER;
3379
4273
  }
4274
+ let discovery = null;
4275
+ async function getDiscovery() {
4276
+ if (discovery === null) {
4277
+ discovery = await PersistentDiscovery.create({
4278
+ cwd: config.codegen.cwd,
4279
+ glob: config.contracts.glob,
4280
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4281
+ });
4282
+ return discovery;
4283
+ }
4284
+ return discovery;
4285
+ }
3380
4286
  try {
3381
- const initialRoutes = await discoverContractsFast({
3382
- cwd: config.codegen.cwd,
3383
- glob: config.contracts.glob,
3384
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3385
- });
4287
+ const initialRoutes = (await getDiscovery()).discover();
3386
4288
  await generate(config, initialRoutes);
3387
4289
  } catch (err) {
3388
4290
  console.warn(
@@ -3395,7 +4297,7 @@ async function watch(config, onChange) {
3395
4297
  }
3396
4298
  let pagesDebounceTimer;
3397
4299
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3398
- const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
4300
+ const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
3399
4301
  ignoreInitial: true,
3400
4302
  persistent: true,
3401
4303
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3421,23 +4323,23 @@ async function watch(config, onChange) {
3421
4323
  pagesWatcher.on("change", schedulePagesRegenerate);
3422
4324
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3423
4325
  let contractsDebounceTimer;
3424
- const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
4326
+ const pendingChangedPaths = /* @__PURE__ */ new Set();
4327
+ const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
3425
4328
  ignoreInitial: true,
3426
4329
  persistent: true,
3427
4330
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3428
4331
  });
3429
- function scheduleContractsRegenerate() {
4332
+ function scheduleContractsRegenerate(changedPath) {
4333
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3430
4334
  if (contractsDebounceTimer !== void 0) {
3431
4335
  clearTimeout(contractsDebounceTimer);
3432
4336
  }
3433
4337
  contractsDebounceTimer = setTimeout(async () => {
3434
4338
  contractsDebounceTimer = void 0;
4339
+ const changed = [...pendingChangedPaths];
4340
+ pendingChangedPaths.clear();
3435
4341
  try {
3436
- const routes = await discoverContractsFast({
3437
- cwd: config.codegen.cwd,
3438
- glob: config.contracts.glob,
3439
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3440
- });
4342
+ const routes = await (await getDiscovery()).rediscover(changed);
3441
4343
  await generate(config, routes);
3442
4344
  } catch (err) {
3443
4345
  console.error(
@@ -3448,17 +4350,17 @@ async function watch(config, onChange) {
3448
4350
  onChange?.();
3449
4351
  }, config.contracts.debounceMs);
3450
4352
  }
3451
- contractsWatcher.on("add", scheduleContractsRegenerate);
3452
- contractsWatcher.on("change", scheduleContractsRegenerate);
3453
- contractsWatcher.on("unlink", scheduleContractsRegenerate);
3454
- const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
4353
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4354
+ contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4355
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4356
+ const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
3455
4357
  ignoreInitial: true,
3456
4358
  persistent: true,
3457
4359
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3458
4360
  });
3459
- formsWatcher.on("add", scheduleContractsRegenerate);
3460
- formsWatcher.on("change", scheduleContractsRegenerate);
3461
- formsWatcher.on("unlink", scheduleContractsRegenerate);
4361
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4362
+ formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4363
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
3462
4364
  return {
3463
4365
  close: async () => {
3464
4366
  if (pagesDebounceTimer !== void 0) {