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