@dudousxd/nestjs-codegen 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,10 +323,85 @@ 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
+ }
388
+ var _findTypeCache = /* @__PURE__ */ new WeakMap();
389
+ function clearTypeResolutionCaches(project) {
390
+ _findTypeCache.delete(project);
391
+ _resolveNamedRefCache.delete(project);
392
+ }
313
393
  function findType(name, sourceFile, project) {
394
+ let byKey = _findTypeCache.get(project);
395
+ if (byKey === void 0) {
396
+ byKey = /* @__PURE__ */ new Map();
397
+ _findTypeCache.set(project, byKey);
398
+ }
399
+ const key = `${sourceFile.getFilePath()}\0${name}`;
400
+ if (byKey.has(key)) return byKey.get(key) ?? null;
314
401
  const local = findTypeInFile(name, sourceFile);
315
- if (local) return local;
316
- return resolveImportedType(name, sourceFile, project);
402
+ const result = local ?? resolveImportedType(name, sourceFile, project);
403
+ byKey.set(key, result);
404
+ return result;
317
405
  }
318
406
  var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
319
407
  function _localDeclForKinds(name, file, kinds) {
@@ -350,6 +438,26 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
350
438
  if (_NON_REF_NAMES.has(refName)) return null;
351
439
  name = refName;
352
440
  }
441
+ return _resolveNamedRef(name, sourceFile, project, opts);
442
+ }
443
+ var _resolveNamedRefCache = /* @__PURE__ */ new WeakMap();
444
+ function _resolveNamedRef(name, sourceFile, project, opts) {
445
+ let byKey = _resolveNamedRefCache.get(project);
446
+ if (byKey === void 0) {
447
+ byKey = /* @__PURE__ */ new Map();
448
+ _resolveNamedRefCache.set(project, byKey);
449
+ }
450
+ const kindsKey = [...opts.kinds].sort().join(",");
451
+ const key = `${sourceFile.getFilePath()}\0${name}\0${kindsKey}\0${opts.allowBareSpecifier ? 1 : 0}`;
452
+ if (byKey.has(key)) {
453
+ const cached = byKey.get(key) ?? null;
454
+ return cached ? { ...cached } : null;
455
+ }
456
+ const computed = _computeNamedRef(name, sourceFile, project, opts);
457
+ byKey.set(key, computed);
458
+ return computed ? { ...computed } : null;
459
+ }
460
+ function _computeNamedRef(name, sourceFile, project, opts) {
353
461
  if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
354
462
  return { name, filePath: sourceFile.getFilePath() };
355
463
  }
@@ -424,7 +532,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
424
532
  emittedClasses: /* @__PURE__ */ new Map(),
425
533
  visiting: /* @__PURE__ */ new Set(),
426
534
  recursiveSchemas: /* @__PURE__ */ new Set(),
427
- depth: 0
535
+ depth: 0,
536
+ typeBindings: /* @__PURE__ */ new Map()
428
537
  };
429
538
  const root = buildObject(classDecl, sourceFile, ctx);
430
539
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -448,11 +557,34 @@ function buildProperty(prop, classFile, ctx) {
448
557
  const typeNode = prop.getTypeNode();
449
558
  const typeText = typeNode?.getText() ?? "unknown";
450
559
  const isArrayType = !!typeNode && Node2.isArrayTypeNode(typeNode);
560
+ const discriminator = resolveDiscriminator(dec("Type"));
561
+ if (discriminator) {
562
+ const options = discriminator.subTypes.map(
563
+ (name) => buildNestedReference(name, classFile, ctx)
564
+ );
565
+ const unionNode = {
566
+ kind: "union",
567
+ options,
568
+ discriminator: discriminator.property
569
+ };
570
+ const wrapArray = has("IsArray") || isArrayType;
571
+ const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
572
+ return applyPresence(node2, decorators);
573
+ }
574
+ const propTypeParam = singularClassName(typeText);
575
+ if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
576
+ const bound = ctx.typeBindings.get(propTypeParam);
577
+ const childNode = buildNestedReference(bound, classFile, ctx);
578
+ const wrapArray = has("IsArray") || isArrayType;
579
+ const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
580
+ return applyPresence(node2, decorators);
581
+ }
451
582
  const typeRefName = resolveTypeFactoryName(dec("Type"));
