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