@dudousxd/nestjs-codegen 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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) {
@@ -453,7 +532,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
453
532
  emittedClasses: /* @__PURE__ */ new Map(),
454
533
  visiting: /* @__PURE__ */ new Set(),
455
534
  recursiveSchemas: /* @__PURE__ */ new Set(),
456
- depth: 0
535
+ depth: 0,
536
+ typeBindings: /* @__PURE__ */ new Map()
457
537
  };
458
538
  const root = buildObject(classDecl, sourceFile, ctx);
459
539
  return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
@@ -477,11 +557,34 @@ function buildProperty(prop, classFile, ctx) {
477
557
  const typeNode = prop.getTypeNode();
478
558
  const typeText = typeNode?.getText() ?? "unknown";
479
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
+ }
480
582
  const typeRefName = resolveTypeFactoryName(dec("Type"));
481
583
  if (has("ValidateNested") || typeRefName) {
584
+ const typeArgs = genericTypeArgNames(typeNode);
482
585
  const childName = typeRefName ?? singularClassName(typeText);
483
586
  if (childName) {
484
- const childNode = buildNestedReference(childName, classFile, ctx);
587
+ const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
485
588
  const wrapArray = has("IsArray") || isArrayType;
486
589
  const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
487
590
  return applyPresence(node2, decorators);
@@ -606,10 +709,12 @@ function baseFromType(typeText, isArrayType) {
606
709
  return { kind: "unknown" };
607
710
  }
608
711
  }
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);
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);
613
718
  ctx.recursiveSchemas.add(reserved);
614
719
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
615
720
  ctx.warnedDecorators.add(`recursive:${reserved}`);
@@ -628,19 +733,27 @@ function buildNestedReference(className, fromFile, ctx) {
628
733
  }
629
734
  return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
630
735
  }
631
- const existing = ctx.emittedClasses.get(className);
736
+ const existing = ctx.emittedClasses.get(cacheKey);
632
737
  if (existing) return { kind: "ref", name: existing };
633
- const schemaName = aliasFor(className, ctx);
738
+ const schemaName = aliasFor(schemaBase, ctx);
634
739
  const resolved = findType(className, fromFile, ctx.project);
635
740
  if (!resolved || resolved.kind !== "class") {
636
741
  return { kind: "object", fields: [], passthrough: true };
637
742
  }
638
- ctx.emittedClasses.set(className, schemaName);
639
- 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);
640
752
  ctx.depth += 1;
641
753
  const childNode = buildObject(resolved.decl, resolved.file, ctx);
642
754
  ctx.depth -= 1;
643
- ctx.visiting.delete(className);
755
+ ctx.visiting.delete(cacheKey);
756
+ for (const [k] of newBindings) ctx.typeBindings.delete(k);
644
757
  ctx.named.set(schemaName, childNode);
645
758
  return { kind: "ref", name: schemaName };
646
759
  }
@@ -687,6 +800,39 @@ function messageRaw(decorator) {
687
800
  }
688
801
  return void 0;
