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