452
583
  if (has("ValidateNested") || typeRefName) {
584
+ const typeArgs = genericTypeArgNames(typeNode);
453
585
  const childName = typeRefName ?? singularClassName(typeText);
454
586
  if (childName) {
455
- const childNode = buildNestedReference(childName, classFile, ctx);
587
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
456
588
  const wrapArray = has("IsArray") || isArrayType;
457
589
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
458
590
  return applyPresence(node2, decorators);
@@ -577,10 +709,12 @@ function baseFromType(typeText, isArrayType) {
577
709
  return { kind: "unknown" };
578
710
  }
579
711
  }
580
- function buildNestedReference(className, fromFile, ctx) {
581
- if (ctx.visiting.has(className)) {
582
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
583
- ctx.emittedClasses.set(className, reserved);
712
+ function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
713
+ const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
714
+ const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
715
+ if (ctx.visiting.has(cacheKey)) {
716
+ const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
717
+ ctx.emittedClasses.set(cacheKey, reserved);
584
718
  ctx.recursiveSchemas.add(reserved);
585
719
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
586
720
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -599,19 +733,27 @@ function buildNestedReference(className, fromFile, ctx) {
599
733
  }
600
734
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
601
735
  }
602
- const existing = ctx.emittedClasses.get(className);
736
+ const existing = ctx.emittedClasses.get(cacheKey);
603
737
  if (existing) return { kind: "ref", name: existing };
604
- const schemaName = aliasFor(className, ctx);
738
+ const schemaName = aliasFor(schemaBase, ctx);
605
739
  const resolved = findType(className, fromFile, ctx.project);
606
740
  if (!resolved || resolved.kind !== "class") {
607
741
  return { kind: "object", fields: [], passthrough: true };
608
742
  }
609
- ctx.emittedClasses.set(className, schemaName);
610
- ctx.visiting.add(className);
743
+ const params = resolved.decl.getTypeParameters().map((p) => p.getName());
744
+ const newBindings = [];
745
+ params.forEach((param, i) => {
746
+ const arg = typeArgs[i];
747
+ if (arg) newBindings.push([param, arg]);
748
+ });
749
+ for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
750
+ ctx.emittedClasses.set(cacheKey, schemaName);
751
+ ctx.visiting.add(cacheKey);
611
752
  ctx.depth += 1;
612
753
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
613
754
  ctx.depth -= 1;
614
- ctx.visiting.delete(className);
755
+ ctx.visiting.delete(cacheKey);
756
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
615
757
  ctx.named.set(schemaName, childNode);
616
758
  return { kind: "ref", name: schemaName };
617
759
  }
@@ -658,6 +800,39 @@ function messageRaw(decorator) {
658
800
  }
659
801
  return void 0;
660
802
  }
803
+ function resolveDiscriminator(decorator) {
804
+ const optsArg = decorator?.getArguments()[1];
805
+ if (!optsArg || !Node2.isObjectLiteralExpression(optsArg)) return null;
806
+ let discProp;
807
+ for (const prop of optsArg.getProperties()) {
808
+ if (Node2.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
809
+ discProp = prop.getInitializer();
810
+ }
811
+ }
812
+ if (!discProp || !Node2.isObjectLiteralExpression(discProp)) return null;
813
+ let property = null;
814
+ const subTypes = [];
815
+ for (const prop of discProp.getProperties()) {
816
+ if (!Node2.isPropertyAssignment(prop)) continue;
817
+ const name = prop.getName();
818
+ const init = prop.getInitializer();
819
+ if (!init) continue;
820
+ if (name === "property" && Node2.isStringLiteral(init)) {
821
+ property = init.getLiteralValue();
822
+ } else if (name === "subTypes" && Node2.isArrayLiteralExpression(init)) {
823
+ for (const el of init.getElements()) {
824
+ if (!Node2.isObjectLiteralExpression(el)) continue;
825
+ for (const p of el.getProperties()) {
826
+ if (!Node2.isPropertyAssignment(p) || p.getName() !== "name") continue;
827
+ const nameInit = p.getInitializer();
828
+ if (nameInit && Node2.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
829
+ }
830
+ }
831
+ }
832
+ }
833
+ if (!property || subTypes.length === 0) return null;
834
+ return { property, subTypes };
835
+ }
661
836
  function resolveTypeFactoryName(decorator) {
662
837
  const arg = firstArg(decorator);
663
838
  if (!arg) return null;
@@ -671,6 +846,17 @@ function singularClassName(typeText) {
671
846
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
672
847
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
673
848
  }
849
+ function genericTypeArgNames(typeNode) {
850
+ if (!typeNode || !Node2.isTypeReference(typeNode)) return [];
851
+ const names = [];
852
+ for (const arg of typeNode.getTypeArguments()) {
853
+ if (!Node2.isTypeReference(arg)) return [];
854
+ const tn = arg.getTypeName();
855
+ if (!Node2.isIdentifier(tn)) return [];
856
+ names.push(tn.getText());
857
+ }
858
+ return names;
859
+ }
674
860
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
675
861
  const arg = firstArg(decorator);
676
862
  if (!arg) return null;
@@ -729,17 +915,34 @@ import {
729
915
  } from "ts-morph";
730
916
 
731
917
  // src/discovery/enum-resolution.ts
918
+ var _enumCache = /* @__PURE__ */ new WeakMap();
919
+ function clearEnumCache(project) {
920
+ _enumCache.delete(project);
921
+ }
732
922
  function resolveEnumValues(name, sourceFile, project) {
923
+ let byKey = _enumCache.get(project);
924
+ if (byKey === void 0) {
925
+ byKey = /* @__PURE__ */ new Map();
926
+ _enumCache.set(project, byKey);
927
+ }
928
+ const key = `${sourceFile.getFilePath()}\0${name}`;
929
+ if (byKey.has(key)) {
930
+ const cached = byKey.get(key) ?? null;
931
+ return cached ? { values: [...cached.values], numeric: cached.numeric } : null;
932
+ }
733
933
  const resolved = findType(name, sourceFile, project);
734
- if (!resolved || resolved.kind !== "enum") return null;
735
- let numeric = true;
736
- const values = resolved.members.map((m) => {
737
- const parsed = JSON.parse(m);
738
- if (typeof parsed === "string") numeric = false;
739
- return String(parsed);
740
- });
741
- if (values.length === 0) return null;
742
- return { values, numeric };
934
+ let result = null;
935
+ if (resolved && resolved.kind === "enum") {
936
+ let numeric = true;
937
+ const values = resolved.members.map((m) => {
938
+ const parsed = JSON.parse(m);
939
+ if (typeof parsed === "string") numeric = false;
940
+ return String(parsed);
941
+ });
942
+ if (values.length > 0) result = { values, numeric };
943
+ }
944
+ byKey.set(key, result);
945
+ return result ? { values: [...result.values], numeric: result.numeric } : null;
743
946
  }
744
947
 
745
948
  // src/discovery/filter-field-types.ts
@@ -1184,24 +1387,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
1184
1387
  "Map",
1185
1388
  "Set"
1186
1389
  ]);
1187
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1390
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
1188
1391
  if (depth <= 0) return "unknown";
1189
1392
  if (Node5.isArrayTypeNode(typeNode)) {
1190
1393
  const elementType = typeNode.getElementTypeNode();
1191
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
1394
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
1192
1395
  }
1193
1396
  if (Node5.isUnionTypeNode(typeNode)) {
1194
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
1397
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
1195
1398
  }
1196
1399
  if (Node5.isIntersectionTypeNode(typeNode)) {
1197
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
1400
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
1198
1401
  }
1199
1402
  if (Node5.isParenthesizedTypeNode(typeNode)) {
1200
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
1403
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
1201
1404
  }
1202
1405
  if (Node5.isTypeReference(typeNode)) {
1203
1406
  const typeName = typeNode.getTypeName();
1204
1407
  const name = Node5.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
1408
+ const bound = subst.get(name);
1409
+ if (bound !== void 0) return bound;
1205
1410
  if (name === "string" || name === "number" || name === "boolean") return name;
1206
1411
  if (name === "Date") return "string";
1207
1412
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -1209,14 +1414,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1209
1414
  return "unknown";
1210
1415
  const wrapperMode = WRAPPER_TYPES[name];
1211
1416
  if (wrapperMode) {
1212
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
1417
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
1213
1418
  }
1214
1419
  if (PASSTHROUGH_UTILITY.has(name)) {
1215
1420
  return typeNode.getText();
1216
1421
  }
1217
1422
  const resolved = findType(name, sourceFile, project);
1218
1423
  if (resolved) {
1219
- return expandTypeDecl(resolved, project, depth - 1);
1424
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
1425
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
1220
1426
  }
1221
1427
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1222
1428
  return "unknown";
@@ -1229,32 +1435,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1229
1435
  if (kind === SyntaxKind2.AnyKeyword) return "unknown";
1230
1436
  return typeNode.getText();
1231
1437
  }
1232
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
1438
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
1233
1439
  const typeArgs = typeNode.getTypeArguments();
1234
1440
  const firstTypeArg = typeArgs[0];
1235
1441
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1236
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
1442
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
1237
1443
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
1238
1444
  }
1239
1445
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
1240
1446
  }
1241
- function expandTypeDecl(result, project, depth) {
1447
+ function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
1448
+ if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
1449
+ const params = result.decl.getTypeParameters().map((p) => p.getName());
1450
+ if (params.length === 0) return /* @__PURE__ */ new Map();
1451
+ const args = typeNode.getTypeArguments();
1452
+ const subst = /* @__PURE__ */ new Map();
1453
+ params.forEach((param, i) => {
1454
+ const arg = args[i];
1455
+ if (arg)
1456
+ subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
1457
+ });
1458
+ return subst;
1459
+ }
1460
+ function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
1242
1461
  if (depth < 0) return "unknown";
1243
1462
  switch (result.kind) {
1244
1463
  case "class":
1245
- return resolvePropertied(result.decl, result.file, project, depth);
1464
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
1246
1465
  case "interface":
1247
- return resolvePropertied(result.decl, result.file, project, depth);
1466
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
1248
1467
  case "typeAlias":
1249
1468
  if (result.typeNode) {
1250
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
1469
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
1251
1470
  }
1252
1471
  return result.text;
1253
1472
  case "enum":
1254
1473
  return result.members.join(" | ");
1255
1474
  }
1256
1475
  }