689
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
+ }
690
836
  function resolveTypeFactoryName(decorator) {
691
837
  const arg = firstArg(decorator);
692
838
  if (!arg) return null;
@@ -700,6 +846,17 @@ function singularClassName(typeText) {
700
846
  const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
701
847
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
702
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
+ }
703
860
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
704
861
  const arg = firstArg(decorator);
705
862
  if (!arg) return null;
@@ -759,6 +916,9 @@ import {
759
916
 
760
917
  // src/discovery/enum-resolution.ts
761
918
  var _enumCache = /* @__PURE__ */ new WeakMap();
919
+ function clearEnumCache(project) {
920
+ _enumCache.delete(project);
921
+ }
762
922
  function resolveEnumValues(name, sourceFile, project) {
763
923
  let byKey = _enumCache.get(project);
764
924
  if (byKey === void 0) {
@@ -1227,24 +1387,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
1227
1387
  "Map",
1228
1388
  "Set"
1229
1389
  ]);
1230
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1390
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
1231
1391
  if (depth <= 0) return "unknown";
1232
1392
  if (Node5.isArrayTypeNode(typeNode)) {
1233
1393
  const elementType = typeNode.getElementTypeNode();
1234
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
1394
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
1235
1395
  }
1236
1396
  if (Node5.isUnionTypeNode(typeNode)) {
1237
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
1397
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
1238
1398
  }
1239
1399
  if (Node5.isIntersectionTypeNode(typeNode)) {
1240
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
1400
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
1241
1401
  }
1242
1402
  if (Node5.isParenthesizedTypeNode(typeNode)) {
1243
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
1403
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
1244
1404
  }
1245
1405
  if (Node5.isTypeReference(typeNode)) {
1246
1406
  const typeName = typeNode.getTypeName();
1247
1407
  const name = Node5.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
1408
+ const bound = subst.get(name);
1409
+ if (bound !== void 0) return bound;
1248
1410
  if (name === "string" || name === "number" || name === "boolean") return name;
1249
1411
  if (name === "Date") return "string";
1250
1412
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
@@ -1252,14 +1414,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1252
1414
  return "unknown";
1253
1415
  const wrapperMode = WRAPPER_TYPES[name];
1254
1416
  if (wrapperMode) {
1255
- return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
1417
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
1256
1418
  }
1257
1419
  if (PASSTHROUGH_UTILITY.has(name)) {
1258
1420
  return typeNode.getText();
1259
1421
  }
1260
1422
  const resolved = findType(name, sourceFile, project);
1261
1423
  if (resolved) {
1262
- return expandTypeDecl(resolved, project, depth - 1);
1424
+ const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
1425
+ return expandTypeDecl(resolved, project, depth - 1, childSubst);
1263
1426
  }
1264
1427
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1265
1428
  return "unknown";
@@ -1272,32 +1435,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1272
1435
  if (kind === SyntaxKind2.AnyKeyword) return "unknown";
1273
1436
  return typeNode.getText();
1274
1437
  }
1275
- function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
1438
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
1276
1439
  const typeArgs = typeNode.getTypeArguments();
1277
1440
  const firstTypeArg = typeArgs[0];
1278
1441
  if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1279
- const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
1442
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
1280
1443
  return mode === "arrayOf" ? `Array<${inner}>` : inner;
1281
1444
  }
1282
1445
  return mode === "arrayOf" ? "Array<unknown>" : "unknown";
1283
1446
  }
1284
- 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()) {
1285
1461
  if (depth < 0) return "unknown";
1286
1462
  switch (result.kind) {
1287
1463
  case "class":
1288
- return resolvePropertied(result.decl, result.file, project, depth);
1464
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
1289
1465
  case "interface":
1290
- return resolvePropertied(result.decl, result.file, project, depth);
1466
+ return resolvePropertied(result.decl, result.file, project, depth, subst);
1291
1467
  case "typeAlias":
1292
1468
  if (result.typeNode) {
1293
- return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
1469
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
1294
1470
  }
1295
1471
  return result.text;
1296
1472
  case "enum":
1297
1473
  return result.members.join(" | ");
1298
1474
  }
1299
1475
  }
1300
- function resolvePropertied(decl, sourceFile, project, depth) {
1476
+ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
1301
1477
  if (depth < 0) return "unknown";
1302
1478
  const lines = [];
1303
1479
  for (const prop of decl.getProperties()) {
@@ -1306,7 +1482,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
1306
1482
  const propTypeNode = prop.getTypeNode();
1307
1483
  let propType = "unknown";
1308
1484
  if (propTypeNode) {
1309
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
1485
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
1310
1486
  }
1311
1487
  lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
1312
1488
  }
@@ -1355,7 +1531,7 @@ function extractParamsType(method, sourceFile, project) {
1355
1531
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
1356
1532
  }
1357
1533
  function extractResponseType(method, sourceFile, project) {
1358
- const apiResponseDecorator = method.getDecorator("ApiResponse");
1534
+ const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
1359
1535
  if (apiResponseDecorator) {
1360
1536
  const args = apiResponseDecorator.getArguments();
1361
1537
  const optsArg = args[0];
@@ -1384,6 +1560,59 @@ function extractResponseType(method, sourceFile, project) {
1384
1560
  }
1385
1561
  return "unknown";
1386
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
+ }
1387
1616
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
1388
1617
  if (!Node5.isIdentifier(node)) return "unknown";
1389
1618
  const name = node.getText();
@@ -1399,17 +1628,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
1399
1628
  unwrapContainers: true
1400
1629
  });
