@dudousxd/nestjs-codegen 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/cli/main.cjs +1080 -160
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1063 -143
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/{index-DA4uySjo.d.cts → index-B0mS84Jj.d.cts} +83 -1
- package/dist/{index-DA4uySjo.d.ts → index-B0mS84Jj.d.ts} +83 -1
- package/dist/index.cjs +1053 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -4
- package/dist/index.d.ts +104 -4
- package/dist/index.js +1034 -105
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +1015 -113
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +1 -1
- package/dist/nest/index.d.ts +1 -1
- package/dist/nest/index.js +1009 -107
- package/dist/nest/index.js.map +1 -1
- package/package.json +30 -11
package/dist/nest/index.cjs
CHANGED
|
@@ -154,13 +154,26 @@ function applyDefaults(userConfig, cwd) {
|
|
|
154
154
|
enabled: userConfig.forms?.enabled ?? true,
|
|
155
155
|
watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
|
|
156
156
|
zodImport: userConfig.forms?.zodImport ?? "zod"
|
|
157
|
+
},
|
|
158
|
+
openapi: {
|
|
159
|
+
enabled: userConfig.openapi?.enabled ?? false,
|
|
160
|
+
fileName: userConfig.openapi?.fileName ?? "openapi.json",
|
|
161
|
+
title: userConfig.openapi?.title ?? "NestJS API",
|
|
162
|
+
version: userConfig.openapi?.version ?? "1.0.0",
|
|
163
|
+
description: userConfig.openapi?.description ?? null
|
|
164
|
+
},
|
|
165
|
+
mocks: {
|
|
166
|
+
enabled: userConfig.mocks?.enabled ?? false,
|
|
167
|
+
fileName: userConfig.mocks?.fileName ?? "mocks.ts",
|
|
168
|
+
seed: userConfig.mocks?.seed ?? 1,
|
|
169
|
+
baseUrl: userConfig.mocks?.baseUrl ?? ""
|
|
157
170
|
}
|
|
158
171
|
};
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
// src/watch/watcher.ts
|
|
162
|
-
var
|
|
163
|
-
var
|
|
175
|
+
var import_promises14 = require("fs/promises");
|
|
176
|
+
var import_node_path16 = require("path");
|
|
164
177
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
165
178
|
|
|
166
179
|
// src/discovery/contracts-fast.ts
|
|
@@ -336,7 +349,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
|
|
|
336
349
|
}
|
|
337
350
|
return null;
|
|
338
351
|
}
|
|
352
|
+
function resolveImportedVariable(name, sourceFile, project) {
|
|
353
|
+
const local = sourceFile.getVariableDeclaration(name);
|
|
354
|
+
if (local) return { decl: local, file: sourceFile };
|
|
355
|
+
return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
|
|
356
|
+
}
|
|
357
|
+
function resolveVariableViaImports(name, sourceFile, project, seen) {
|
|
358
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
359
|
+
const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
|
|
360
|
+
if (!namedImport) continue;
|
|
361
|
+
const sourceName = namedImport.getName();
|
|
362
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
363
|
+
const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
|
|
364
|
+
if (found) return found;
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
|
|
369
|
+
const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
|
|
370
|
+
for (const candidate of candidates) {
|
|
371
|
+
let importedFile = project.getSourceFile(candidate);
|
|
372
|
+
if (!importedFile) {
|
|
373
|
+
try {
|
|
374
|
+
importedFile = project.addSourceFileAtPath(candidate);
|
|
375
|
+
} catch {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const found = resolveVariableInFile(name, importedFile, project, seen);
|
|
380
|
+
if (found) return found;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
function resolveVariableInFile(name, file, project, seen) {
|
|
385
|
+
const filePath = file.getFilePath();
|
|
386
|
+
if (seen.has(filePath)) return null;
|
|
387
|
+
seen.add(filePath);
|
|
388
|
+
const local = file.getVariableDeclaration(name);
|
|
389
|
+
if (local) return { decl: local, file };
|
|
390
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
391
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
392
|
+
const namedExports = exportDecl.getNamedExports();
|
|
393
|
+
if (moduleSpecifier) {
|
|
394
|
+
const hasStar = namedExports.length === 0;
|
|
395
|
+
const reExport2 = namedExports.find(
|
|
396
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
397
|
+
);
|
|
398
|
+
if (!hasStar && !reExport2) continue;
|
|
399
|
+
const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
|
|
400
|
+
const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
|
|
401
|
+
if (found) return found;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const reExport = namedExports.find(
|
|
405
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
406
|
+
);
|
|
407
|
+
if (!reExport) continue;
|
|
408
|
+
const sourceName = reExport.getName();
|
|
409
|
+
const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
|
|
410
|
+
if (viaImports) return viaImports;
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
339
414
|
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
415
|
+
function clearTypeResolutionCaches(project) {
|
|
416
|
+
_findTypeCache.delete(project);
|
|
417
|
+
_resolveNamedRefCache.delete(project);
|
|
418
|
+
}
|
|
340
419
|
function findType(name, sourceFile, project) {
|
|
341
420
|
let byKey = _findTypeCache.get(project);
|
|
342
421
|
if (byKey === void 0) {
|
|
@@ -477,9 +556,11 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
477
556
|
warnings: [],
|
|
478
557
|
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
479
558
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
559
|
+
usedSchemaNames: /* @__PURE__ */ new Set(),
|
|
480
560
|
visiting: /* @__PURE__ */ new Set(),
|
|
481
561
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
482
|
-
depth: 0
|
|
562
|
+
depth: 0,
|
|
563
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
483
564
|
};
|
|
484
565
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
485
566
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -503,11 +584,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
503
584
|
const typeNode = prop.getTypeNode();
|
|
504
585
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
505
586
|
const isArrayType = !!typeNode && import_ts_morph2.Node.isArrayTypeNode(typeNode);
|
|
587
|
+
const discriminator = resolveDiscriminator(dec("Type"));
|
|
588
|
+
if (discriminator) {
|
|
589
|
+
const options = discriminator.subTypes.map(
|
|
590
|
+
(name) => buildNestedReference(name, classFile, ctx)
|
|
591
|
+
);
|
|
592
|
+
const unionNode = {
|
|
593
|
+
kind: "union",
|
|
594
|
+
options,
|
|
595
|
+
discriminator: discriminator.property
|
|
596
|
+
};
|
|
597
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
598
|
+
const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
|
|
599
|
+
return applyPresence(node2, decorators);
|
|
600
|
+
}
|
|
601
|
+
const propTypeParam = singularClassName(typeText);
|
|
602
|
+
if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
|
|
603
|
+
const bound = ctx.typeBindings.get(propTypeParam);
|
|
604
|
+
const childNode = buildNestedReference(bound, classFile, ctx);
|
|
605
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
606
|
+
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
607
|
+
return applyPresence(node2, decorators);
|
|
608
|
+
}
|
|
506
609
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
507
610
|
if (has("ValidateNested") || typeRefName) {
|
|
611
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
508
612
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
509
613
|
if (childName) {
|
|
510
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
614
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
511
615
|
const wrapArray = has("IsArray") || isArrayType;
|
|
512
616
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
513
617
|
return applyPresence(node2, decorators);
|
|
@@ -632,10 +736,13 @@ function baseFromType(typeText, isArrayType) {
|
|
|
632
736
|
return { kind: "unknown" };
|
|
633
737
|
}
|
|
634
738
|
}
|
|
635
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
739
|
+
function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
|
|
740
|
+
const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
|
|
741
|
+
const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
|
|
742
|
+
if (ctx.visiting.has(cacheKey)) {
|
|
743
|
+
const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
|
|
744
|
+
ctx.emittedClasses.set(cacheKey, reserved);
|
|
745
|
+
ctx.usedSchemaNames.add(reserved);
|
|
639
746
|
ctx.recursiveSchemas.add(reserved);
|
|
640
747
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
641
748
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -654,29 +761,37 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
654
761
|
}
|
|
655
762
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
656
763
|
}
|
|
657
|
-
const existing = ctx.emittedClasses.get(
|
|
764
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
658
765
|
if (existing) return { kind: "ref", name: existing };
|
|
659
|
-
const schemaName = aliasFor(
|
|
766
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
660
767
|
const resolved = findType(className, fromFile, ctx.project);
|
|
661
768
|
if (!resolved || resolved.kind !== "class") {
|
|
662
769
|
return { kind: "object", fields: [], passthrough: true };
|
|
663
770
|
}
|
|
664
|
-
|
|
665
|
-
|
|
771
|
+
const params = resolved.decl.getTypeParameters().map((p) => p.getName());
|
|
772
|
+
const newBindings = [];
|
|
773
|
+
params.forEach((param, i) => {
|
|
774
|
+
const arg = typeArgs[i];
|
|
775
|
+
if (arg) newBindings.push([param, arg]);
|
|
776
|
+
});
|
|
777
|
+
for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
|
|
778
|
+
ctx.emittedClasses.set(cacheKey, schemaName);
|
|
779
|
+
ctx.usedSchemaNames.add(schemaName);
|
|
780
|
+
ctx.visiting.add(cacheKey);
|
|
666
781
|
ctx.depth += 1;
|
|
667
782
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
668
783
|
ctx.depth -= 1;
|
|
669
|
-
ctx.visiting.delete(
|
|
784
|
+
ctx.visiting.delete(cacheKey);
|
|
785
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
670
786
|
ctx.named.set(schemaName, childNode);
|
|
787
|
+
ctx.usedSchemaNames.add(schemaName);
|
|
671
788
|
return { kind: "ref", name: schemaName };
|
|
672
789
|
}
|
|
673
790
|
function aliasFor(className, ctx) {
|
|
674
791
|
const baseName = `${className}Schema`;
|
|
675
792
|
let candidate = baseName;
|
|
676
793
|
let i = 1;
|
|
677
|
-
|
|
678
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
679
|
-
while (used.has(candidate)) {
|
|
794
|
+
while (ctx.usedSchemaNames.has(candidate)) {
|
|
680
795
|
candidate = `${baseName}_${i}`;
|
|
681
796
|
i += 1;
|
|
682
797
|
}
|
|
@@ -713,6 +828,39 @@ function messageRaw(decorator) {
|
|
|
713
828
|
}
|
|
714
829
|
return void 0;
|
|
715
830
|
}
|
|
831
|
+
function resolveDiscriminator(decorator) {
|
|
832
|
+
const optsArg = decorator?.getArguments()[1];
|
|
833
|
+
if (!optsArg || !import_ts_morph2.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
834
|
+
let discProp;
|
|
835
|
+
for (const prop of optsArg.getProperties()) {
|
|
836
|
+
if (import_ts_morph2.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
|
|
837
|
+
discProp = prop.getInitializer();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (!discProp || !import_ts_morph2.Node.isObjectLiteralExpression(discProp)) return null;
|
|
841
|
+
let property = null;
|
|
842
|
+
const subTypes = [];
|
|
843
|
+
for (const prop of discProp.getProperties()) {
|
|
844
|
+
if (!import_ts_morph2.Node.isPropertyAssignment(prop)) continue;
|
|
845
|
+
const name = prop.getName();
|
|
846
|
+
const init = prop.getInitializer();
|
|
847
|
+
if (!init) continue;
|
|
848
|
+
if (name === "property" && import_ts_morph2.Node.isStringLiteral(init)) {
|
|
849
|
+
property = init.getLiteralValue();
|
|
850
|
+
} else if (name === "subTypes" && import_ts_morph2.Node.isArrayLiteralExpression(init)) {
|
|
851
|
+
for (const el of init.getElements()) {
|
|
852
|
+
if (!import_ts_morph2.Node.isObjectLiteralExpression(el)) continue;
|
|
853
|
+
for (const p of el.getProperties()) {
|
|
854
|
+
if (!import_ts_morph2.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
|
|
855
|
+
const nameInit = p.getInitializer();
|
|
856
|
+
if (nameInit && import_ts_morph2.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (!property || subTypes.length === 0) return null;
|
|
862
|
+
return { property, subTypes };
|
|
863
|
+
}
|
|
716
864
|
function resolveTypeFactoryName(decorator) {
|
|
717
865
|
const arg = firstArg(decorator);
|
|
718
866
|
if (!arg) return null;
|
|
@@ -726,6 +874,17 @@ function singularClassName(typeText) {
|
|
|
726
874
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
727
875
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
728
876
|
}
|
|
877
|
+
function genericTypeArgNames(typeNode) {
|
|
878
|
+
if (!typeNode || !import_ts_morph2.Node.isTypeReference(typeNode)) return [];
|
|
879
|
+
const names = [];
|
|
880
|
+
for (const arg of typeNode.getTypeArguments()) {
|
|
881
|
+
if (!import_ts_morph2.Node.isTypeReference(arg)) return [];
|
|
882
|
+
const tn = arg.getTypeName();
|
|
883
|
+
if (!import_ts_morph2.Node.isIdentifier(tn)) return [];
|
|
884
|
+
names.push(tn.getText());
|
|
885
|
+
}
|
|
886
|
+
return names;
|
|
887
|
+
}
|
|
729
888
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
730
889
|
const arg = firstArg(decorator);
|
|
731
890
|
if (!arg) return null;
|
|
@@ -780,6 +939,9 @@ var import_ts_morph3 = require("ts-morph");
|
|
|
780
939
|
|
|
781
940
|
// src/discovery/enum-resolution.ts
|
|
782
941
|
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
942
|
+
function clearEnumCache(project) {
|
|
943
|
+
_enumCache.delete(project);
|
|
944
|
+
}
|
|
783
945
|
function resolveEnumValues(name, sourceFile, project) {
|
|
784
946
|
let byKey = _enumCache.get(project);
|
|
785
947
|
if (byKey === void 0) {
|
|
@@ -1248,24 +1410,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
1248
1410
|
"Map",
|
|
1249
1411
|
"Set"
|
|
1250
1412
|
]);
|
|
1251
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
1413
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
1252
1414
|
if (depth <= 0) return "unknown";
|
|
1253
1415
|
if (import_ts_morph5.Node.isArrayTypeNode(typeNode)) {
|
|
1254
1416
|
const elementType = typeNode.getElementTypeNode();
|
|
1255
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
1417
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
1256
1418
|
}
|
|
1257
1419
|
if (import_ts_morph5.Node.isUnionTypeNode(typeNode)) {
|
|
1258
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
1420
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
1259
1421
|
}
|
|
1260
1422
|
if (import_ts_morph5.Node.isIntersectionTypeNode(typeNode)) {
|
|
1261
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
1423
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
1262
1424
|
}
|
|
1263
1425
|
if (import_ts_morph5.Node.isParenthesizedTypeNode(typeNode)) {
|
|
1264
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
1426
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
1265
1427
|
}
|
|
1266
1428
|
if (import_ts_morph5.Node.isTypeReference(typeNode)) {
|
|
1267
1429
|
const typeName = typeNode.getTypeName();
|
|
1268
1430
|
const name = import_ts_morph5.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
1431
|
+
const bound = subst.get(name);
|
|
1432
|
+
if (bound !== void 0) return bound;
|
|
1269
1433
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
1270
1434
|
if (name === "Date") return "string";
|
|
1271
1435
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -1273,14 +1437,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1273
1437
|
return "unknown";
|
|
1274
1438
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
1275
1439
|
if (wrapperMode) {
|
|
1276
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
1440
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
1277
1441
|
}
|
|
1278
1442
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
1279
1443
|
return typeNode.getText();
|
|
1280
1444
|
}
|
|
1281
1445
|
const resolved = findType(name, sourceFile, project);
|
|
1282
1446
|
if (resolved) {
|
|
1283
|
-
|
|
1447
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
1448
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
1284
1449
|
}
|
|
1285
1450
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
1286
1451
|
return "unknown";
|
|
@@ -1293,32 +1458,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1293
1458
|
if (kind === import_ts_morph5.SyntaxKind.AnyKeyword) return "unknown";
|
|
1294
1459
|
return typeNode.getText();
|
|
1295
1460
|
}
|
|
1296
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
1461
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
1297
1462
|
const typeArgs = typeNode.getTypeArguments();
|
|
1298
1463
|
const firstTypeArg = typeArgs[0];
|
|
1299
1464
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1300
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1465
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
1301
1466
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
1302
1467
|
}
|
|
1303
1468
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
1304
1469
|
}
|
|
1305
|
-
function
|
|
1470
|
+
function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
|
|
1471
|
+
if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
|
|
1472
|
+
const params = result.decl.getTypeParameters().map((p) => p.getName());
|
|
1473
|
+
if (params.length === 0) return /* @__PURE__ */ new Map();
|
|
1474
|
+
const args = typeNode.getTypeArguments();
|
|
1475
|
+
const subst = /* @__PURE__ */ new Map();
|
|
1476
|
+
params.forEach((param, i) => {
|
|
1477
|
+
const arg = args[i];
|
|
1478
|
+
if (arg)
|
|
1479
|
+
subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
|
|
1480
|
+
});
|
|
1481
|
+
return subst;
|
|
1482
|
+
}
|
|
1483
|
+
function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
1306
1484
|
if (depth < 0) return "unknown";
|
|
1307
1485
|
switch (result.kind) {
|
|
1308
1486
|
case "class":
|
|
1309
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
1487
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
1310
1488
|
case "interface":
|
|
1311
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
1489
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
1312
1490
|
case "typeAlias":
|
|
1313
1491
|
if (result.typeNode) {
|
|
1314
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
1492
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
1315
1493
|
}
|
|
1316
1494
|
return result.text;
|
|
1317
1495
|
case "enum":
|
|
1318
1496
|
return result.members.join(" | ");
|
|
1319
1497
|
}
|
|
1320
1498
|
}
|
|
1321
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
1499
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
1322
1500
|
if (depth < 0) return "unknown";
|
|
1323
1501
|
const lines = [];
|
|
1324
1502
|
for (const prop of decl.getProperties()) {
|
|
@@ -1327,7 +1505,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
1327
1505
|
const propTypeNode = prop.getTypeNode();
|
|
1328
1506
|
let propType = "unknown";
|
|
1329
1507
|
if (propTypeNode) {
|
|
1330
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
1508
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
1331
1509
|
}
|
|
1332
1510
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
1333
1511
|
}
|
|
@@ -1376,7 +1554,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
1376
1554
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
1377
1555
|
}
|
|
1378
1556
|
function extractResponseType(method, sourceFile, project) {
|
|
1379
|
-
const apiResponseDecorator = method.
|
|
1557
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
1380
1558
|
if (apiResponseDecorator) {
|
|
1381
1559
|
const args = apiResponseDecorator.getArguments();
|
|
1382
1560
|
const optsArg = args[0];
|
|
@@ -1405,6 +1583,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1405
1583
|
}
|
|
1406
1584
|
return "unknown";
|
|
1407
1585
|
}
|
|
1586
|
+
function apiResponseStatus(decorator) {
|
|
1587
|
+
const optsArg = decorator.getArguments()[0];
|
|
1588
|
+
if (!optsArg || !import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
1589
|
+
for (const prop of optsArg.getProperties()) {
|
|
1590
|
+
if (!import_ts_morph5.Node.isPropertyAssignment(prop)) continue;
|
|
1591
|
+
if (prop.getName() !== "status") continue;
|
|
1592
|
+
const val = prop.getInitializer();
|
|
1593
|
+
if (val && import_ts_morph5.Node.isNumericLiteral(val)) return Number(val.getLiteralValue());
|
|
1594
|
+
}
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
function apiResponseTypeNode(decorator) {
|
|
1598
|
+
const optsArg = decorator.getArguments()[0];
|
|
1599
|
+
if (!optsArg || !import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
1600
|
+
for (const prop of optsArg.getProperties()) {
|
|
1601
|
+
if (!import_ts_morph5.Node.isPropertyAssignment(prop)) continue;
|
|
1602
|
+
if (prop.getName() !== "type") continue;
|
|
1603
|
+
const val = prop.getInitializer();
|
|
1604
|
+
if (!val) return null;
|
|
1605
|
+
if (import_ts_morph5.Node.isArrayLiteralExpression(val)) {
|
|
1606
|
+
const first = val.getElements()[0];
|
|
1607
|
+
return first ? { node: first, isArray: true } : null;
|
|
1608
|
+
}
|
|
1609
|
+
return { node: val, isArray: false };
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
function extractErrorType(method, sourceFile, project) {
|
|
1614
|
+
for (const decorator of method.getDecorators()) {
|
|
1615
|
+
if (decorator.getName() !== "ApiResponse") continue;
|
|
1616
|
+
const status = apiResponseStatus(decorator);
|
|
1617
|
+
if (status === null || status < 400) continue;
|
|
1618
|
+
const typeInfo = apiResponseTypeNode(decorator);
|
|
1619
|
+
if (!typeInfo) continue;
|
|
1620
|
+
const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
|
|
1621
|
+
const type = typeInfo.isArray ? `Array<${inner}>` : inner;
|
|
1622
|
+
let ref = null;
|
|
1623
|
+
if (import_ts_morph5.Node.isIdentifier(typeInfo.node)) {
|
|
1624
|
+
const name = typeInfo.node.getText();
|
|
1625
|
+
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
1626
|
+
if (localDecl?.isExported()) {
|
|
1627
|
+
ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
|
|
1628
|
+
} else {
|
|
1629
|
+
const resolved = resolveImportedType(name, sourceFile, project);
|
|
1630
|
+
if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
|
|
1631
|
+
ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return { type, ref };
|
|
1636
|
+
}
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1408
1639
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
1409
1640
|
if (!import_ts_morph5.Node.isIdentifier(node)) return "unknown";
|
|
1410
1641
|
const name = node.getText();
|
|
@@ -1420,17 +1651,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
1420
1651
|
unwrapContainers: true
|
|
1421
1652
|
});
|
|
1422
1653
|
}
|
|
1654
|
+
var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
|
|
1655
|
+
var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
|
|
1656
|
+
var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
|
|
1657
|
+
function detectStreamElement(method) {
|
|
1658
|
+
const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
|
|
1659
|
+
let node = method.getReturnTypeNode();
|
|
1660
|
+
node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
|
|
1661
|
+
const containerEl = streamContainerElement(node);
|
|
1662
|
+
if (containerEl) {
|
|
1663
|
+
return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
|
|
1664
|
+
}
|
|
1665
|
+
if (hasSse) return node ?? null;
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
function streamContainerElement(node) {
|
|
1669
|
+
if (!node || !import_ts_morph5.Node.isTypeReference(node)) return null;
|
|
1670
|
+
const typeName = node.getTypeName();
|
|
1671
|
+
const name = import_ts_morph5.Node.isIdentifier(typeName) ? typeName.getText() : "";
|
|
1672
|
+
if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
|
|
1673
|
+
return node.getTypeArguments()[0] ?? null;
|
|
1674
|
+
}
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
function unwrapNamedContainer(node, names) {
|
|
1678
|
+
if (!node || !import_ts_morph5.Node.isTypeReference(node)) return node;
|
|
1679
|
+
const typeName = node.getTypeName();
|
|
1680
|
+
const name = import_ts_morph5.Node.isIdentifier(typeName) ? typeName.getText() : "";
|
|
1681
|
+
if (names.has(name)) {
|
|
1682
|
+
return node.getTypeArguments()[0] ?? node;
|
|
1683
|
+
}
|
|
1684
|
+
return node;
|
|
1685
|
+
}
|
|
1423
1686
|
function extractDtoContract(method, sourceFile, project) {
|
|
1424
1687
|
let body = extractBodyType(method, sourceFile, project);
|
|
1425
1688
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
1426
1689
|
const query = extractQueryType(method, sourceFile, project);
|
|
1690
|
+
const streamElement = detectStreamElement(method);
|
|
1691
|
+
const isStream = streamElement !== null;
|
|
1427
1692
|
if (filterInfo && filterInfo.source === "body") {
|
|
1428
1693
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
1429
1694
|
body = body ?? bodyType;
|
|
1430
1695
|
}
|
|
1431
1696
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
1432
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
1433
|
-
|
|
1697
|
+
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
1698
|
+
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
1699
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
1434
1700
|
return null;
|
|
1435
1701
|
}
|
|
1436
1702
|
let bodyRef = null;
|
|
@@ -1444,12 +1710,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
1444
1710
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
1445
1711
|
}
|
|
1446
1712
|
}
|
|
1447
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
1713
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
1448
1714
|
if (returnTypeNode) {
|
|
1449
1715
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
1450
1716
|
}
|
|
1451
|
-
if (!responseRef) {
|
|
1452
|
-
const apiResp = method.
|
|
1717
|
+
if (!responseRef && !isStream) {
|
|
1718
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
1453
1719
|
if (apiResp) {
|
|
1454
1720
|
const args = apiResp.getArguments();
|
|
1455
1721
|
const optsArg = args[0];
|
|
@@ -1491,16 +1757,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
1491
1757
|
query,
|
|
1492
1758
|
body,
|
|
1493
1759
|
response,
|
|
1760
|
+
error: errorInfo?.type ?? null,
|
|
1494
1761
|
params: paramsType,
|
|
1495
1762
|
queryRef,
|
|
1496
1763
|
bodyRef,
|
|
1497
1764
|
responseRef,
|
|
1765
|
+
errorRef: errorInfo?.ref ?? null,
|
|
1498
1766
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
1499
1767
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
1500
1768
|
filterSource: filterInfo?.source ?? null,
|
|
1501
1769
|
formWarnings,
|
|
1502
1770
|
bodySchema,
|
|
1503
|
-
querySchema
|
|
1771
|
+
querySchema,
|
|
1772
|
+
stream: isStream
|
|
1504
1773
|
};
|
|
1505
1774
|
}
|
|
1506
1775
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -1618,6 +1887,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
1618
1887
|
let query = null;
|
|
1619
1888
|
let body = null;
|
|
1620
1889
|
let response = "unknown";
|
|
1890
|
+
let error = null;
|
|
1621
1891
|
let bodyZodText = null;
|
|
1622
1892
|
let queryZodText = null;
|
|
1623
1893
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -1633,25 +1903,27 @@ function parseDefineContractCall(callExpr) {
|
|
|
1633
1903
|
bodyZodText = val.getText();
|
|
1634
1904
|
} else if (propName === "response") {
|
|
1635
1905
|
response = zodAstToTs(val);
|
|
1906
|
+
} else if (propName === "error") {
|
|
1907
|
+
error = zodAstToTs(val);
|
|
1636
1908
|
}
|
|
1637
1909
|
}
|
|
1638
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
1910
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
1639
1911
|
}
|
|
1640
1912
|
|
|
1641
1913
|
// src/discovery/contracts-fast.ts
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1914
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
1915
|
+
return tsconfig ? (0, import_node_path3.resolve)(tsconfig) : (0, import_node_path3.join)(cwd, "tsconfig.json");
|
|
1916
|
+
}
|
|
1917
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
1646
1918
|
try {
|
|
1647
|
-
|
|
1919
|
+
return new import_ts_morph7.Project({
|
|
1648
1920
|
tsConfigFilePath: tsconfigPath,
|
|
1649
1921
|
skipAddingFilesFromTsConfig: true,
|
|
1650
1922
|
skipLoadingLibFiles: true,
|
|
1651
1923
|
skipFileDependencyResolution: true
|
|
1652
1924
|
});
|
|
1653
1925
|
} catch {
|
|
1654
|
-
|
|
1926
|
+
return new import_ts_morph7.Project({
|
|
1655
1927
|
skipAddingFilesFromTsConfig: true,
|
|
1656
1928
|
skipLoadingLibFiles: true,
|
|
1657
1929
|
skipFileDependencyResolution: true,
|
|
@@ -1662,20 +1934,98 @@ async function discoverContractsFast(opts) {
|
|
|
1662
1934
|
}
|
|
1663
1935
|
});
|
|
1664
1936
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
project.addSourceFileAtPath(f);
|
|
1668
|
-
}
|
|
1669
|
-
const routes = [];
|
|
1937
|
+
}
|
|
1938
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
1670
1939
|
setDiscoveryContext(project, {
|
|
1671
1940
|
projectRoot: cwd,
|
|
1672
1941
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
1673
1942
|
});
|
|
1674
|
-
|
|
1675
|
-
|
|
1943
|
+
}
|
|
1944
|
+
function extractRoutesFrom(project, controllerPaths) {
|
|
1945
|
+
const routes = [];
|
|
1946
|
+
for (const path of controllerPaths) {
|
|
1947
|
+
const sourceFile = project.getSourceFile(path);
|
|
1948
|
+
if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
|
|
1676
1949
|
}
|
|
1677
1950
|
return routes;
|
|
1678
1951
|
}
|
|
1952
|
+
var PersistentDiscovery = class _PersistentDiscovery {
|
|
1953
|
+
project;
|
|
1954
|
+
cwd;
|
|
1955
|
+
glob;
|
|
1956
|
+
/** Absolute paths of the controllers currently loaded as extraction roots. */
|
|
1957
|
+
controllerPaths = /* @__PURE__ */ new Set();
|
|
1958
|
+
constructor(project, cwd, glob) {
|
|
1959
|
+
this.project = project;
|
|
1960
|
+
this.cwd = cwd;
|
|
1961
|
+
this.glob = glob;
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Build the initial persistent Project: create it, glob + add all controllers,
|
|
1965
|
+
* bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
|
|
1966
|
+
*/
|
|
1967
|
+
static async create(opts) {
|
|
1968
|
+
const { cwd, glob, tsconfig } = opts;
|
|
1969
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
1970
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
1971
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
1972
|
+
const instance = new _PersistentDiscovery(project, cwd, glob);
|
|
1973
|
+
const files = await (0, import_fast_glob.default)(glob, { cwd, absolute: true, onlyFiles: true });
|
|
1974
|
+
for (const f of files) {
|
|
1975
|
+
project.addSourceFileAtPath(f);
|
|
1976
|
+
instance.controllerPaths.add(f);
|
|
1977
|
+
}
|
|
1978
|
+
return instance;
|
|
1979
|
+
}
|
|
1980
|
+
/** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
|
|
1981
|
+
discover() {
|
|
1982
|
+
return this.runExtraction();
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Re-discover after one or more files changed. Refreshes the changed file(s)
|
|
1986
|
+
* from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
|
|
1987
|
+
* to pick up added/removed controllers, clears the per-Project caches, then
|
|
1988
|
+
* re-extracts. `changedPaths` is a hint; correctness does not depend on it
|
|
1989
|
+
* being exhaustive because re-globbing + refresh-on-presence covers the set.
|
|
1990
|
+
*/
|
|
1991
|
+
async rediscover(changedPaths) {
|
|
1992
|
+
if (changedPaths) {
|
|
1993
|
+
for (const p of changedPaths) {
|
|
1994
|
+
const abs = (0, import_node_path3.resolve)(p);
|
|
1995
|
+
const sf = this.project.getSourceFile(abs);
|
|
1996
|
+
if (sf) {
|
|
1997
|
+
await sf.refreshFromFileSystem();
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
const globbed = new Set(
|
|
2002
|
+
await (0, import_fast_glob.default)(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
|
|
2003
|
+
);
|
|
2004
|
+
for (const f of globbed) {
|
|
2005
|
+
if (!this.controllerPaths.has(f)) {
|
|
2006
|
+
try {
|
|
2007
|
+
this.project.addSourceFileAtPath(f);
|
|
2008
|
+
this.controllerPaths.add(f);
|
|
2009
|
+
} catch {
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
for (const f of this.controllerPaths) {
|
|
2014
|
+
if (!globbed.has(f)) {
|
|
2015
|
+
const sf = this.project.getSourceFile(f);
|
|
2016
|
+
if (sf) this.project.removeSourceFile(sf);
|
|
2017
|
+
this.controllerPaths.delete(f);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return this.runExtraction();
|
|
2021
|
+
}
|
|
2022
|
+
/** Clear stale per-Project caches, then extract over the controller set. */
|
|
2023
|
+
runExtraction() {
|
|
2024
|
+
clearTypeResolutionCaches(this.project);
|
|
2025
|
+
clearEnumCache(this.project);
|
|
2026
|
+
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
1679
2029
|
function decoratorStringArg(decoratorExpr) {
|
|
1680
2030
|
if (!decoratorExpr) return void 0;
|
|
1681
2031
|
if (import_ts_morph7.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -1731,6 +2081,11 @@ function resolveVerb(method) {
|
|
|
1731
2081
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
1732
2082
|
}
|
|
1733
2083
|
}
|
|
2084
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
2085
|
+
if (sseDecorator) {
|
|
2086
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
2087
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
2088
|
+
}
|
|
1734
2089
|
return null;
|
|
1735
2090
|
}
|
|
1736
2091
|
function readAsDecorator(node, label) {
|
|
@@ -1773,7 +2128,17 @@ function buildRoute(args) {
|
|
|
1773
2128
|
};
|
|
1774
2129
|
}
|
|
1775
2130
|
function extractContractRoute(args) {
|
|
1776
|
-
const {
|
|
2131
|
+
const {
|
|
2132
|
+
cls,
|
|
2133
|
+
method,
|
|
2134
|
+
applyContractDecorator,
|
|
2135
|
+
verb,
|
|
2136
|
+
prefix,
|
|
2137
|
+
className,
|
|
2138
|
+
sourceFile,
|
|
2139
|
+
project,
|
|
2140
|
+
seenNames
|
|
2141
|
+
} = args;
|
|
1777
2142
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
1778
2143
|
if (!firstDecoratorArg) return null;
|
|
1779
2144
|
let contractDef = null;
|
|
@@ -1783,18 +2148,19 @@ function extractContractRoute(args) {
|
|
|
1783
2148
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
1784
2149
|
} else if (import_ts_morph7.Node.isIdentifier(firstDecoratorArg)) {
|
|
1785
2150
|
const identName = firstDecoratorArg.getText();
|
|
1786
|
-
const
|
|
1787
|
-
if (!
|
|
2151
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
2152
|
+
if (!resolvedVar) {
|
|
1788
2153
|
console.warn(
|
|
1789
|
-
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()}
|
|
2154
|
+
`[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
|
|
1790
2155
|
);
|
|
1791
2156
|
return null;
|
|
1792
2157
|
}
|
|
2158
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
1793
2159
|
const initializer = varDecl.getInitializer();
|
|
1794
2160
|
if (!initializer) return null;
|
|
1795
2161
|
contractDef = parseDefineContractCall(initializer);
|
|
1796
2162
|
if (contractDef && varDecl.isExported()) {
|
|
1797
|
-
const filePath =
|
|
2163
|
+
const filePath = declFile.getFilePath();
|
|
1798
2164
|
if (contractDef.body !== null) {
|
|
1799
2165
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
1800
2166
|
}
|
|
@@ -1827,6 +2193,7 @@ function extractContractRoute(args) {
|
|
|
1827
2193
|
query: contractDef.query,
|
|
1828
2194
|
body: contractDef.body,
|
|
1829
2195
|
response: contractDef.response,
|
|
2196
|
+
error: contractDef.error,
|
|
1830
2197
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
1831
2198
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
1832
2199
|
// would drag server-only deps into the client bundle).
|
|
@@ -1858,15 +2225,18 @@ function extractDtoRoute(args) {
|
|
|
1858
2225
|
query: dtoContract?.query ?? null,
|
|
1859
2226
|
body: dtoContract?.body ?? null,
|
|
1860
2227
|
response: dtoContract?.response ?? "unknown",
|
|
2228
|
+
error: dtoContract?.error ?? null,
|
|
1861
2229
|
queryRef: dtoContract?.queryRef ?? null,
|
|
1862
2230
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
1863
2231
|
responseRef: dtoContract?.responseRef ?? null,
|
|
2232
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
1864
2233
|
filterFields: dtoContract?.filterFields ?? null,
|
|
1865
2234
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
1866
2235
|
filterSource: dtoContract?.filterSource ?? null,
|
|
1867
2236
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
1868
2237
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
1869
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
2238
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
2239
|
+
stream: dtoContract?.stream ?? false
|
|
1870
2240
|
}
|
|
1871
2241
|
});
|
|
1872
2242
|
}
|
|
@@ -1890,6 +2260,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
1890
2260
|
prefix,
|
|
1891
2261
|
className,
|
|
1892
2262
|
sourceFile,
|
|
2263
|
+
project,
|
|
1893
2264
|
seenNames
|
|
1894
2265
|
}) : extractDtoRoute({
|
|
1895
2266
|
cls,
|
|
@@ -1908,8 +2279,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
1908
2279
|
}
|
|
1909
2280
|
|
|
1910
2281
|
// src/generate.ts
|
|
1911
|
-
var
|
|
1912
|
-
var
|
|
2282
|
+
var import_promises11 = require("fs/promises");
|
|
2283
|
+
var import_node_path14 = require("path");
|
|
1913
2284
|
|
|
1914
2285
|
// src/discovery/pages.ts
|
|
1915
2286
|
var import_promises2 = require("fs/promises");
|
|
@@ -2438,17 +2809,28 @@ function emitFilterQueryType(c) {
|
|
|
2438
2809
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
2439
2810
|
}
|
|
2440
2811
|
function buildResponseType(c, outDir) {
|
|
2812
|
+
const respRef = c.contractSource.responseRef;
|
|
2813
|
+
if (c.contractSource.stream) {
|
|
2814
|
+
if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
2815
|
+
return c.contractSource.response;
|
|
2816
|
+
}
|
|
2441
2817
|
if (c.controllerRef) {
|
|
2442
2818
|
let relPath = (0, import_node_path6.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
2443
2819
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
2444
2820
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
2445
2821
|
}
|
|
2446
|
-
const respRef = c.contractSource.responseRef;
|
|
2447
2822
|
if (respRef) {
|
|
2448
2823
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
2449
2824
|
}
|
|
2450
2825
|
return c.contractSource.response;
|
|
2451
2826
|
}
|
|
2827
|
+
function buildErrorType(c) {
|
|
2828
|
+
const errRef = c.contractSource.errorRef;
|
|
2829
|
+
if (errRef) {
|
|
2830
|
+
return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
|
|
2831
|
+
}
|
|
2832
|
+
return c.contractSource.error ?? "unknown";
|
|
2833
|
+
}
|
|
2452
2834
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
2453
2835
|
const pad = " ".repeat(indent);
|
|
2454
2836
|
const lines = [];
|
|
@@ -2463,12 +2845,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2463
2845
|
const bodyRef = c.contractSource.bodyRef;
|
|
2464
2846
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
2465
2847
|
const response = buildResponseType(c, outDir);
|
|
2848
|
+
const error = buildErrorType(c);
|
|
2466
2849
|
const params = buildParamsType(c.params);
|
|
2467
2850
|
const safeMethod = JSON.stringify(method);
|
|
2468
2851
|
const safeUrl = JSON.stringify(c.path);
|
|
2469
2852
|
const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
|
|
2853
|
+
const stream = c.contractSource.stream ? "true" : "false";
|
|
2470
2854
|
lines.push(
|
|
2471
|
-
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
|
|
2855
|
+
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
|
|
2472
2856
|
);
|
|
2473
2857
|
} else {
|
|
2474
2858
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -2540,15 +2924,21 @@ function emitReqHelper() {
|
|
|
2540
2924
|
""
|
|
2541
2925
|
];
|
|
2542
2926
|
}
|
|
2543
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
2927
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
2544
2928
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
2545
2929
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
2930
|
+
if (streamExpr) {
|
|
2931
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
2932
|
+
}
|
|
2546
2933
|
for (const [name, value] of Object.entries(members)) {
|
|
2547
2934
|
lines.push(`${pad} ${name}: ${value},`);
|
|
2548
2935
|
}
|
|
2549
2936
|
lines.push(`${pad}}),`);
|
|
2550
2937
|
return lines;
|
|
2551
2938
|
}
|
|
2939
|
+
function renderStreamExpr(req) {
|
|
2940
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
2941
|
+
}
|
|
2552
2942
|
function emitApiObjectBlock(tree, indent, p) {
|
|
2553
2943
|
const pad = " ".repeat(indent);
|
|
2554
2944
|
const lines = [];
|
|
@@ -2583,7 +2973,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2583
2973
|
}
|
|
2584
2974
|
const members = {};
|
|
2585
2975
|
for (const [name, { value }] of owned) members[name] = value;
|
|
2586
|
-
|
|
2976
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
2977
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
2587
2978
|
}
|
|
2588
2979
|
return lines;
|
|
2589
2980
|
}
|
|
@@ -2621,6 +3012,8 @@ var ROUTE_NAMESPACE = [
|
|
|
2621
3012
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
2622
3013
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
2623
3014
|
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
3015
|
+
" /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
|
|
3016
|
+
' export type Stream<K extends string> = ResolveByName<K, "response">;',
|
|
2624
3017
|
" export type Request<K extends string> = {",
|
|
2625
3018
|
" body: Body<K>;",
|
|
2626
3019
|
" query: Query<K>;",
|
|
@@ -2637,6 +3030,7 @@ var PATH_NAMESPACE = [
|
|
|
2637
3030
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
2638
3031
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
2639
3032
|
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
3033
|
+
' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
2640
3034
|
"}",
|
|
2641
3035
|
""
|
|
2642
3036
|
];
|
|
@@ -2648,6 +3042,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
2648
3042
|
" export type Params<K extends string> = never;",
|
|
2649
3043
|
" export type Error<K extends string> = never;",
|
|
2650
3044
|
" export type FilterFields<K extends string> = never;",
|
|
3045
|
+
" export type Stream<K extends string> = never;",
|
|
2651
3046
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
2652
3047
|
"}",
|
|
2653
3048
|
""
|
|
@@ -2660,6 +3055,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
2660
3055
|
" export type Params<M extends string, U extends string> = never;",
|
|
2661
3056
|
" export type Error<M extends string, U extends string> = never;",
|
|
2662
3057
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
3058
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
2663
3059
|
"}",
|
|
2664
3060
|
""
|
|
2665
3061
|
];
|
|
@@ -2683,7 +3079,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
2683
3079
|
for (const r of contracted) {
|
|
2684
3080
|
const cs = r.contract?.contractSource;
|
|
2685
3081
|
if (!cs) continue;
|
|
2686
|
-
const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
|
|
3082
|
+
const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
|
|
2687
3083
|
for (const ref of refs) {
|
|
2688
3084
|
if (!ref) continue;
|
|
2689
3085
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -2861,18 +3257,27 @@ function refRootIdentifier(refName) {
|
|
|
2861
3257
|
function hasSource(src) {
|
|
2862
3258
|
return !!(src.schema || src.zodText || src.zodRef);
|
|
2863
3259
|
}
|
|
3260
|
+
function escapeRegExp(s) {
|
|
3261
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3262
|
+
}
|
|
3263
|
+
var wordBoundaryRegexCache = /* @__PURE__ */ new Map();
|
|
3264
|
+
function wordBoundaryRegex(token) {
|
|
3265
|
+
let re = wordBoundaryRegexCache.get(token);
|
|
3266
|
+
if (re === void 0) {
|
|
3267
|
+
re = new RegExp(`\\b${escapeRegExp(token)}\\b`, "g");
|
|
3268
|
+
wordBoundaryRegexCache.set(token, re);
|
|
3269
|
+
}
|
|
3270
|
+
return re;
|
|
3271
|
+
}
|
|
2864
3272
|
function applyRenames(text, renames) {
|
|
2865
3273
|
if (!renames || renames.size === 0) return text;
|
|
2866
3274
|
let out = text;
|
|
2867
3275
|
for (const [from, to] of renames) {
|
|
2868
3276
|
if (from === to) continue;
|
|
2869
|
-
out = out.replace(
|
|
3277
|
+
out = out.replace(wordBoundaryRegex(from), to);
|
|
2870
3278
|
}
|
|
2871
3279
|
return out;
|
|
2872
3280
|
}
|
|
2873
|
-
function escapeRegExp(s) {
|
|
2874
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2875
|
-
}
|
|
2876
3281
|
function isSelfReferential(name, text) {
|
|
2877
3282
|
return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
|
|
2878
3283
|
}
|
|
@@ -2884,7 +3289,23 @@ function planNestedSchemas(entries) {
|
|
|
2884
3289
|
const local = Object.entries(entry.nestedSchemas);
|
|
2885
3290
|
if (local.length === 0) continue;
|
|
2886
3291
|
const rename = /* @__PURE__ */ new Map();
|
|
2887
|
-
|
|
3292
|
+
const renameValues = /* @__PURE__ */ new Set();
|
|
3293
|
+
const setRename = (key, value) => {
|
|
3294
|
+
const prev = rename.get(key);
|
|
3295
|
+
rename.set(key, value);
|
|
3296
|
+
if (prev !== void 0 && prev !== value) {
|
|
3297
|
+
let stillUsed = false;
|
|
3298
|
+
for (const v of rename.values()) {
|
|
3299
|
+
if (v === prev) {
|
|
3300
|
+
stillUsed = true;
|
|
3301
|
+
break;
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
if (!stillUsed) renameValues.delete(prev);
|
|
3305
|
+
}
|
|
3306
|
+
renameValues.add(value);
|
|
3307
|
+
};
|
|
3308
|
+
for (const [name] of local) setRename(name, name);
|
|
2888
3309
|
const textFor = (name) => {
|
|
2889
3310
|
const raw = entry.nestedSchemas?.[name] ?? "";
|
|
2890
3311
|
return applyRenames(raw, rename);
|
|
@@ -2902,11 +3323,11 @@ function planNestedSchemas(entries) {
|
|
|
2902
3323
|
if (existing === text) continue;
|
|
2903
3324
|
let i = 2;
|
|
2904
3325
|
let candidate = `${name}_${i}`;
|
|
2905
|
-
while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) ||
|
|
3326
|
+
while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || renameValues.has(candidate)) {
|
|
2906
3327
|
i += 1;
|
|
2907
3328
|
candidate = `${name}_${i}`;
|
|
2908
3329
|
}
|
|
2909
|
-
|
|
3330
|
+
setRename(name, candidate);
|
|
2910
3331
|
changed = true;
|
|
2911
3332
|
}
|
|
2912
3333
|
}
|
|
@@ -3116,11 +3537,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
3116
3537
|
await (0, import_promises6.writeFile)((0, import_node_path9.join)(outDir, "index.d.ts"), content, "utf8");
|
|
3117
3538
|
}
|
|
3118
3539
|
|
|
3119
|
-
// src/emit/emit-
|
|
3540
|
+
// src/emit/emit-mocks.ts
|
|
3120
3541
|
var import_promises7 = require("fs/promises");
|
|
3121
3542
|
var import_node_path10 = require("path");
|
|
3122
|
-
|
|
3543
|
+
|
|
3544
|
+
// src/ir/schema-node-to-json-schema.ts
|
|
3545
|
+
var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
|
|
3546
|
+
function parseLiteral(raw) {
|
|
3547
|
+
const t = raw.trim();
|
|
3548
|
+
if (t === "true") return true;
|
|
3549
|
+
if (t === "false") return false;
|
|
3550
|
+
if (t === "null") return null;
|
|
3551
|
+
const q = t[0];
|
|
3552
|
+
if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
|
|
3553
|
+
return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
|
|
3554
|
+
}
|
|
3555
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
|
|
3556
|
+
return Number(t);
|
|
3557
|
+
}
|
|
3558
|
+
return t;
|
|
3559
|
+
}
|
|
3560
|
+
function literalsType(values) {
|
|
3561
|
+
const types = new Set(values.map((v) => v === null ? "null" : typeof v));
|
|
3562
|
+
if (types.size === 1) {
|
|
3563
|
+
const only = [...types][0];
|
|
3564
|
+
if (only === "string") return "string";
|
|
3565
|
+
if (only === "number") return "number";
|
|
3566
|
+
if (only === "boolean") return "boolean";
|
|
3567
|
+
}
|
|
3568
|
+
return void 0;
|
|
3569
|
+
}
|
|
3570
|
+
function convert(node, ctx) {
|
|
3571
|
+
switch (node.kind) {
|
|
3572
|
+
case "string": {
|
|
3573
|
+
const out = { type: "string" };
|
|
3574
|
+
for (const c of node.checks) {
|
|
3575
|
+
if (c.check === "email") out.format = "email";
|
|
3576
|
+
else if (c.check === "url") out.format = "uri";
|
|
3577
|
+
else if (c.check === "uuid") out.format = "uuid";
|
|
3578
|
+
else if (c.check === "min") out.minLength = Number(c.value);
|
|
3579
|
+
else if (c.check === "max") out.maxLength = Number(c.value);
|
|
3580
|
+
else if (c.check === "regex") {
|
|
3581
|
+
const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
|
|
3582
|
+
out.pattern = m ? m[1] : c.pattern;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
return out;
|
|
3586
|
+
}
|
|
3587
|
+
case "number": {
|
|
3588
|
+
const out = { type: "number" };
|
|
3589
|
+
for (const c of node.checks) {
|
|
3590
|
+
if (c.check === "int") out.type = "integer";
|
|
3591
|
+
else if (c.check === "min") out.minimum = Number(c.value);
|
|
3592
|
+
else if (c.check === "max") out.maximum = Number(c.value);
|
|
3593
|
+
else if (c.check === "positive") out.exclusiveMinimum = 0;
|
|
3594
|
+
else if (c.check === "negative") out.exclusiveMaximum = 0;
|
|
3595
|
+
}
|
|
3596
|
+
return out;
|
|
3597
|
+
}
|
|
3598
|
+
case "boolean":
|
|
3599
|
+
return { type: "boolean" };
|
|
3600
|
+
case "date":
|
|
3601
|
+
return { type: "string", format: "date-time" };
|
|
3602
|
+
case "unknown":
|
|
3603
|
+
return node.note ? { description: node.note } : {};
|
|
3604
|
+
case "instanceof":
|
|
3605
|
+
return { type: "object", description: `instanceof ${node.ctor}` };
|
|
3606
|
+
case "enum": {
|
|
3607
|
+
const values = node.literals.map(parseLiteral);
|
|
3608
|
+
const t = literalsType(values);
|
|
3609
|
+
const out = { enum: values };
|
|
3610
|
+
if (t) out.type = t;
|
|
3611
|
+
return out;
|
|
3612
|
+
}
|
|
3613
|
+
case "literal": {
|
|
3614
|
+
const value = parseLiteral(node.raw);
|
|
3615
|
+
const out = { const: value };
|
|
3616
|
+
const t = literalsType([value]);
|
|
3617
|
+
if (t) out.type = t;
|
|
3618
|
+
return out;
|
|
3619
|
+
}
|
|
3620
|
+
case "union": {
|
|
3621
|
+
const options = node.options.map((o) => convert(o, ctx));
|
|
3622
|
+
const out = { oneOf: options };
|
|
3623
|
+
if (node.discriminator) {
|
|
3624
|
+
out.discriminator = { propertyName: node.discriminator };
|
|
3625
|
+
}
|
|
3626
|
+
return out;
|
|
3627
|
+
}
|
|
3628
|
+
case "object": {
|
|
3629
|
+
const properties = {};
|
|
3630
|
+
const required = [];
|
|
3631
|
+
for (const f of node.fields) {
|
|
3632
|
+
if (f.value.kind === "optional") {
|
|
3633
|
+
properties[f.key] = convert(f.value.inner, ctx);
|
|
3634
|
+
} else {
|
|
3635
|
+
properties[f.key] = convert(f.value, ctx);
|
|
3636
|
+
required.push(f.key);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
const out = { type: "object", properties };
|
|
3640
|
+
if (required.length > 0) out.required = required;
|
|
3641
|
+
out.additionalProperties = node.passthrough;
|
|
3642
|
+
return out;
|
|
3643
|
+
}
|
|
3644
|
+
case "array":
|
|
3645
|
+
return { type: "array", items: convert(node.element, ctx) };
|
|
3646
|
+
case "optional":
|
|
3647
|
+
return widenNullable(convert(node.inner, ctx));
|
|
3648
|
+
case "ref":
|
|
3649
|
+
case "lazyRef":
|
|
3650
|
+
return { $ref: `${ctx.refPrefix}${node.name}` };
|
|
3651
|
+
case "annotated":
|
|
3652
|
+
return convert(node.inner, ctx);
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
function widenNullable(schema) {
|
|
3656
|
+
if (schema.$ref) {
|
|
3657
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
3658
|
+
}
|
|
3659
|
+
if (typeof schema.type === "string") {
|
|
3660
|
+
return { ...schema, type: [schema.type, "null"] };
|
|
3661
|
+
}
|
|
3662
|
+
if (Array.isArray(schema.type)) {
|
|
3663
|
+
return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
|
|
3664
|
+
}
|
|
3665
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
3666
|
+
}
|
|
3667
|
+
function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
|
|
3668
|
+
const named = {};
|
|
3669
|
+
for (const [name, node] of mod.named) {
|
|
3670
|
+
named[name] = convert(node, ctx);
|
|
3671
|
+
}
|
|
3672
|
+
return { root: convert(mod.root, ctx), named };
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
// src/emit/mock-gen-runtime.ts
|
|
3676
|
+
var MOCK_GEN_RUNTIME = `
|
|
3677
|
+
/** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
|
|
3678
|
+
function makeRng(seed) {
|
|
3679
|
+
let a = seed >>> 0;
|
|
3680
|
+
return {
|
|
3681
|
+
next() {
|
|
3682
|
+
a |= 0;
|
|
3683
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
3684
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
3685
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
3686
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
3687
|
+
},
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
function __pick(rng, items) {
|
|
3692
|
+
return items[Math.floor(rng.next() * items.length)];
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
function __intBetween(rng, min, max) {
|
|
3696
|
+
return Math.floor(rng.next() * (max - min + 1)) + min;
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
|
|
3700
|
+
const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
|
|
3701
|
+
const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
|
|
3702
|
+
|
|
3703
|
+
function __fakeWords(rng, count) {
|
|
3704
|
+
let out = [];
|
|
3705
|
+
for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
|
|
3706
|
+
return out.join(' ');
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
function __hex(rng, len) {
|
|
3710
|
+
let s = '';
|
|
3711
|
+
for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
|
|
3712
|
+
return s;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
function __fakeUuid(rng) {
|
|
3716
|
+
return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
function __fakeString(rng, schema) {
|
|
3720
|
+
switch (schema.format) {
|
|
3721
|
+
case 'email':
|
|
3722
|
+
return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
|
|
3723
|
+
case 'uri':
|
|
3724
|
+
case 'url':
|
|
3725
|
+
return 'https://example.com/' + __pick(rng, __WORDS);
|
|
3726
|
+
case 'uuid':
|
|
3727
|
+
return __fakeUuid(rng);
|
|
3728
|
+
case 'date-time':
|
|
3729
|
+
return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
|
|
3730
|
+
default:
|
|
3731
|
+
return __fakeWords(rng, __intBetween(rng, 1, 3));
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
/** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
|
|
3736
|
+
function generateMock(schema, rng, defs, depth) {
|
|
3737
|
+
defs = defs || {};
|
|
3738
|
+
depth = depth || 0;
|
|
3739
|
+
if (schema.$ref) {
|
|
3740
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
3741
|
+
const target = defs[name];
|
|
3742
|
+
if (!target || depth > 4) return null;
|
|
3743
|
+
return generateMock(target, rng, defs, depth + 1);
|
|
3744
|
+
}
|
|
3745
|
+
if ('const' in schema) return schema.const;
|
|
3746
|
+
if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
|
|
3747
|
+
if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
|
|
3748
|
+
if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
|
|
3749
|
+
let type = Array.isArray(schema.type)
|
|
3750
|
+
? (schema.type.filter((t) => t !== 'null')[0] || 'null')
|
|
3751
|
+
: schema.type;
|
|
3752
|
+
switch (type) {
|
|
3753
|
+
case 'string':
|
|
3754
|
+
return __fakeString(rng, schema);
|
|
3755
|
+
case 'integer':
|
|
3756
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
|
|
3757
|
+
case 'number':
|
|
3758
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
|
|
3759
|
+
case 'boolean':
|
|
3760
|
+
return rng.next() < 0.5;
|
|
3761
|
+
case 'null':
|
|
3762
|
+
return null;
|
|
3763
|
+
case 'array': {
|
|
3764
|
+
const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
|
|
3765
|
+
const items = schema.items || {};
|
|
3766
|
+
let arr = [];
|
|
3767
|
+
for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
|
|
3768
|
+
return arr;
|
|
3769
|
+
}
|
|
3770
|
+
case 'object': {
|
|
3771
|
+
const out = {};
|
|
3772
|
+
const props = schema.properties || {};
|
|
3773
|
+
for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
|
|
3774
|
+
return out;
|
|
3775
|
+
}
|
|
3776
|
+
default:
|
|
3777
|
+
return {};
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
`.trim();
|
|
3781
|
+
|
|
3782
|
+
// src/emit/emit-mocks.ts
|
|
3783
|
+
var REF_PREFIX = "#/components/schemas/";
|
|
3784
|
+
function toMswPath(path, baseUrl) {
|
|
3785
|
+
return `${baseUrl}${path}`;
|
|
3786
|
+
}
|
|
3787
|
+
function responseSchemaFor(route, defs) {
|
|
3788
|
+
const cs = route.contract.contractSource;
|
|
3789
|
+
if (cs.responseSchema) {
|
|
3790
|
+
const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
|
|
3791
|
+
for (const [name, node] of Object.entries(named)) {
|
|
3792
|
+
if (!(name in defs)) defs[name] = node;
|
|
3793
|
+
}
|
|
3794
|
+
return root;
|
|
3795
|
+
}
|
|
3796
|
+
return {};
|
|
3797
|
+
}
|
|
3798
|
+
function buildMocksFile(routes, opts = {}) {
|
|
3799
|
+
const seed = opts.seed ?? 1;
|
|
3800
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
3801
|
+
const contracted = routes.filter((r) => r.contract);
|
|
3802
|
+
const defs = {};
|
|
3803
|
+
const handlers = [];
|
|
3804
|
+
for (const r of contracted) {
|
|
3805
|
+
const schema = responseSchemaFor(r, defs);
|
|
3806
|
+
const method = r.method.toLowerCase();
|
|
3807
|
+
const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
|
|
3808
|
+
const path = toMswPath(r.path, baseUrl);
|
|
3809
|
+
const cs = r.contract.contractSource;
|
|
3810
|
+
const schemaLiteral = JSON.stringify(schema);
|
|
3811
|
+
const pathLit = JSON.stringify(path);
|
|
3812
|
+
if (cs.stream) {
|
|
3813
|
+
handlers.push(
|
|
3814
|
+
[
|
|
3815
|
+
` // ${r.name} (stream)`,
|
|
3816
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
3817
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
3818
|
+
" const body = `data: ${JSON.stringify(value)}\\n\\n`;",
|
|
3819
|
+
" return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
|
|
3820
|
+
" }),"
|
|
3821
|
+
].join("\n")
|
|
3822
|
+
);
|
|
3823
|
+
} else {
|
|
3824
|
+
handlers.push(
|
|
3825
|
+
[
|
|
3826
|
+
` // ${r.name}`,
|
|
3827
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
3828
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
3829
|
+
" return HttpResponse.json(value);",
|
|
3830
|
+
" }),"
|
|
3831
|
+
].join("\n")
|
|
3832
|
+
);
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
const lines = [
|
|
3836
|
+
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
3837
|
+
"// MSW handlers returning deterministic, schema-shaped mock data.",
|
|
3838
|
+
"/* eslint-disable */",
|
|
3839
|
+
"// @ts-nocheck",
|
|
3840
|
+
"",
|
|
3841
|
+
"import { http, HttpResponse } from 'msw';",
|
|
3842
|
+
"",
|
|
3843
|
+
`const SEED = ${seed};`,
|
|
3844
|
+
"",
|
|
3845
|
+
"// ---------------------------------------------------------------------------",
|
|
3846
|
+
"// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
|
|
3847
|
+
"// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
|
|
3848
|
+
"// ---------------------------------------------------------------------------",
|
|
3849
|
+
MOCK_GEN_RUNTIME,
|
|
3850
|
+
"",
|
|
3851
|
+
"// Shared component schemas referenced by $ref.",
|
|
3852
|
+
`const DEFS = ${JSON.stringify(defs, null, 2)};`,
|
|
3853
|
+
"",
|
|
3854
|
+
"/** MSW request handlers, one per contracted route. */",
|
|
3855
|
+
"export const handlers = [",
|
|
3856
|
+
...handlers,
|
|
3857
|
+
"];",
|
|
3858
|
+
""
|
|
3859
|
+
];
|
|
3860
|
+
return lines.join("\n");
|
|
3861
|
+
}
|
|
3862
|
+
async function emitMocks(routes, outDir, opts = {}) {
|
|
3123
3863
|
await (0, import_promises7.mkdir)(outDir, { recursive: true });
|
|
3864
|
+
const content = buildMocksFile(routes, opts);
|
|
3865
|
+
const fileName = opts.fileName ?? "mocks.ts";
|
|
3866
|
+
await (0, import_promises7.writeFile)((0, import_node_path10.join)(outDir, fileName), content, "utf8");
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
// src/emit/emit-openapi.ts
|
|
3870
|
+
var import_promises8 = require("fs/promises");
|
|
3871
|
+
var import_node_path11 = require("path");
|
|
3872
|
+
var REF_PREFIX2 = "#/components/schemas/";
|
|
3873
|
+
function toOpenApiPath(path) {
|
|
3874
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
3875
|
+
}
|
|
3876
|
+
function positionSchema(schema, tsType, components) {
|
|
3877
|
+
if (schema) {
|
|
3878
|
+
const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
|
|
3879
|
+
for (const [name, node] of Object.entries(named)) {
|
|
3880
|
+
if (!(name in components)) components[name] = node;
|
|
3881
|
+
}
|
|
3882
|
+
return root;
|
|
3883
|
+
}
|
|
3884
|
+
return tsType ? { description: tsType } : {};
|
|
3885
|
+
}
|
|
3886
|
+
function buildParameters(route) {
|
|
3887
|
+
const params = [];
|
|
3888
|
+
for (const p of route.params) {
|
|
3889
|
+
if (p.source === "path") {
|
|
3890
|
+
params.push({
|
|
3891
|
+
name: p.name,
|
|
3892
|
+
in: "path",
|
|
3893
|
+
required: true,
|
|
3894
|
+
schema: { type: "string" }
|
|
3895
|
+
});
|
|
3896
|
+
} else if (p.source === "query") {
|
|
3897
|
+
params.push({
|
|
3898
|
+
name: p.name,
|
|
3899
|
+
in: "query",
|
|
3900
|
+
required: false,
|
|
3901
|
+
schema: { type: "string" }
|
|
3902
|
+
});
|
|
3903
|
+
} else if (p.source === "header") {
|
|
3904
|
+
params.push({
|
|
3905
|
+
name: p.name,
|
|
3906
|
+
in: "header",
|
|
3907
|
+
required: false,
|
|
3908
|
+
schema: { type: "string" }
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
return params;
|
|
3913
|
+
}
|
|
3914
|
+
function buildResponses(cs, components) {
|
|
3915
|
+
const responses = {};
|
|
3916
|
+
const successSchema = positionSchema(
|
|
3917
|
+
// Prefer rich response IR when present; otherwise fall back to the TS type.
|
|
3918
|
+
cs.responseSchema ?? null,
|
|
3919
|
+
cs.response,
|
|
3920
|
+
components
|
|
3921
|
+
);
|
|
3922
|
+
const successContentType = cs.stream ? "text/event-stream" : "application/json";
|
|
3923
|
+
responses["200"] = {
|
|
3924
|
+
description: cs.stream ? "Server-sent event stream" : "Successful response",
|
|
3925
|
+
content: { [successContentType]: { schema: successSchema } }
|
|
3926
|
+
};
|
|
3927
|
+
const errorSchema = positionSchema(null, cs.error ?? null, components);
|
|
3928
|
+
const errorBody = {
|
|
3929
|
+
description: "Error response",
|
|
3930
|
+
content: { "application/json": { schema: errorSchema } }
|
|
3931
|
+
};
|
|
3932
|
+
if (cs.error || cs.errorRef) {
|
|
3933
|
+
responses["400"] = errorBody;
|
|
3934
|
+
responses.default = errorBody;
|
|
3935
|
+
} else {
|
|
3936
|
+
responses.default = {
|
|
3937
|
+
description: "Error response",
|
|
3938
|
+
content: { "application/json": { schema: {} } }
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
return responses;
|
|
3942
|
+
}
|
|
3943
|
+
function buildOperation(route, components) {
|
|
3944
|
+
const cs = route.contract.contractSource;
|
|
3945
|
+
const op = {
|
|
3946
|
+
operationId: route.name,
|
|
3947
|
+
parameters: buildParameters(route),
|
|
3948
|
+
responses: buildResponses(cs, components)
|
|
3949
|
+
};
|
|
3950
|
+
const method = route.method.toUpperCase();
|
|
3951
|
+
const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
|
|
3952
|
+
if (hasBody && (cs.bodySchema || cs.body)) {
|
|
3953
|
+
const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
|
|
3954
|
+
op.requestBody = {
|
|
3955
|
+
required: true,
|
|
3956
|
+
content: { "application/json": { schema: bodySchema } }
|
|
3957
|
+
};
|
|
3958
|
+
}
|
|
3959
|
+
return op;
|
|
3960
|
+
}
|
|
3961
|
+
function buildOpenApiSpec(routes, opts = {}) {
|
|
3962
|
+
const components = {};
|
|
3963
|
+
const paths = {};
|
|
3964
|
+
for (const route of routes) {
|
|
3965
|
+
if (!route.contract) continue;
|
|
3966
|
+
const oaPath = toOpenApiPath(route.path);
|
|
3967
|
+
const method = route.method.toLowerCase();
|
|
3968
|
+
let pathItem = paths[oaPath];
|
|
3969
|
+
if (!pathItem) {
|
|
3970
|
+
pathItem = {};
|
|
3971
|
+
paths[oaPath] = pathItem;
|
|
3972
|
+
}
|
|
3973
|
+
pathItem[method] = buildOperation(route, components);
|
|
3974
|
+
}
|
|
3975
|
+
const info = opts.info ?? {};
|
|
3976
|
+
const doc = {
|
|
3977
|
+
openapi: "3.1.0",
|
|
3978
|
+
info: {
|
|
3979
|
+
title: info.title ?? "NestJS API",
|
|
3980
|
+
version: info.version ?? "1.0.0",
|
|
3981
|
+
...info.description ? { description: info.description } : {}
|
|
3982
|
+
},
|
|
3983
|
+
paths,
|
|
3984
|
+
components: { schemas: components }
|
|
3985
|
+
};
|
|
3986
|
+
return doc;
|
|
3987
|
+
}
|
|
3988
|
+
async function emitOpenApi(routes, outDir, opts = {}) {
|
|
3989
|
+
await (0, import_promises8.mkdir)(outDir, { recursive: true });
|
|
3990
|
+
const doc = buildOpenApiSpec(routes, opts);
|
|
3991
|
+
const fileName = opts.fileName ?? "openapi.json";
|
|
3992
|
+
await (0, import_promises8.writeFile)((0, import_node_path11.join)(outDir, fileName), `${JSON.stringify(doc, null, 2)}
|
|
3993
|
+
`, "utf8");
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
// src/emit/emit-pages.ts
|
|
3997
|
+
var import_promises9 = require("fs/promises");
|
|
3998
|
+
var import_node_path12 = require("path");
|
|
3999
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
4000
|
+
await (0, import_promises9.mkdir)(outDir, { recursive: true });
|
|
3124
4001
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
3125
4002
|
const augBody = pages.map((p) => {
|
|
3126
4003
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -3139,7 +4016,7 @@ ${augBody}
|
|
|
3139
4016
|
}
|
|
3140
4017
|
${sharedPropsBlock}}
|
|
3141
4018
|
`;
|
|
3142
|
-
await (0,
|
|
4019
|
+
await (0, import_promises9.writeFile)((0, import_node_path12.join)(outDir, "pages.d.ts"), content, "utf8");
|
|
3143
4020
|
}
|
|
3144
4021
|
function buildSharedPropsBlock(sharedProps) {
|
|
3145
4022
|
if (!sharedProps) return "";
|
|
@@ -3158,7 +4035,7 @@ ${propsBody}
|
|
|
3158
4035
|
`;
|
|
3159
4036
|
}
|
|
3160
4037
|
function buildAugmentationType(page, outDir) {
|
|
3161
|
-
let importPath = (0,
|
|
4038
|
+
let importPath = (0, import_node_path12.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
|
|
3162
4039
|
if (!importPath.startsWith(".")) {
|
|
3163
4040
|
importPath = `./${importPath}`;
|
|
3164
4041
|
}
|
|
@@ -3169,12 +4046,12 @@ function needsQuotes(name) {
|
|
|
3169
4046
|
}
|
|
3170
4047
|
|
|
3171
4048
|
// src/emit/emit-routes.ts
|
|
3172
|
-
var
|
|
3173
|
-
var
|
|
4049
|
+
var import_promises10 = require("fs/promises");
|
|
4050
|
+
var import_node_path13 = require("path");
|
|
3174
4051
|
async function emitRoutes(routes, outDir) {
|
|
3175
|
-
await (0,
|
|
4052
|
+
await (0, import_promises10.mkdir)(outDir, { recursive: true });
|
|
3176
4053
|
const content = buildRoutesFile(routes);
|
|
3177
|
-
await (0,
|
|
4054
|
+
await (0, import_promises10.writeFile)((0, import_node_path13.join)(outDir, "routes.ts"), content, "utf8");
|
|
3178
4055
|
}
|
|
3179
4056
|
function buildRoutesFile(routes) {
|
|
3180
4057
|
if (routes.length === 0) {
|
|
@@ -3322,21 +4199,38 @@ async function generate(config, inputRoutes = []) {
|
|
|
3322
4199
|
});
|
|
3323
4200
|
}
|
|
3324
4201
|
const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
|
|
4202
|
+
if (hasContracts && config.openapi.enabled) {
|
|
4203
|
+
await emitOpenApi(routes, config.codegen.outDir, {
|
|
4204
|
+
fileName: config.openapi.fileName,
|
|
4205
|
+
info: {
|
|
4206
|
+
title: config.openapi.title,
|
|
4207
|
+
version: config.openapi.version,
|
|
4208
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
4209
|
+
}
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
4212
|
+
if (hasContracts && config.mocks.enabled) {
|
|
4213
|
+
await emitMocks(routes, config.codegen.outDir, {
|
|
4214
|
+
fileName: config.mocks.fileName,
|
|
4215
|
+
seed: config.mocks.seed,
|
|
4216
|
+
baseUrl: config.mocks.baseUrl
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
3325
4219
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
3326
4220
|
if (extensions.length > 0) {
|
|
3327
4221
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
3328
4222
|
for (const file of extraFiles) {
|
|
3329
|
-
const dest = (0,
|
|
3330
|
-
await (0,
|
|
3331
|
-
await (0,
|
|
4223
|
+
const dest = (0, import_node_path14.join)(config.codegen.outDir, file.path);
|
|
4224
|
+
await (0, import_promises11.mkdir)((0, import_node_path14.dirname)(dest), { recursive: true });
|
|
4225
|
+
await (0, import_promises11.writeFile)(dest, file.contents, "utf8");
|
|
3332
4226
|
}
|
|
3333
4227
|
}
|
|
3334
4228
|
}
|
|
3335
4229
|
|
|
3336
4230
|
// src/watch/lock-file.ts
|
|
3337
|
-
var
|
|
3338
|
-
var
|
|
3339
|
-
var
|
|
4231
|
+
var import_promises12 = require("fs/promises");
|
|
4232
|
+
var import_promises13 = require("fs/promises");
|
|
4233
|
+
var import_node_path15 = require("path");
|
|
3340
4234
|
var LOCK_FILE = ".watcher.lock";
|
|
3341
4235
|
function isProcessAlive(pid) {
|
|
3342
4236
|
try {
|
|
@@ -3347,21 +4241,21 @@ function isProcessAlive(pid) {
|
|
|
3347
4241
|
}
|
|
3348
4242
|
}
|
|
3349
4243
|
async function acquireLock(outDir) {
|
|
3350
|
-
await (0,
|
|
3351
|
-
const lockPath = (0,
|
|
4244
|
+
await (0, import_promises13.mkdir)(outDir, { recursive: true });
|
|
4245
|
+
const lockPath = (0, import_node_path15.join)(outDir, LOCK_FILE);
|
|
3352
4246
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3353
4247
|
try {
|
|
3354
|
-
const fd = await (0,
|
|
4248
|
+
const fd = await (0, import_promises12.open)(lockPath, "wx");
|
|
3355
4249
|
await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
|
|
3356
4250
|
`, "utf8");
|
|
3357
4251
|
await fd.close();
|
|
3358
4252
|
} catch (err) {
|
|
3359
4253
|
if (err.code === "EEXIST") {
|
|
3360
4254
|
try {
|
|
3361
|
-
const raw = await (0,
|
|
4255
|
+
const raw = await (0, import_promises13.readFile)(lockPath, "utf8");
|
|
3362
4256
|
const existing = JSON.parse(raw);
|
|
3363
4257
|
if (isProcessAlive(existing.pid)) return null;
|
|
3364
|
-
await (0,
|
|
4258
|
+
await (0, import_promises13.unlink)(lockPath);
|
|
3365
4259
|
return acquireLock(outDir);
|
|
3366
4260
|
} catch {
|
|
3367
4261
|
return null;
|
|
@@ -3372,7 +4266,7 @@ async function acquireLock(outDir) {
|
|
|
3372
4266
|
return {
|
|
3373
4267
|
release: async () => {
|
|
3374
4268
|
try {
|
|
3375
|
-
await (0,
|
|
4269
|
+
await (0, import_promises13.unlink)(lockPath);
|
|
3376
4270
|
} catch {
|
|
3377
4271
|
}
|
|
3378
4272
|
}
|
|
@@ -3388,7 +4282,7 @@ async function watch(config, onChange) {
|
|
|
3388
4282
|
if (lock === null) {
|
|
3389
4283
|
let holderPid = "unknown";
|
|
3390
4284
|
try {
|
|
3391
|
-
const raw = await (0,
|
|
4285
|
+
const raw = await (0, import_promises14.readFile)((0, import_node_path16.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3392
4286
|
const data = JSON.parse(raw);
|
|
3393
4287
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3394
4288
|
} catch {
|
|
@@ -3398,12 +4292,20 @@ async function watch(config, onChange) {
|
|
|
3398
4292
|
);
|
|
3399
4293
|
return NO_OP_WATCHER;
|
|
3400
4294
|
}
|
|
4295
|
+
let discovery = null;
|
|
4296
|
+
async function getDiscovery() {
|
|
4297
|
+
if (discovery === null) {
|
|
4298
|
+
discovery = await PersistentDiscovery.create({
|
|
4299
|
+
cwd: config.codegen.cwd,
|
|
4300
|
+
glob: config.contracts.glob,
|
|
4301
|
+
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
4302
|
+
});
|
|
4303
|
+
return discovery;
|
|
4304
|
+
}
|
|
4305
|
+
return discovery;
|
|
4306
|
+
}
|
|
3401
4307
|
try {
|
|
3402
|
-
const initialRoutes = await
|
|
3403
|
-
cwd: config.codegen.cwd,
|
|
3404
|
-
glob: config.contracts.glob,
|
|
3405
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3406
|
-
});
|
|
4308
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3407
4309
|
await generate(config, initialRoutes);
|
|
3408
4310
|
} catch (err) {
|
|
3409
4311
|
console.warn(
|
|
@@ -3416,7 +4318,7 @@ async function watch(config, onChange) {
|
|
|
3416
4318
|
}
|
|
3417
4319
|
let pagesDebounceTimer;
|
|
3418
4320
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3419
|
-
const pagesWatcher = import_chokidar.default.watch((0,
|
|
4321
|
+
const pagesWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, pagesGlob), {
|
|
3420
4322
|
ignoreInitial: true,
|
|
3421
4323
|
persistent: true,
|
|
3422
4324
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3442,23 +4344,23 @@ async function watch(config, onChange) {
|
|
|
3442
4344
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3443
4345
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3444
4346
|
let contractsDebounceTimer;
|
|
3445
|
-
const
|
|
4347
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4348
|
+
const contractsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.contracts.glob), {
|
|
3446
4349
|
ignoreInitial: true,
|
|
3447
4350
|
persistent: true,
|
|
3448
4351
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3449
4352
|
});
|
|
3450
|
-
function scheduleContractsRegenerate() {
|
|
4353
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4354
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3451
4355
|
if (contractsDebounceTimer !== void 0) {
|
|
3452
4356
|
clearTimeout(contractsDebounceTimer);
|
|
3453
4357
|
}
|
|
3454
4358
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3455
4359
|
contractsDebounceTimer = void 0;
|
|
4360
|
+
const changed = [...pendingChangedPaths];
|
|
4361
|
+
pendingChangedPaths.clear();
|
|
3456
4362
|
try {
|
|
3457
|
-
const routes = await
|
|
3458
|
-
cwd: config.codegen.cwd,
|
|
3459
|
-
glob: config.contracts.glob,
|
|
3460
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3461
|
-
});
|
|
4363
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3462
4364
|
await generate(config, routes);
|
|
3463
4365
|
} catch (err) {
|
|
3464
4366
|
console.error(
|
|
@@ -3469,17 +4371,17 @@ async function watch(config, onChange) {
|
|
|
3469
4371
|
onChange?.();
|
|
3470
4372
|
}, config.contracts.debounceMs);
|
|
3471
4373
|
}
|
|
3472
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3473
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3474
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3475
|
-
const formsWatcher = import_chokidar.default.watch((0,
|
|
4374
|
+
contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4375
|
+
contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4376
|
+
contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
4377
|
+
const formsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.forms.watch), {
|
|
3476
4378
|
ignoreInitial: true,
|
|
3477
4379
|
persistent: true,
|
|
3478
4380
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3479
4381
|
});
|
|
3480
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3481
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3482
|
-
formsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
4382
|
+
formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4383
|
+
formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4384
|
+
formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
3483
4385
|
return {
|
|
3484
4386
|
close: async () => {
|
|
3485
4387
|
if (pagesDebounceTimer !== void 0) {
|