1257
- function resolvePropertied(decl, sourceFile, project, depth) {
1476
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
1258
1477
  if (depth < 0) return "unknown";
1259
1478
  const lines = [];
1260
1479
  for (const prop of decl.getProperties()) {
@@ -1263,7 +1482,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
1263
1482
  const propTypeNode = prop.getTypeNode();
1264
1483
  let propType = "unknown";
1265
1484
  if (propTypeNode) {
1266
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
1485
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
1267
1486
  }
1268
1487
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
1269
1488
  }
@@ -1312,7 +1531,7 @@ function extractParamsType(method, sourceFile, project) {
1312
1531
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
1313
1532
  }
1314
1533
  function extractResponseType(method, sourceFile, project) {
1315
- const apiResponseDecorator = method.getDecorator("ApiResponse");
1534
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
1316
1535
  if (apiResponseDecorator) {
1317
1536
  const args = apiResponseDecorator.getArguments();
1318
1537
  const optsArg = args[0];
@@ -1341,6 +1560,59 @@ function extractResponseType(method, sourceFile, project) {
1341
1560
  }
1342
1561
  return "unknown";
1343
1562
  }
1563
+ function apiResponseStatus(decorator) {
1564
+ const optsArg = decorator.getArguments()[0];
1565
+ if (!optsArg || !Node5.isObjectLiteralExpression(optsArg)) return null;
1566
+ for (const prop of optsArg.getProperties()) {
1567
+ if (!Node5.isPropertyAssignment(prop)) continue;
1568
+ if (prop.getName() !== "status") continue;
1569
+ const val = prop.getInitializer();
1570
+ if (val && Node5.isNumericLiteral(val)) return Number(val.getLiteralValue());
1571
+ }
1572
+ return null;
1573
+ }
1574
+ function apiResponseTypeNode(decorator) {
1575
+ const optsArg = decorator.getArguments()[0];
1576
+ if (!optsArg || !Node5.isObjectLiteralExpression(optsArg)) return null;
1577
+ for (const prop of optsArg.getProperties()) {
1578
+ if (!Node5.isPropertyAssignment(prop)) continue;
1579
+ if (prop.getName() !== "type") continue;
1580
+ const val = prop.getInitializer();
1581
+ if (!val) return null;
1582
+ if (Node5.isArrayLiteralExpression(val)) {
1583
+ const first = val.getElements()[0];
1584
+ return first ? { node: first, isArray: true } : null;
1585
+ }
1586
+ return { node: val, isArray: false };
1587
+ }
1588
+ return null;
1589
+ }
1590
+ function extractErrorType(method, sourceFile, project) {
1591
+ for (const decorator of method.getDecorators()) {
1592
+ if (decorator.getName() !== "ApiResponse") continue;
1593
+ const status = apiResponseStatus(decorator);
1594
+ if (status === null || status < 400) continue;
1595
+ const typeInfo = apiResponseTypeNode(decorator);
1596
+ if (!typeInfo) continue;
1597
+ const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
1598
+ const type = typeInfo.isArray ? `Array<${inner}>` : inner;
1599
+ let ref = null;
1600
+ if (Node5.isIdentifier(typeInfo.node)) {
1601
+ const name = typeInfo.node.getText();
1602
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
1603
+ if (localDecl?.isExported()) {
1604
+ ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
1605
+ } else {
1606
+ const resolved = resolveImportedType(name, sourceFile, project);
1607
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
1608
+ ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
1609
+ }
1610
+ }
1611
+ }
1612
+ return { type, ref };
1613
+ }
1614
+ return null;
1615
+ }
1344
1616
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
1345
1617
  if (!Node5.isIdentifier(node)) return "unknown";
1346
1618
  const name = node.getText();
@@ -1356,17 +1628,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
1356
1628
  unwrapContainers: true
1357
1629
  });
1358
1630
  }
1631
+ var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
1632
+ var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
1633
+ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
1634
+ function detectStreamElement(method) {
1635
+ const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
1636
+ let node = method.getReturnTypeNode();
1637
+ node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
1638
+ const containerEl = streamContainerElement(node);
1639
+ if (containerEl) {
1640
+ return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
1641
+ }
1642
+ if (hasSse) return node ?? null;
1643
+ return null;
1644
+ }
1645
+ function streamContainerElement(node) {
1646
+ if (!node || !Node5.isTypeReference(node)) return null;
1647
+ const typeName = node.getTypeName();
1648
+ const name = Node5.isIdentifier(typeName) ? typeName.getText() : "";
1649
+ if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
1650
+ return node.getTypeArguments()[0] ?? null;
1651
+ }
1652
+ return null;
1653
+ }
1654
+ function unwrapNamedContainer(node, names) {
1655
+ if (!node || !Node5.isTypeReference(node)) return node;
1656
+ const typeName = node.getTypeName();
1657
+ const name = Node5.isIdentifier(typeName) ? typeName.getText() : "";
1658
+ if (names.has(name)) {
1659
+ return node.getTypeArguments()[0] ?? node;
1660
+ }
1661
+ return node;
1662
+ }
1359
1663
  function extractDtoContract(method, sourceFile, project) {
1360
1664
  let body = extractBodyType(method, sourceFile, project);
1361
1665
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
1362
1666
  const query = extractQueryType(method, sourceFile, project);
1667
+ const streamElement = detectStreamElement(method);
1668
+ const isStream = streamElement !== null;
1363
1669
  if (filterInfo && filterInfo.source === "body") {
1364
1670
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
1365
1671
  body = body ?? bodyType;
1366
1672
  }
1367
1673
  const paramsType = extractParamsType(method, sourceFile, project);
1368
- const response = extractResponseType(method, sourceFile, project);
1369
- if (body === null && query === null && paramsType === null && response === "unknown" && filterInfo === null) {
1674
+ const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
1675
+ const errorInfo = extractErrorType(method, sourceFile, project);
1676
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
1370
1677
  return null;
1371
1678
  }
1372
1679
  let bodyRef = null;
@@ -1380,12 +1687,12 @@ function extractDtoContract(method, sourceFile, project) {
1380
1687
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
1381
1688
  }
1382
1689
  }
1383
- const returnTypeNode = method.getReturnTypeNode();
1690
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
1384
1691
  if (returnTypeNode) {
1385
1692
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
1386
1693
  }