1401
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
+ }
1402
1663
  function extractDtoContract(method, sourceFile, project) {
1403
1664
  let body = extractBodyType(method, sourceFile, project);
1404
1665
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
1405
1666
  const query = extractQueryType(method, sourceFile, project);
1667
+ const streamElement = detectStreamElement(method);
1668
+ const isStream = streamElement !== null;
1406
1669
  if (filterInfo && filterInfo.source === "body") {
1407
1670
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
1408
1671
  body = body ?? bodyType;
1409
1672
  }
1410
1673
  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) {
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) {
1413
1677
  return null;
1414
1678
  }
1415
1679
  let bodyRef = null;
@@ -1423,12 +1687,12 @@ function extractDtoContract(method, sourceFile, project) {
1423
1687
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
1424
1688
  }
1425
1689
  }
1426
- const returnTypeNode = method.getReturnTypeNode();
1690
+ const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
1427
1691
  if (returnTypeNode) {
1428
1692
  responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
1429
1693
  }
1430
- if (!responseRef) {
1431
- const apiResp = method.getDecorator("ApiResponse");
1694
+ if (!responseRef && !isStream) {
1695
+ const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
1432
1696
  if (apiResp) {
1433
1697
  const args = apiResp.getArguments();
1434
1698
  const optsArg = args[0];
@@ -1470,16 +1734,19 @@ function extractDtoContract(method, sourceFile, project) {
1470
1734
  query,
1471
1735
  body,
1472
1736
  response,
1737
+ error: errorInfo?.type ?? null,
1473
1738
  params: paramsType,
1474
1739
  queryRef,
1475
1740
  bodyRef,
1476
1741
  responseRef,
1742
+ errorRef: errorInfo?.ref ?? null,
1477
1743
  filterFields: filterInfo?.fieldNames ?? null,
1478
1744
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
1479
1745
  filterSource: filterInfo?.source ?? null,
1480
1746
  formWarnings,
1481
1747
  bodySchema,
1482
- querySchema
1748
+ querySchema,
1749
+ stream: isStream
1483
1750
  };
1484
1751
  }
1485
1752
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -1597,6 +1864,7 @@ function parseDefineContractCall(callExpr) {
1597
1864
  let query = null;
1598
1865
  let body = null;
1599
1866
  let response = "unknown";
1867
+ let error = null;
1600
1868
  let bodyZodText = null;
1601
1869
  let queryZodText = null;
1602
1870
  for (const prop of optsArg.getProperties()) {
@@ -1612,25 +1880,27 @@ function parseDefineContractCall(callExpr) {
1612
1880
  bodyZodText = val.getText();
1613
1881
  } else if (propName === "response") {
1614
1882
  response = zodAstToTs(val);
1883
+ } else if (propName === "error") {
1884
+ error = zodAstToTs(val);
1615
1885
  }
1616
1886
  }
1617
- return { query, body, response, bodyZodText, queryZodText };
1887
+ return { query, body, response, error, bodyZodText, queryZodText };
1618
1888
  }
1619
1889
 
1620
1890
  // 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;
1891
+ function resolveTsconfigPath(cwd, tsconfig) {
1892
+ return tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
1893
+ }
1894
+ function createDiscoveryProject(tsconfigPath) {
1625
1895
  try {
1626
- project = new Project({
1896
+ return new Project({
1627
1897
  tsConfigFilePath: tsconfigPath,
1628
1898
  skipAddingFilesFromTsConfig: true,
1629
1899
  skipLoadingLibFiles: true,
1630
1900
  skipFileDependencyResolution: true
1631
1901
  });
1632
1902
  } catch {
1633
- project = new Project({
1903
+ return new Project({
1634
1904
  skipAddingFilesFromTsConfig: true,
1635
1905
  skipLoadingLibFiles: true,
1636
1906
  skipFileDependencyResolution: true,
@@ -1641,20 +1911,98 @@ async function discoverContractsFast(opts) {
1641
1911
  }
1642
1912
  });
1643
1913
  }
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 = [];
1914
+ }
1915
+ function bindDiscoveryContext(project, cwd, tsconfigPath) {
1649
1916
  setDiscoveryContext(project, {
1650
1917
  projectRoot: cwd,
1651
1918
  tsconfigPaths: loadTsconfigPaths(tsconfigPath)
1652
1919
  });
1653
- for (const sourceFile of project.getSourceFiles()) {
1654
- 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));
1655
1926
  }
1656
1927
  return routes;
1657
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
+ };
1658
2006
  function decoratorStringArg(decoratorExpr) {
1659
2007
  if (!decoratorExpr) return void 0;
1660
2008
  if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
@@ -1710,6 +2058,11 @@ function resolveVerb(method) {
1710
2058
  return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
1711
2059
  }
1712
2060
  }
2061
+ const sseDecorator = method.getDecorator("Sse");
2062
+ if (sseDecorator) {
2063
+ const pathArg = sseDecorator.getArguments()[0];
2064
+ return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
2065
+ }
1713
2066
  return null;
1714
2067
  }
1715
2068
  function readAsDecorator(node, label) {
@@ -1752,7 +2105,17 @@ function buildRoute(args) {
1752
2105
  };
1753
2106
  }
1754
2107
  function extractContractRoute(args) {
1755
- 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;
1756
2119
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
1757
2120
  if (!firstDecoratorArg) return null;
1758
2121
  let contractDef = null;
@@ -1762,18 +2125,19 @@ function extractContractRoute(args) {
1762
2125
  contractDef = parseDefineContractCall(firstDecoratorArg);
1763
2126
  } else if (Node7.isIdentifier(firstDecoratorArg)) {
1764
2127
  const identName = firstDecoratorArg.getText();
1765
- const varDecl = sourceFile.getVariableDeclaration(identName);
1766
- if (!varDecl) {
2128
+ const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
2129
+ if (!resolvedVar) {
1767
2130
  console.warn(
1768
- `[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`
1769
2132
  );
1770
2133
  return null;
1771
2134
  }
2135
+ const { decl: varDecl, file: declFile } = resolvedVar;
1772
2136
  const initializer = varDecl.getInitializer();
1773
2137
  if (!initializer) return null;
1774
2138
  contractDef = parseDefineContractCall(initializer);
1775
2139
  if (contractDef && varDecl.isExported()) {
1776
- const filePath = sourceFile.getFilePath();
2140
+ const filePath = declFile.getFilePath();
1777
2141
  if (contractDef.body !== null) {
1778
2142
  bodyZodRef = { name: `${identName}.body`, filePath };
1779
2143
  }
@@ -1806,6 +2170,7 @@ function extractContractRoute(args) {
1806
2170
  query: contractDef.query,
1807
2171
  body: contractDef.body,
1808
2172
  response: contractDef.response,
2173
+ error: contractDef.error,
1809
2174
  // Path A: capture both the importable ref and the raw text. The emitter
1810
2175
  // prefers inlining the text (client-safe — re-exporting from a controller
1811
2176
  // would drag server-only deps into the client bundle).
@@ -1837,15 +2202,18 @@ function extractDtoRoute(args) {
1837
2202
  query: dtoContract?.query ?? null,
1838
2203
  body: dtoContract?.body ?? null,
1839
2204
  response: dtoContract?.response ?? "unknown",
2205
+ error: dtoContract?.error ?? null,
1840
2206
  queryRef: dtoContract?.queryRef ?? null,
1841
2207
  bodyRef: dtoContract?.bodyRef ?? null,
1842
2208
  responseRef: dtoContract?.responseRef ?? null,
2209
+ errorRef: dtoContract?.errorRef ?? null,
1843
2210
  filterFields: dtoContract?.filterFields ?? null,
1844
2211
  filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
1845
2212
  filterSource: dtoContract?.filterSource ?? null,
1846
2213
  formWarnings: dtoContract?.formWarnings ?? [],
1847
2214
  bodySchema: dtoContract?.bodySchema ?? null,
1848
- querySchema: dtoContract?.querySchema ?? null
2215
+ querySchema: dtoContract?.querySchema ?? null,
2216
+ stream: dtoContract?.stream ?? false
1849
2217
  }
1850
2218
  });
1851
2219
  }
@@ -1869,6 +2237,7 @@ function extractFromSourceFile(sourceFile, project) {
1869
2237
  prefix,
1870
2238
  className,
1871
2239
  sourceFile,
2240
+ project,
1872
2241
  seenNames
1873
2242
  }) : extractDtoRoute({
1874
2243
  cls,
@@ -1887,8 +2256,8 @@ function extractFromSourceFile(sourceFile, project) {
1887
2256
  }
1888
2257
 
1889
2258
  // src/generate.ts
1890
- import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1891
- 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";
1892
2261
 
1893
2262
  // src/discovery/pages.ts
1894
2263
  import { readFile } from "fs/promises";
@@ -2417,17 +2786,28 @@ function emitFilterQueryType(c) {
2417
2786
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
2418
2787
  }
2419
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
+ }
2420
2794
  if (c.controllerRef) {
2421
2795
  let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
2422
2796
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
2423
2797
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
2424
2798
  }
2425
- const respRef = c.contractSource.responseRef;
2426
2799
  if (respRef) {
2427
2800
  return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
2428
2801
  }
2429
2802
  return c.contractSource.response;
2430
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
+ }
2431
2811
  function emitRouterTypeBlock(tree, indent, outDir) {
2432
2812
  const pad = " ".repeat(indent);
2433
2813
  const lines = [];
@@ -2442,12 +2822,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
2442
2822
  const bodyRef = c.contractSource.bodyRef;
2443
2823
  const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
2444
2824
  const response = buildResponseType(c, outDir);
2825
+ const error = buildErrorType(c);
2445
2826
  const params = buildParamsType(c.params);
2446
2827
  const safeMethod = JSON.stringify(method);
2447
2828
  const safeUrl = JSON.stringify(c.path);
2448
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";
2449
2831
  lines.push(
2450
- `${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} };`
2451
2833
  );
2452
2834
  } else {
2453
2835
  lines.push(`${pad}${objKey}: {`);
@@ -2519,15 +2901,21 @@ function emitReqHelper() {
2519
2901
  ""
2520
2902
  ];
2521
2903
  }
2522
- function renderLeaf(pad, objKey, req, requestExpr, members) {
2904
+ function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
2523
2905
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
2524
2906
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
2907
+ if (streamExpr) {
2908
+ lines.push(`${pad} stream: () => ${streamExpr},`);
2909
+ }
2525
2910
  for (const [name, value] of Object.entries(members)) {
2526
2911
  lines.push(`${pad} ${name}: ${value},`);
2527
2912
  }
2528
2913
  lines.push(`${pad}}),`);
2529
2914
  return lines;
2530
2915
  }
2916
+ function renderStreamExpr(req) {
2917
+ return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
2918
+ }
2531
2919
  function emitApiObjectBlock(tree, indent, p) {
2532
2920
  const pad = " ".repeat(indent);
2533
2921
  const lines = [];
@@ -2562,7 +2950,8 @@ function emitApiObjectBlock(tree, indent, p) {
2562
2950
  }
2563
2951
  const members = {};
2564
2952
  for (const [name, { value }] of owned) members[name] = value;
2565
- 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));
2566
2955
  }
2567
2956
  return lines;
2568
2957
  }
@@ -2600,6 +2989,8 @@ var ROUTE_NAMESPACE = [
2600
2989
  ' export type Params<K extends string> = ResolveByName<K, "params">;',
2601
2990
  ' export type Error<K extends string> = ResolveByName<K, "error">;',
2602
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">;',
2603
2994
  " export type Request<K extends string> = {",
2604
2995
  " body: Body<K>;",
2605
2996
  " query: Query<K>;",
@@ -2616,6 +3007,7 @@ var PATH_NAMESPACE = [
2616
3007
  ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
2617
3008
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
2618
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">;',
2619
3011
  "}",
2620
3012
  ""
2621
3013
  ];
@@ -2627,6 +3019,7 @@ var EMPTY_ROUTE_NAMESPACE = [
2627
3019
  " export type Params<K extends string> = never;",
2628
3020
  " export type Error<K extends string> = never;",
2629
3021
  " export type FilterFields<K extends string> = never;",
3022
+ " export type Stream<K extends string> = never;",
2630
3023
  " export type Request<K extends string> = { body: never; query: never; params: never };",
2631
3024
  "}",
2632
3025
  ""
@@ -2639,6 +3032,7 @@ var EMPTY_PATH_NAMESPACE = [
2639
3032
  " export type Params<M extends string, U extends string> = never;",
2640
3033
  " export type Error<M extends string, U extends string> = never;",
2641
3034
  " export type FilterFields<M extends string, U extends string> = never;",
3035
+ " export type Stream<M extends string, U extends string> = never;",
2642
3036
  "}",
2643
3037
  ""
2644
3038
  ];
@@ -2662,7 +3056,7 @@ function buildApiFile(routes, outDir, opts = {}) {
2662
3056
  for (const r of contracted) {
2663
3057
  const cs = r.contract?.contractSource;
2664
3058
  if (!cs) continue;
2665
- 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];
2666
3060
  for (const ref of refs) {
2667
3061
  if (!ref) continue;
2668
3062
  let names = importsByFile.get(ref.filePath);
@@ -3095,11 +3489,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
3095
3489
  await writeFile4(join8(outDir, "index.d.ts"), content, "utf8");
3096
3490
  }
3097
3491
 
3098
- // src/emit/emit-pages.ts
3492
+ // src/emit/emit-mocks.ts
3099
3493
  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 = {}) {
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 = {}) {
3102
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 });
3103
3953
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
3104
3954
  const augBody = pages.map((p) => {
3105
3955
  const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
@@ -3118,7 +3968,7 @@ ${augBody}
3118
3968
  }
3119
3969
  ${sharedPropsBlock}}
3120
3970
  `;
3121
- await writeFile5(join9(outDir, "pages.d.ts"), content, "utf8");
3971
+ await writeFile7(join11(outDir, "pages.d.ts"), content, "utf8");
3122
3972
  }
3123
3973
  function buildSharedPropsBlock(sharedProps) {
3124
3974
  if (!sharedProps) return "";
@@ -3148,12 +3998,12 @@ function needsQuotes(name) {
3148
3998
  }
3149
3999
 
3150
4000
  // src/emit/emit-routes.ts
3151
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
3152
- import { join as join10 } from "path";
4001
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
4002
+ import { join as join12 } from "path";
3153
4003
  async function emitRoutes(routes, outDir) {
3154
- await mkdir6(outDir, { recursive: true });
4004
+ await mkdir8(outDir, { recursive: true });
3155
4005
  const content = buildRoutesFile(routes);
3156
- await writeFile6(join10(outDir, "routes.ts"), content, "utf8");
4006
+ await writeFile8(join12(outDir, "routes.ts"), content, "utf8");
3157
4007
  }
3158
4008
  function buildRoutesFile(routes) {
3159
4009
  if (routes.length === 0) {
@@ -3301,21 +4151,38 @@ async function generate(config, inputRoutes = []) {
3301
4151
  });
3302
4152
  }
3303
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
+ }
3304
4171
  await emitIndex(config.codegen.outDir, hasContracts, hasForms);
3305
4172
  if (extensions.length > 0) {
3306
4173
  const extraFiles = await collectEmittedFiles(extensions, ctx);
3307
4174
  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");
4175
+ const dest = join13(config.codegen.outDir, file.path);
4176
+ await mkdir9(dirname3(dest), { recursive: true });
4177
+ await writeFile9(dest, file.contents, "utf8");
3311
4178
  }
3312
4179
  }
3313
4180
  }
3314
4181
 
3315
4182
  // src/watch/lock-file.ts
3316
4183
  import { open } from "fs/promises";
3317
- import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3318
- 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";
3319
4186
  var LOCK_FILE = ".watcher.lock";
3320
4187
  function isProcessAlive(pid) {
3321
4188
  try {
@@ -3326,8 +4193,8 @@ function isProcessAlive(pid) {
3326
4193
  }
3327
4194
  }
3328
4195
  async function acquireLock(outDir) {
3329
- await mkdir8(outDir, { recursive: true });
3330
- const lockPath = join12(outDir, LOCK_FILE);
4196
+ await mkdir10(outDir, { recursive: true });
4197
+ const lockPath = join14(outDir, LOCK_FILE);
3331
4198
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3332
4199
  try {
3333
4200
  const fd = await open(lockPath, "wx");
@@ -3367,7 +4234,7 @@ async function watch(config, onChange) {
3367
4234
  if (lock === null) {
3368
4235
  let holderPid = "unknown";
3369
4236
  try {
3370
- const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
4237
+ const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
3371
4238
  const data = JSON.parse(raw);
3372
4239
  if (data.pid !== void 0) holderPid = String(data.pid);
3373
4240
  } catch {
@@ -3377,12 +4244,20 @@ async function watch(config, onChange) {
3377
4244
  );
3378
4245
  return NO_OP_WATCHER;
3379
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
+ }
3380
4259
  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
- });
4260
+ const initialRoutes = (await getDiscovery()).discover();
3386
4261
  await generate(config, initialRoutes);
3387
4262
  } catch (err) {
3388
4263
  console.warn(
@@ -3395,7 +4270,7 @@ async function watch(config, onChange) {
3395
4270
  }
3396
4271
  let pagesDebounceTimer;
3397
4272
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3398
- const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
4273
+ const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
3399
4274
  ignoreInitial: true,
3400
4275
  persistent: true,
3401
4276
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3421,23 +4296,23 @@ async function watch(config, onChange) {
3421
4296
  pagesWatcher.on("change", schedulePagesRegenerate);
3422
4297
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3423
4298
  let contractsDebounceTimer;
3424
- 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), {
3425
4301
  ignoreInitial: true,
3426
4302
  persistent: true,
3427
4303
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3428
4304
  });
3429
- function scheduleContractsRegenerate() {
4305
+ function scheduleContractsRegenerate(changedPath) {
4306
+ if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
3430
4307
  if (contractsDebounceTimer !== void 0) {
3431
4308
  clearTimeout(contractsDebounceTimer);
3432
4309
  }
3433
4310
  contractsDebounceTimer = setTimeout(async () => {
3434
4311
  contractsDebounceTimer = void 0;
4312
+ const changed = [...pendingChangedPaths];
4313
+ pendingChangedPaths.clear();
3435
4314
  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
- });
4315
+ const routes = await (await getDiscovery()).rediscover(changed);
3441
4316
  await generate(config, routes);
3442
4317
  } catch (err) {
3443
4318
  console.error(
@@ -3448,17 +4323,17 @@ async function watch(config, onChange) {
3448
4323
  onChange?.();
3449
4324
  }, config.contracts.debounceMs);
3450
4325
  }
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), {
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), {
3455
4330
  ignoreInitial: true,
3456
4331
  persistent: true,
3457
4332
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3458
4333
  });
3459
- formsWatcher.on("add", scheduleContractsRegenerate);
3460
- formsWatcher.on("change", scheduleContractsRegenerate);
3461
- 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));
3462
4337
  return {
3463
4338
  close: async () => {
3464
4339
  if (pagesDebounceTimer !== void 0) {