1387
- if (!responseRef) {
1388
- const apiResp = method.getDecorator("ApiResponse");
1694
+ if (!responseRef && !isStream) {
1695
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
1389
1696
  if (apiResp) {
1390
1697
  const args = apiResp.getArguments();
1391
1698
  const optsArg = args[0];
@@ -1427,16 +1734,19 @@ function extractDtoContract(method, sourceFile, project) {
1427
1734
  query,
1428
1735
  body,
1429
1736
  response,
1737
+ error: errorInfo?.type ?? null,
1430
1738
  params: paramsType,
1431
1739
  queryRef,
1432
1740
  bodyRef,
1433
1741
  responseRef,
1742
+ errorRef: errorInfo?.ref ?? null,
1434
1743
  filterFields: filterInfo?.fieldNames ?? null,
1435
1744
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
1436
1745
  filterSource: filterInfo?.source ?? null,
1437
1746
  formWarnings,
1438
1747
  bodySchema,
1439
- querySchema
1748
+ querySchema,
1749
+ stream: isStream
1440
1750
  };
1441
1751
  }
1442
1752
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -1554,6 +1864,7 @@ function parseDefineContractCall(callExpr) {
1554
1864
  let query = null;
1555
1865
  let body = null;
1556
1866
  let response = "unknown";
1867
+ let error = null;
1557
1868
  let bodyZodText = null;
1558
1869
  let queryZodText = null;
1559
1870
  for (const prop of optsArg.getProperties()) {
@@ -1569,25 +1880,27 @@ function parseDefineContractCall(callExpr) {
1569
1880
  bodyZodText = val.getText();
1570
1881
  } else if (propName === "response") {
1571
1882
  response = zodAstToTs(val);
1883
+ } else if (propName === "error") {
1884
+ error = zodAstToTs(val);
1572
1885
  }
1573
1886
  }
1574
- return { query, body, response, bodyZodText, queryZodText };
1887
+ return { query, body, response, error, bodyZodText, queryZodText };
1575
1888
  }
1576
1889
 
1577
1890
  // src/discovery/contracts-fast.ts
1578
- async function discoverContractsFast(opts) {
1579
- const { cwd, glob, tsconfig } = opts;
1580
- const tsconfigPath = tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
1581
- let project;
1891
+ function resolveTsconfigPath(cwd, tsconfig) {
1892
+ return tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
1893
+ }
1894
+ function createDiscoveryProject(tsconfigPath) {
1582
1895
  try {
1583
- project = new Project({
1896
+ return new Project({
1584
1897
  tsConfigFilePath: tsconfigPath,
1585
1898
  skipAddingFilesFromTsConfig: true,
1586
1899
  skipLoadingLibFiles: true,
1587
1900
  skipFileDependencyResolution: true
1588
1901
  });
1589
1902
  } catch {
1590
- project = new Project({
1903
+ return new Project({
1591
1904
  skipAddingFilesFromTsConfig: true,
1592
1905
  skipLoadingLibFiles: true,
1593
1906
  skipFileDependencyResolution: true,
@@ -1598,20 +1911,98 @@ async function discoverContractsFast(opts) {
1598
1911
  }
1599
1912
  });
1600
1913
  }
1601
- const files = await fg(glob, { cwd, absolute: true, onlyFiles: true });
1602
- for (const f of files) {
1603
- project.addSourceFileAtPath(f);
1604
- }
1605
- const routes = [];
1914
+ }
1915
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
1606
1916
  setDiscoveryContext(project, {
1607
1917
  projectRoot: cwd,
1608
1918
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
1609
1919
  });
1610
- for (const sourceFile of project.getSourceFiles()) {
1611
- routes.push(...extractFromSourceFile(sourceFile, project));
1920
+ }
1921
+ function extractRoutesFrom(project, controllerPaths) {
1922
+ const routes = [];
1923
+ for (const path of controllerPaths) {
1924
+ const sourceFile = project.getSourceFile(path);
1925
+ if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
1612
1926
  }
1613
1927
  return routes;
1614
1928
  }
1929
+ var PersistentDiscovery = class _PersistentDiscovery {
1930
+ project;
1931
+ cwd;
1932
+ glob;
1933
+ /** Absolute paths of the controllers currently loaded as extraction roots. */
1934
+ controllerPaths = /* @__PURE__ */ new Set();
1935
+ constructor(project, cwd, glob) {
1936
+ this.project = project;
1937
+ this.cwd = cwd;
1938
+ this.glob = glob;
1939
+ }
1940
+ /**
1941
+ * Build the initial persistent Project: create it, glob + add all controllers,
1942
+ * bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
1943
+ */
1944
+ static async create(opts) {
1945
+ const { cwd, glob, tsconfig } = opts;
1946
+ const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
1947
+ const project = createDiscoveryProject(tsconfigPath);
1948
+ bindDiscoveryContext(project, cwd, tsconfigPath);
1949
+ const instance = new _PersistentDiscovery(project, cwd, glob);
1950
+ const files = await fg(glob, { cwd, absolute: true, onlyFiles: true });
1951
+ for (const f of files) {
1952
+ project.addSourceFileAtPath(f);
1953
+ instance.controllerPaths.add(f);
1954
+ }
1955
+ return instance;
1956
+ }
1957
+ /** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
1958
+ discover() {
1959
+ return this.runExtraction();
1960
+ }
1961
+ /**
1962
+ * Re-discover after one or more files changed. Refreshes the changed file(s)
1963
+ * from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
1964
+ * to pick up added/removed controllers, clears the per-Project caches, then
1965
+ * re-extracts. `changedPaths` is a hint; correctness does not depend on it
1966
+ * being exhaustive because re-globbing + refresh-on-presence covers the set.
1967
+ */
1968
+ async rediscover(changedPaths) {
1969
+ if (changedPaths) {
1970
+ for (const p of changedPaths) {
1971
+ const abs = resolve3(p);
1972
+ const sf = this.project.getSourceFile(abs);
1973
+ if (sf) {
1974
+ await sf.refreshFromFileSystem();
1975
+ }
1976
+ }
1977
+ }
1978
+ const globbed = new Set(
1979
+ await fg(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
1980
+ );
1981
+ for (const f of globbed) {
1982
+ if (!this.controllerPaths.has(f)) {
1983
+ try {
1984
+ this.project.addSourceFileAtPath(f);
1985
+ this.controllerPaths.add(f);
1986
+ } catch {
1987
+ }
1988
+ }
1989
+ }
1990
+ for (const f of this.controllerPaths) {
1991
+ if (!globbed.has(f)) {
1992
+ const sf = this.project.getSourceFile(f);
1993
+ if (sf) this.project.removeSourceFile(sf);
1994
+ this.controllerPaths.delete(f);
1995
+ }
1996
+ }
1997
+ return this.runExtraction();
1998
+ }
1999
+ /** Clear stale per-Project caches, then extract over the controller set. */
2000
+ runExtraction() {
2001
+ clearTypeResolutionCaches(this.project);
2002
+ clearEnumCache(this.project);
2003
+ return extractRoutesFrom(this.project, this.controllerPaths);
2004
+ }
2005
+ };
1615
2006
  function decoratorStringArg(decoratorExpr) {
1616
2007
  if (!decoratorExpr) return void 0;
1617
2008
  if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -1667,6 +2058,11 @@ function resolveVerb(method) {
1667
2058
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
1668
2059
  }
1669
2060
  }
2061
+ const sseDecorator = method.getDecorator("Sse");
2062
+ if (sseDecorator) {
2063
+ const pathArg = sseDecorator.getArguments()[0];
2064
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
2065
+ }
1670
2066
  return null;
1671
2067
  }
1672
2068
  function readAsDecorator(node, label) {
@@ -1709,7 +2105,17 @@ function buildRoute(args) {
1709
2105
  };
1710
2106
  }
1711
2107
  function extractContractRoute(args) {
1712
- const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
2108
+ const {
2109
+ cls,
2110
+ method,
2111
+ applyContractDecorator,
2112
+ verb,
2113
+ prefix,
2114
+ className,
2115
+ sourceFile,
2116
+ project,
2117
+ seenNames
2118
+ } = args;
1713
2119
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
1714
2120
  if (!firstDecoratorArg) return null;
1715
2121
  let contractDef = null;
@@ -1719,18 +2125,19 @@ function extractContractRoute(args) {
1719
2125
  contractDef = parseDefineContractCall(firstDecoratorArg);
1720
2126
  } else if (Node7.isIdentifier(firstDecoratorArg)) {
1721
2127
  const identName = firstDecoratorArg.getText();
1722
- const varDecl = sourceFile.getVariableDeclaration(identName);
1723
- if (!varDecl) {
2128
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
2129
+ if (!resolvedVar) {
1724
2130
  console.warn(
1725
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
2131
+ `[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
1726
2132
  );
1727
2133
  return null;
1728
2134
  }
2135
+ const { decl: varDecl, file: declFile } = resolvedVar;
1729
2136
  const initializer = varDecl.getInitializer();
1730
2137
  if (!initializer) return null;
1731
2138
  contractDef = parseDefineContractCall(initializer);
1732
2139
  if (contractDef && varDecl.isExported()) {
1733
- const filePath = sourceFile.getFilePath();
2140
+ const filePath = declFile.getFilePath();
1734
2141
  if (contractDef.body !== null) {
1735
2142
  bodyZodRef = { name: `${identName}.body`, filePath };
1736
2143
  }
@@ -1763,6 +2170,7 @@ function extractContractRoute(args) {
1763
2170
  query: contractDef.query,
1764
2171
  body: contractDef.body,
1765
2172
  response: contractDef.response,
2173
+ error: contractDef.error,
1766
2174
  // Path A: capture both the importable ref and the raw text. The emitter
1767
2175
  // prefers inlining the text (client-safe — re-exporting from a controller
1768
2176
  // would drag server-only deps into the client bundle).
@@ -1794,15 +2202,18 @@ function extractDtoRoute(args) {
1794
2202
  query: dtoContract?.query ?? null,
1795
2203
  body: dtoContract?.body ?? null,
1796
2204
  response: dtoContract?.response ?? "unknown",
2205
+ error: dtoContract?.error ?? null,
1797
2206
  queryRef: dtoContract?.queryRef ?? null,
1798
2207
  bodyRef: dtoContract?.bodyRef ?? null,
1799
2208
  responseRef: dtoContract?.responseRef ?? null,
2209
+ errorRef: dtoContract?.errorRef ?? null,
1800
2210
  filterFields: dtoContract?.filterFields ?? null,
1801
2211
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
1802
2212
  filterSource: dtoContract?.filterSource ?? null,
1803
2213
  formWarnings: dtoContract?.formWarnings ?? [],
1804
2214
  bodySchema: dtoContract?.bodySchema ?? null,
1805
- querySchema: dtoContract?.querySchema ?? null
2215
+ querySchema: dtoContract?.querySchema ?? null,
2216
+ stream: dtoContract?.stream ?? false
1806
2217
  }
1807
2218
  });
1808
2219
  }
@@ -1826,6 +2237,7 @@ function extractFromSourceFile(sourceFile, project) {
1826
2237
  prefix,
1827
2238
  className,
1828
2239
  sourceFile,
2240
+ project,
1829
2241
  seenNames
1830
2242
  }) : extractDtoRoute({
1831
2243
  cls,
@@ -1844,8 +2256,8 @@ function extractFromSourceFile(sourceFile, project) {
1844
2256
  }
1845
2257
 
1846
2258
  // src/generate.ts
1847
- import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1848
- import { dirname as dirname3, join as join11 } from "path";
2259
+ import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
2260
+ import { dirname as dirname3, join as join13 } from "path";
1849
2261
 
1850
2262
  // src/discovery/pages.ts
1851
2263
  import { readFile } from "fs/promises";
@@ -2374,17 +2786,28 @@ function emitFilterQueryType(c) {
2374
2786
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
2375
2787
  }
2376
2788
  function buildResponseType(c, outDir) {
2789
+ const respRef = c.contractSource.responseRef;
2790
+ if (c.contractSource.stream) {
2791
+ if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
2792
+ return c.contractSource.response;
2793
+ }
2377
2794
  if (c.controllerRef) {
2378
2795
  let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
2379
2796
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
2380
2797
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
2381
2798
  }
2382
- const respRef = c.contractSource.responseRef;
2383
2799
  if (respRef) {
2384
2800
  return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
2385
2801
  }
2386
2802
  return c.contractSource.response;
2387
2803
  }
2804
+ function buildErrorType(c) {
2805
+ const errRef = c.contractSource.errorRef;
2806
+ if (errRef) {
2807
+ return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
2808
+ }
2809
+ return c.contractSource.error ?? "unknown";
2810
+ }
2388
2811
  function emitRouterTypeBlock(tree, indent, outDir) {
2389
2812
  const pad = " ".repeat(indent);
2390
2813
  const lines = [];
@@ -2399,12 +2822,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
2399
2822
  const bodyRef = c.contractSource.bodyRef;
2400
2823
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
2401
2824
  const response = buildResponseType(c, outDir);
2825
+ const error = buildErrorType(c);
2402
2826
  const params = buildParamsType(c.params);
2403
2827
  const safeMethod = JSON.stringify(method);
2404
2828
  const safeUrl = JSON.stringify(c.path);
2405
2829
  const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
2830
+ const stream = c.contractSource.stream ? "true" : "false";
2406
2831
  lines.push(
2407
- `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
2832
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
2408
2833
  );
2409
2834
  } else {
2410
2835
  lines.push(`${pad}${objKey}: {`);
@@ -2476,15 +2901,21 @@ function emitReqHelper() {
2476
2901
  ""
2477
2902
  ];
2478
2903
  }
2479
- function renderLeaf(pad, objKey, req, requestExpr, members) {
2904
+ function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
2480
2905
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
2481
2906
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
2907
+ if (streamExpr) {
2908
+ lines.push(`${pad} stream: () => ${streamExpr},`);
2909
+ }
2482
2910
  for (const [name, value] of Object.entries(members)) {
2483
2911
  lines.push(`${pad} ${name}: ${value},`);
2484
2912
  }
2485
2913
  lines.push(`${pad}}),`);
2486
2914
  return lines;
2487
2915
  }
2916
+ function renderStreamExpr(req) {
2917
+ return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
2918
+ }
2488
2919
  function emitApiObjectBlock(tree, indent, p) {
2489
2920
  const pad = " ".repeat(indent);
2490
2921
  const lines = [];
@@ -2519,7 +2950,8 @@ function emitApiObjectBlock(tree, indent, p) {
2519
2950
  }
2520
2951
  const members = {};
2521
2952
  for (const [name, { value }] of owned) members[name] = value;
2522
- lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
2953
+ const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
2954
+ lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
2523
2955
  }
2524
2956
  return lines;
2525
2957
  }
@@ -2557,6 +2989,8 @@ var ROUTE_NAMESPACE = [
2557
2989
  ' export type Params<K extends string> = ResolveByName<K, "params">;',
2558
2990
  ' export type Error<K extends string> = ResolveByName<K, "error">;',
2559
2991
  ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
2992
+ " /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
2993
+ ' export type Stream<K extends string> = ResolveByName<K, "response">;',
2560
2994
  " export type Request<K extends string> = {",
2561
2995
  " body: Body<K>;",
2562
2996
  " query: Query<K>;",
@@ -2573,6 +3007,7 @@ var PATH_NAMESPACE = [
2573
3007
  ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
2574
3008
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
2575
3009
  ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
3010
+ ' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
2576
3011
  "}",
2577
3012
  ""
2578
3013
  ];
@@ -2584,6 +3019,7 @@ var EMPTY_ROUTE_NAMESPACE = [
2584
3019
  " export type Params<K extends string> = never;",
2585
3020
  " export type Error<K extends string> = never;",
2586
3021
  " export type FilterFields<K extends string> = never;",
3022
+ " export type Stream<K extends string> = never;",
2587
3023
  " export type Request<K extends string> = { body: never; query: never; params: never };",
2588
3024
  "}",
2589
3025
  ""
@@ -2596,6 +3032,7 @@ var EMPTY_PATH_NAMESPACE = [
2596
3032
  " export type Params<M extends string, U extends string> = never;",
2597
3033
  " export type Error<M extends string, U extends string> = never;",
2598
3034
  " export type FilterFields<M extends string, U extends string> = never;",
3035
+ " export type Stream<M extends string, U extends string> = never;",
2599
3036
  "}",
2600
3037
  ""
2601
3038
  ];
@@ -2619,7 +3056,7 @@ function buildApiFile(routes, outDir, opts = {}) {
2619
3056
  for (const r of contracted) {
2620
3057
  const cs = r.contract?.contractSource;
2621
3058
  if (!cs) continue;
2622
- const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
3059
+ const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
2623
3060
  for (const ref of refs) {
2624
3061
  if (!ref) continue;
2625
3062
  let names = importsByFile.get(ref.filePath);
@@ -3052,11 +3489,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
3052
3489
  await writeFile4(join8(outDir, "index.d.ts"), content, "utf8");
3053
3490
  }
3054
3491
 
3055
- // src/emit/emit-pages.ts
3492
+ // src/emit/emit-mocks.ts
3056
3493
  import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
3057
- import { join as join9, relative as relative5 } from "path";
3058
- async function emitPages(pages, outDir, _options = {}) {
3494
+ import { join as join9 } from "path";
3495
+
3496
+ // src/ir/schema-node-to-json-schema.ts
3497
+ var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
3498
+ function parseLiteral(raw) {
3499
+ const t = raw.trim();
3500
+ if (t === "true") return true;
3501
+ if (t === "false") return false;
3502
+ if (t === "null") return null;
3503
+ const q = t[0];
3504
+ if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
3505
+ return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
3506
+ }
3507
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
3508
+ return Number(t);
3509
+ }
3510
+ return t;
3511
+ }
3512
+ function literalsType(values) {
3513
+ const types = new Set(values.map((v) => v === null ? "null" : typeof v));
3514
+ if (types.size === 1) {
3515
+ const only = [...types][0];
3516
+ if (only === "string") return "string";
3517
+ if (only === "number") return "number";
3518
+ if (only === "boolean") return "boolean";
3519
+ }
3520
+ return void 0;
3521
+ }
3522
+ function convert(node, ctx) {
3523
+ switch (node.kind) {
3524
+ case "string": {
3525
+ const out = { type: "string" };
3526
+ for (const c of node.checks) {
3527
+ if (c.check === "email") out.format = "email";
3528
+ else if (c.check === "url") out.format = "uri";
3529
+ else if (c.check === "uuid") out.format = "uuid";
3530
+ else if (c.check === "min") out.minLength = Number(c.value);
3531
+ else if (c.check === "max") out.maxLength = Number(c.value);
3532
+ else if (c.check === "regex") {
3533
+ const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
3534
+ out.pattern = m ? m[1] : c.pattern;
3535
+ }
3536
+ }
3537
+ return out;
3538
+ }
3539
+ case "number": {
3540
+ const out = { type: "number" };
3541
+ for (const c of node.checks) {
3542
+ if (c.check === "int") out.type = "integer";
3543
+ else if (c.check === "min") out.minimum = Number(c.value);
3544
+ else if (c.check === "max") out.maximum = Number(c.value);
3545
+ else if (c.check === "positive") out.exclusiveMinimum = 0;
3546
+ else if (c.check === "negative") out.exclusiveMaximum = 0;
3547
+ }
3548
+ return out;
3549
+ }
3550
+ case "boolean":
3551
+ return { type: "boolean" };
3552
+ case "date":
3553
+ return { type: "string", format: "date-time" };
3554
+ case "unknown":
3555
+ return node.note ? { description: node.note } : {};
3556
+ case "instanceof":
3557
+ return { type: "object", description: `instanceof ${node.ctor}` };
3558
+ case "enum": {
3559
+ const values = node.literals.map(parseLiteral);
3560
+ const t = literalsType(values);
3561
+ const out = { enum: values };
3562
+ if (t) out.type = t;
3563
+ return out;
3564
+ }
3565
+ case "literal": {
3566
+ const value = parseLiteral(node.raw);
3567
+ const out = { const: value };
3568
+ const t = literalsType([value]);
3569
+ if (t) out.type = t;
3570
+ return out;
3571
+ }
3572
+ case "union": {
3573
+ const options = node.options.map((o) => convert(o, ctx));
3574
+ const out = { oneOf: options };
3575
+ if (node.discriminator) {
3576
+ out.discriminator = { propertyName: node.discriminator };
3577
+ }
3578
+ return out;
3579
+ }
3580
+ case "object": {
3581
+ const properties = {};
3582
+ const required = [];
3583
+ for (const f of node.fields) {
3584
+ if (f.value.kind === "optional") {
3585
+ properties[f.key] = convert(f.value.inner, ctx);
3586
+ } else {
3587
+ properties[f.key] = convert(f.value, ctx);
3588
+ required.push(f.key);
3589
+ }
3590
+ }
3591
+ const out = { type: "object", properties };
3592
+ if (required.length > 0) out.required = required;
3593
+ out.additionalProperties = node.passthrough;
3594
+ return out;
3595
+ }
3596
+ case "array":
3597
+ return { type: "array", items: convert(node.element, ctx) };
3598
+ case "optional":
3599
+ return widenNullable(convert(node.inner, ctx));
3600
+ case "ref":
3601
+ case "lazyRef":
3602
+ return { $ref: `${ctx.refPrefix}${node.name}` };
3603
+ case "annotated":
3604
+ return convert(node.inner, ctx);
3605
+ }
3606
+ }
3607
+ function widenNullable(schema) {
3608
+ if (schema.$ref) {
3609
+ return { anyOf: [schema, { type: "null" }] };
3610
+ }
3611
+ if (typeof schema.type === "string") {
3612
+ return { ...schema, type: [schema.type, "null"] };
3613
+ }
3614
+ if (Array.isArray(schema.type)) {
3615
+ return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
3616
+ }
3617
+ return { anyOf: [schema, { type: "null" }] };
3618
+ }
3619
+ function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
3620
+ const named = {};
3621
+ for (const [name, node] of mod.named) {
3622
+ named[name] = convert(node, ctx);
3623
+ }
3624
+ return { root: convert(mod.root, ctx), named };
3625
+ }
3626
+
3627
+ // src/emit/mock-gen-runtime.ts
3628
+ var MOCK_GEN_RUNTIME = `
3629
+ /** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
3630
+ function makeRng(seed) {
3631
+ let a = seed >>> 0;
3632
+ return {
3633
+ next() {
3634
+ a |= 0;
3635
+ a = (a + 0x6d2b79f5) | 0;
3636
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
3637
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
3638
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
3639
+ },
3640
+ };
3641
+ }
3642
+
3643
+ function __pick(rng, items) {
3644
+ return items[Math.floor(rng.next() * items.length)];
3645
+ }
3646
+
3647
+ function __intBetween(rng, min, max) {
3648
+ return Math.floor(rng.next() * (max - min + 1)) + min;
3649
+ }
3650
+
3651
+ const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
3652
+ const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
3653
+ const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
3654
+
3655
+ function __fakeWords(rng, count) {
3656
+ let out = [];
3657
+ for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
3658
+ return out.join(' ');
3659
+ }
3660
+
3661
+ function __hex(rng, len) {
3662
+ let s = '';
3663
+ for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
3664
+ return s;
3665
+ }
3666
+
3667
+ function __fakeUuid(rng) {
3668
+ return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
3669
+ }
3670
+
3671
+ function __fakeString(rng, schema) {
3672
+ switch (schema.format) {
3673
+ case 'email':
3674
+ return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
3675
+ case 'uri':
3676
+ case 'url':
3677
+ return 'https://example.com/' + __pick(rng, __WORDS);
3678
+ case 'uuid':
3679
+ return __fakeUuid(rng);
3680
+ case 'date-time':
3681
+ return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
3682
+ default:
3683
+ return __fakeWords(rng, __intBetween(rng, 1, 3));
3684
+ }
3685
+ }
3686
+
3687
+ /** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
3688
+ function generateMock(schema, rng, defs, depth) {
3689
+ defs = defs || {};
3690
+ depth = depth || 0;
3691
+ if (schema.$ref) {
3692
+ const name = schema.$ref.replace('#/components/schemas/', '');
3693
+ const target = defs[name];
3694
+ if (!target || depth > 4) return null;
3695
+ return generateMock(target, rng, defs, depth + 1);
3696
+ }
3697
+ if ('const' in schema) return schema.const;
3698
+ if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
3699
+ if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
3700
+ if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
3701
+ let type = Array.isArray(schema.type)
3702
+ ? (schema.type.filter((t) => t !== 'null')[0] || 'null')
3703
+ : schema.type;
3704
+ switch (type) {
3705
+ case 'string':
3706
+ return __fakeString(rng, schema);
3707
+ case 'integer':
3708
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
3709
+ case 'number':
3710
+ return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
3711
+ case 'boolean':
3712
+ return rng.next() < 0.5;
3713
+ case 'null':
3714
+ return null;
3715
+ case 'array': {
3716
+ const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
3717
+ const items = schema.items || {};
3718
+ let arr = [];
3719
+ for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
3720
+ return arr;
3721
+ }
3722
+ case 'object': {
3723
+ const out = {};
3724
+ const props = schema.properties || {};
3725
+ for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
3726
+ return out;
3727
+ }
3728
+ default:
3729
+ return {};
3730
+ }
3731
+ }
3732
+ `.trim();
3733
+
3734
+ // src/emit/emit-mocks.ts
3735
+ var REF_PREFIX = "#/components/schemas/";
3736
+ function toMswPath(path, baseUrl) {
3737
+ return `${baseUrl}${path}`;
3738
+ }
3739
+ function responseSchemaFor(route, defs) {
3740
+ const cs = route.contract.contractSource;
3741
+ if (cs.responseSchema) {
3742
+ const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
3743
+ for (const [name, node] of Object.entries(named)) {
3744
+ if (!(name in defs)) defs[name] = node;
3745
+ }
3746
+ return root;
3747
+ }
3748
+ return {};
3749
+ }
3750
+ function buildMocksFile(routes, opts = {}) {
3751
+ const seed = opts.seed ?? 1;
3752
+ const baseUrl = opts.baseUrl ?? "";
3753
+ const contracted = routes.filter((r) => r.contract);
3754
+ const defs = {};
3755
+ const handlers = [];
3756
+ for (const r of contracted) {
3757
+ const schema = responseSchemaFor(r, defs);
3758
+ const method = r.method.toLowerCase();
3759
+ const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
3760
+ const path = toMswPath(r.path, baseUrl);
3761
+ const cs = r.contract.contractSource;
3762
+ const schemaLiteral = JSON.stringify(schema);
3763
+ const pathLit = JSON.stringify(path);
3764
+ if (cs.stream) {
3765
+ handlers.push(
3766
+ [
3767
+ ` // ${r.name} (stream)`,
3768
+ ` http.${mswMethod}(${pathLit}, () => {`,
3769
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
3770
+ " const body = `data: ${JSON.stringify(value)}\\n\\n`;",
3771
+ " return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
3772
+ " }),"
3773
+ ].join("\n")
3774
+ );
3775
+ } else {
3776
+ handlers.push(
3777
+ [
3778
+ ` // ${r.name}`,
3779
+ ` http.${mswMethod}(${pathLit}, () => {`,
3780
+ ` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
3781
+ " return HttpResponse.json(value);",
3782
+ " }),"
3783
+ ].join("\n")
3784
+ );
3785
+ }
3786
+ }
3787
+ const lines = [
3788
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
3789
+ "// MSW handlers returning deterministic, schema-shaped mock data.",
3790
+ "/* eslint-disable */",
3791
+ "// @ts-nocheck",
3792
+ "",
3793
+ "import { http, HttpResponse } from 'msw';",
3794
+ "",
3795
+ `const SEED = ${seed};`,
3796
+ "",
3797
+ "// ---------------------------------------------------------------------------",
3798
+ "// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
3799
+ "// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
3800
+ "// ---------------------------------------------------------------------------",
3801
+ MOCK_GEN_RUNTIME,
3802
+ "",
3803
+ "// Shared component schemas referenced by $ref.",
3804
+ `const DEFS = ${JSON.stringify(defs, null, 2)};`,
3805
+ "",
3806
+ "/** MSW request handlers, one per contracted route. */",
3807
+ "export const handlers = [",
3808
+ ...handlers,
3809
+ "];",
3810
+ ""
3811
+ ];
3812
+ return lines.join("\n");
3813
+ }
3814
+ async function emitMocks(routes, outDir, opts = {}) {
3059
3815
  await mkdir5(outDir, { recursive: true });
3816
+ const content = buildMocksFile(routes, opts);
3817
+ const fileName = opts.fileName ?? "mocks.ts";
3818
+ await writeFile5(join9(outDir, fileName), content, "utf8");
3819
+ }
3820
+
3821
+ // src/emit/emit-openapi.ts
3822
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
3823
+ import { join as join10 } from "path";
3824
+ var REF_PREFIX2 = "#/components/schemas/";
3825
+ function toOpenApiPath(path) {
3826
+ return path.replace(/:([^/]+)/g, "{$1}");
3827
+ }
3828
+ function positionSchema(schema, tsType, components) {
3829
+ if (schema) {
3830
+ const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
3831
+ for (const [name, node] of Object.entries(named)) {
3832
+ if (!(name in components)) components[name] = node;
3833
+ }
3834
+ return root;
3835
+ }
3836
+ return tsType ? { description: tsType } : {};
3837
+ }
3838
+ function buildParameters(route) {
3839
+ const params = [];
3840
+ for (const p of route.params) {
3841
+ if (p.source === "path") {
3842
+ params.push({
3843
+ name: p.name,
3844
+ in: "path",
3845
+ required: true,
3846
+ schema: { type: "string" }
3847
+ });
3848
+ } else if (p.source === "query") {
3849
+ params.push({
3850
+ name: p.name,
3851
+ in: "query",
3852
+ required: false,
3853
+ schema: { type: "string" }
3854
+ });
3855
+ } else if (p.source === "header") {
3856
+ params.push({
3857
+ name: p.name,
3858
+ in: "header",
3859
+ required: false,
3860
+ schema: { type: "string" }
3861
+ });
3862
+ }
3863
+ }
3864
+ return params;
3865
+ }
3866
+ function buildResponses(cs, components) {
3867
+ const responses = {};
3868
+ const successSchema = positionSchema(
3869
+ // Prefer rich response IR when present; otherwise fall back to the TS type.
3870
+ cs.responseSchema ?? null,
3871
+ cs.response,
3872
+ components
3873
+ );
3874
+ const successContentType = cs.stream ? "text/event-stream" : "application/json";
3875
+ responses["200"] = {
3876
+ description: cs.stream ? "Server-sent event stream" : "Successful response",
3877
+ content: { [successContentType]: { schema: successSchema } }
3878
+ };
3879
+ const errorSchema = positionSchema(null, cs.error ?? null, components);
3880
+ const errorBody = {
3881
+ description: "Error response",
3882
+ content: { "application/json": { schema: errorSchema } }
3883
+ };
3884
+ if (cs.error || cs.errorRef) {
3885
+ responses["400"] = errorBody;
3886
+ responses.default = errorBody;
3887
+ } else {
3888
+ responses.default = {
3889
+ description: "Error response",
3890
+ content: { "application/json": { schema: {} } }
3891
+ };
3892
+ }
3893
+ return responses;
3894
+ }
3895
+ function buildOperation(route, components) {
3896
+ const cs = route.contract.contractSource;
3897
+ const op = {
3898
+ operationId: route.name,
3899
+ parameters: buildParameters(route),
3900
+ responses: buildResponses(cs, components)
3901
+ };
3902
+ const method = route.method.toUpperCase();
3903
+ const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
3904
+ if (hasBody && (cs.bodySchema || cs.body)) {
3905
+ const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
3906
+ op.requestBody = {
3907
+ required: true,
3908
+ content: { "application/json": { schema: bodySchema } }
3909
+ };
3910
+ }
3911
+ return op;
3912
+ }
3913
+ function buildOpenApiSpec(routes, opts = {}) {
3914
+ const components = {};
3915
+ const paths = {};
3916
+ for (const route of routes) {
3917
+ if (!route.contract) continue;
3918
+ const oaPath = toOpenApiPath(route.path);
3919
+ const method = route.method.toLowerCase();
3920
+ let pathItem = paths[oaPath];
3921
+ if (!pathItem) {
3922
+ pathItem = {};
3923
+ paths[oaPath] = pathItem;
3924
+ }
3925
+ pathItem[method] = buildOperation(route, components);
3926
+ }
3927
+ const info = opts.info ?? {};
3928
+ const doc = {
3929
+ openapi: "3.1.0",
3930
+ info: {
3931
+ title: info.title ?? "NestJS API",
3932
+ version: info.version ?? "1.0.0",
3933
+ ...info.description ? { description: info.description } : {}
3934
+ },
3935
+ paths,
3936
+ components: { schemas: components }
3937
+ };
3938
+ return doc;
3939
+ }
3940
+ async function emitOpenApi(routes, outDir, opts = {}) {
3941
+ await mkdir6(outDir, { recursive: true });
3942
+ const doc = buildOpenApiSpec(routes, opts);
3943
+ const fileName = opts.fileName ?? "openapi.json";
3944
+ await writeFile6(join10(outDir, fileName), `${JSON.stringify(doc, null, 2)}
3945
+ `, "utf8");
3946
+ }
3947
+
3948
+ // src/emit/emit-pages.ts
3949
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
3950
+ import { join as join11, relative as relative5 } from "path";
3951
+ async function emitPages(pages, outDir, _options = {}) {
3952
+ await mkdir7(outDir, { recursive: true });
3060
3953
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
3061
3954
  const augBody = pages.map((p) => {
3062
3955
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
@@ -3075,7 +3968,7 @@ ${augBody}
3075
3968
  }
3076
3969
  ${sharedPropsBlock}}
3077
3970
  `;
3078
- await writeFile5(join9(outDir, "pages.d.ts"), content, "utf8");
3971
+ await writeFile7(join11(outDir, "pages.d.ts"), content, "utf8");
3079
3972
  }
3080
3973
  function buildSharedPropsBlock(sharedProps) {
3081
3974
  if (!sharedProps) return "";
@@ -3105,12 +3998,12 @@ function needsQuotes(name) {
3105
3998
  }
3106
3999
 
3107
4000
  // src/emit/emit-routes.ts
3108
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
3109
- import { join as join10 } from "path";
4001
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
4002
+ import { join as join12 } from "path";
3110
4003
  async function emitRoutes(routes, outDir) {
3111
- await mkdir6(outDir, { recursive: true });
4004
+ await mkdir8(outDir, { recursive: true });
3112
4005
  const content = buildRoutesFile(routes);
3113
- await writeFile6(join10(outDir, "routes.ts"), content, "utf8");
4006
+ await writeFile8(join12(outDir, "routes.ts"), content, "utf8");
3114
4007
  }
3115
4008
  function buildRoutesFile(routes) {
3116
4009
  if (routes.length === 0) {
@@ -3258,21 +4151,38 @@ async function generate(config, inputRoutes = []) {
3258
4151
  });
3259
4152
  }
3260
4153
  const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
4154
+ if (hasContracts && config.openapi.enabled) {
4155
+ await emitOpenApi(routes, config.codegen.outDir, {
4156
+ fileName: config.openapi.fileName,
4157
+ info: {
4158
+ title: config.openapi.title,
4159
+ version: config.openapi.version,
4160
+ ...config.openapi.description ? { description: config.openapi.description } : {}
4161
+ }
4162
+ });
4163
+ }
4164
+ if (hasContracts && config.mocks.enabled) {
4165
+ await emitMocks(routes, config.codegen.outDir, {
4166
+ fileName: config.mocks.fileName,
4167
+ seed: config.mocks.seed,
4168
+ baseUrl: config.mocks.baseUrl
4169
+ });
4170
+ }
3261
4171
  await emitIndex(config.codegen.outDir, hasContracts, hasForms);
3262
4172
  if (extensions.length > 0) {
3263
4173
  const extraFiles = await collectEmittedFiles(extensions, ctx);
3264
4174
  for (const file of extraFiles) {
3265
- const dest = join11(config.codegen.outDir, file.path);
3266
- await mkdir7(dirname3(dest), { recursive: true });
3267
- await writeFile7(dest, file.contents, "utf8");
4175
+ const dest = join13(config.codegen.outDir, file.path);
4176
+ await mkdir9(dirname3(dest), { recursive: true });
4177
+ await writeFile9(dest, file.contents, "utf8");
3268
4178
  }
3269
4179
  }
3270
4180
  }
3271
4181
 
3272
4182
  // src/watch/lock-file.ts
3273
4183
  import { open } from "fs/promises";
3274
- import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3275
- import { join as join12 } from "path";
4184
+ import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
4185
+ import { join as join14 } from "path";
3276
4186
  var LOCK_FILE = ".watcher.lock";
3277
4187
  function isProcessAlive(pid) {
3278
4188
  try {
@@ -3283,8 +4193,8 @@ function isProcessAlive(pid) {
3283
4193
  }
3284
4194
  }
3285
4195
  async function acquireLock(outDir) {
3286
- await mkdir8(outDir, { recursive: true });
3287
- const lockPath = join12(outDir, LOCK_FILE);
4196
+ await mkdir10(outDir, { recursive: true });
4197
+ const lockPath = join14(outDir, LOCK_FILE);
3288
4198
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3289
4199
  try {
3290
4200
  const fd = await open(lockPath, "wx");
@@ -3324,7 +4234,7 @@ async function watch(config, onChange) {
3324
4234
  if (lock === null) {
3325
4235
  let holderPid = "unknown";
3326
4236
  try {
3327
- const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
4237
+ const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
3328
4238
  const data = JSON.parse(raw);
3329
4239
  if (data.pid !== void 0) holderPid = String(data.pid);
3330
4240
  } catch {
@@ -3334,12 +4244,20 @@ async function watch(config, onChange) {
3334
4244
  );
3335
4245
  return NO_OP_WATCHER;
3336
4246
  }
4247
+ let discovery = null;
4248
+ async function getDiscovery() {
4249
+ if (discovery === null) {
4250
+ discovery = await PersistentDiscovery.create({
4251
+ cwd: config.codegen.cwd,
4252
+ glob: config.contracts.glob,
4253
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4254
+ });
4255
+ return discovery;
4256
+ }
4257
+ return discovery;
4258
+ }
3337
4259
  try {
3338
- const initialRoutes = await discoverContractsFast({
3339
- cwd: config.codegen.cwd,
3340
- glob: config.contracts.glob,
3341
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3342
- });
4260
+ const initialRoutes = (await getDiscovery()).discover();
3343
4261
  await generate(config, initialRoutes);
3344
4262
  } catch (err) {
3345
4263
  console.warn(
@@ -3352,7 +4270,7 @@ async function watch(config, onChange) {
3352
4270
  }
3353
4271
  let pagesDebounceTimer;
3354
4272
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3355
- const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
4273
+ const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
3356
4274
  ignoreInitial: true,
3357
4275
  persistent: true,
3358
4276
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3378,23 +4296,23 @@ async function watch(config, onChange) {
3378
4296
  pagesWatcher.on("change", schedulePagesRegenerate);
3379
4297
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3380
4298
  let contractsDebounceTimer;
3381
- const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
4299
+ const pendingChangedPaths = /* @__PURE__ */ new Set();
4300
+ const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
3382
4301
  ignoreInitial: true,
3383
4302
  persistent: true,
3384
4303
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3385
4304
  });
3386
- function scheduleContractsRegenerate() {
4305
+ function scheduleContractsRegenerate(changedPath) {
4306
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3387
4307
  if (contractsDebounceTimer !== void 0) {
3388
4308
  clearTimeout(contractsDebounceTimer);
3389
4309
  }
3390
4310
  contractsDebounceTimer = setTimeout(async () => {
3391
4311
  contractsDebounceTimer = void 0;
4312
+ const changed = [...pendingChangedPaths];
4313
+ pendingChangedPaths.clear();
3392
4314
  try {
3393
- const routes = await discoverContractsFast({
3394
- cwd: config.codegen.cwd,
3395
- glob: config.contracts.glob,
3396
- ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3397
- });
4315
+ const routes = await (await getDiscovery()).rediscover(changed);
3398
4316
  await generate(config, routes);
3399
4317
  } catch (err) {
3400
4318
  console.error(
@@ -3405,17 +4323,17 @@ async function watch(config, onChange) {
3405
4323
  onChange?.();
3406
4324
  }, config.contracts.debounceMs);
3407
4325
  }
3408
- contractsWatcher.on("add", scheduleContractsRegenerate);
3409
- contractsWatcher.on("change", scheduleContractsRegenerate);
3410
- contractsWatcher.on("unlink", scheduleContractsRegenerate);
3411
- const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
4326
+ contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4327
+ contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4328
+ contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4329
+ const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
3412
4330
  ignoreInitial: true,
3413
4331
  persistent: true,
3414
4332
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3415
4333
  });
3416
- formsWatcher.on("add", scheduleContractsRegenerate);
3417
- formsWatcher.on("change", scheduleContractsRegenerate);
3418
- formsWatcher.on("unlink", scheduleContractsRegenerate);
4334
+ formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4335
+ formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4336
+ formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
3419
4337
  return {
3420
4338
  close: async () => {
3421
4339
  if (pagesDebounceTimer !== void 0) {