@dudousxd/nestjs-codegen 0.4.0 → 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 +37 -0
- package/dist/cli/main.cjs +1097 -161
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1080 -144
- 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 +1070 -119
- 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 +1051 -106
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +1032 -114
- 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 +1026 -108
- 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,10 +323,85 @@ 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
|
+
}
|
|
388
|
+
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
389
|
+
function clearTypeResolutionCaches(project) {
|
|
390
|
+
_findTypeCache.delete(project);
|
|
391
|
+
_resolveNamedRefCache.delete(project);
|
|
392
|
+
}
|
|
313
393
|
function findType(name, sourceFile, project) {
|
|
394
|
+
let byKey = _findTypeCache.get(project);
|
|
395
|
+
if (byKey === void 0) {
|
|
396
|
+
byKey = /* @__PURE__ */ new Map();
|
|
397
|
+
_findTypeCache.set(project, byKey);
|
|
398
|
+
}
|
|
399
|
+
const key = `${sourceFile.getFilePath()}\0${name}`;
|
|
400
|
+
if (byKey.has(key)) return byKey.get(key) ?? null;
|
|
314
401
|
const local = findTypeInFile(name, sourceFile);
|
|
315
|
-
|
|
316
|
-
|
|
402
|
+
const result = local ?? resolveImportedType(name, sourceFile, project);
|
|
403
|
+
byKey.set(key, result);
|
|
404
|
+
return result;
|
|
317
405
|
}
|
|
318
406
|
var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
|
|
319
407
|
function _localDeclForKinds(name, file, kinds) {
|
|
@@ -350,6 +438,26 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
350
438
|
if (_NON_REF_NAMES.has(refName)) return null;
|
|
351
439
|
name = refName;
|
|
352
440
|
}
|
|
441
|
+
return _resolveNamedRef(name, sourceFile, project, opts);
|
|
442
|
+
}
|
|
443
|
+
var _resolveNamedRefCache = /* @__PURE__ */ new WeakMap();
|
|
444
|
+
function _resolveNamedRef(name, sourceFile, project, opts) {
|
|
445
|
+
let byKey = _resolveNamedRefCache.get(project);
|
|
446
|
+
if (byKey === void 0) {
|
|
447
|
+
byKey = /* @__PURE__ */ new Map();
|
|
448
|
+
_resolveNamedRefCache.set(project, byKey);
|
|
449
|
+
}
|
|
450
|
+
const kindsKey = [...opts.kinds].sort().join(",");
|
|
451
|
+
const key = `${sourceFile.getFilePath()}\0${name}\0${kindsKey}\0${opts.allowBareSpecifier ? 1 : 0}`;
|
|
452
|
+
if (byKey.has(key)) {
|
|
453
|
+
const cached = byKey.get(key) ?? null;
|
|
454
|
+
return cached ? { ...cached } : null;
|
|
455
|
+
}
|
|
456
|
+
const computed = _computeNamedRef(name, sourceFile, project, opts);
|
|
457
|
+
byKey.set(key, computed);
|
|
458
|
+
return computed ? { ...computed } : null;
|
|
459
|
+
}
|
|
460
|
+
function _computeNamedRef(name, sourceFile, project, opts) {
|
|
353
461
|
if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
|
|
354
462
|
return { name, filePath: sourceFile.getFilePath() };
|
|
355
463
|
}
|
|
@@ -424,7 +532,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
424
532
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
425
533
|
visiting: /* @__PURE__ */ new Set(),
|
|
426
534
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
427
|
-
depth: 0
|
|
535
|
+
depth: 0,
|
|
536
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
428
537
|
};
|
|
429
538
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
430
539
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -448,11 +557,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
448
557
|
const typeNode = prop.getTypeNode();
|
|
449
558
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
450
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
|
+
}
|
|
451
582
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
452
583
|
if (has("ValidateNested") || typeRefName) {
|
|
584
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
453
585
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
454
586
|
if (childName) {
|
|
455
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
587
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
456
588
|
const wrapArray = has("IsArray") || isArrayType;
|
|
457
589
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
458
590
|
return applyPresence(node2, decorators);
|
|
@@ -577,10 +709,12 @@ function baseFromType(typeText, isArrayType) {
|
|
|
577
709
|
return { kind: "unknown" };
|
|
578
710
|
}
|
|
579
711
|
}
|
|
580
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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);
|
|
584
718
|
ctx.recursiveSchemas.add(reserved);
|
|
585
719
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
586
720
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -599,19 +733,27 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
599
733
|
}
|
|
600
734
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
601
735
|
}
|
|
602
|
-
const existing = ctx.emittedClasses.get(
|
|
736
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
603
737
|
if (existing) return { kind: "ref", name: existing };
|
|
604
|
-
const schemaName = aliasFor(
|
|
738
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
605
739
|
const resolved = findType(className, fromFile, ctx.project);
|
|
606
740
|
if (!resolved || resolved.kind !== "class") {
|
|
607
741
|
return { kind: "object", fields: [], passthrough: true };
|
|
608
742
|
}
|
|
609
|
-
|
|
610
|
-
|
|
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);
|
|
611
752
|
ctx.depth += 1;
|
|
612
753
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
613
754
|
ctx.depth -= 1;
|
|
614
|
-
ctx.visiting.delete(
|
|
755
|
+
ctx.visiting.delete(cacheKey);
|
|
756
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
615
757
|
ctx.named.set(schemaName, childNode);
|
|
616
758
|
return { kind: "ref", name: schemaName };
|
|
617
759
|
}
|
|
@@ -658,6 +800,39 @@ function messageRaw(decorator) {
|
|
|
658
800
|
}
|
|
659
801
|
return void 0;
|
|
660
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
|
+
}
|
|
661
836
|
function resolveTypeFactoryName(decorator) {
|
|
662
837
|
const arg = firstArg(decorator);
|
|
663
838
|
if (!arg) return null;
|
|
@@ -671,6 +846,17 @@ function singularClassName(typeText) {
|
|
|
671
846
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
672
847
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
673
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
|
+
}
|
|
674
860
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
675
861
|
const arg = firstArg(decorator);
|
|
676
862
|
if (!arg) return null;
|
|
@@ -729,17 +915,34 @@ import {
|
|
|
729
915
|
} from "ts-morph";
|
|
730
916
|
|
|
731
917
|
// src/discovery/enum-resolution.ts
|
|
918
|
+
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
919
|
+
function clearEnumCache(project) {
|
|
920
|
+
_enumCache.delete(project);
|
|
921
|
+
}
|
|
732
922
|
function resolveEnumValues(name, sourceFile, project) {
|
|
923
|
+
let byKey = _enumCache.get(project);
|
|
924
|
+
if (byKey === void 0) {
|
|
925
|
+
byKey = /* @__PURE__ */ new Map();
|
|
926
|
+
_enumCache.set(project, byKey);
|
|
927
|
+
}
|
|
928
|
+
const key = `${sourceFile.getFilePath()}\0${name}`;
|
|
929
|
+
if (byKey.has(key)) {
|
|
930
|
+
const cached = byKey.get(key) ?? null;
|
|
931
|
+
return cached ? { values: [...cached.values], numeric: cached.numeric } : null;
|
|
932
|
+
}
|
|
733
933
|
const resolved = findType(name, sourceFile, project);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
934
|
+
let result = null;
|
|
935
|
+
if (resolved && resolved.kind === "enum") {
|
|
936
|
+
let numeric = true;
|
|
937
|
+
const values = resolved.members.map((m) => {
|
|
938
|
+
const parsed = JSON.parse(m);
|
|
939
|
+
if (typeof parsed === "string") numeric = false;
|
|
940
|
+
return String(parsed);
|
|
941
|
+
});
|
|
942
|
+
if (values.length > 0) result = { values, numeric };
|
|
943
|
+
}
|
|
944
|
+
byKey.set(key, result);
|
|
945
|
+
return result ? { values: [...result.values], numeric: result.numeric } : null;
|
|
743
946
|
}
|
|
744
947
|
|
|
745
948
|
// src/discovery/filter-field-types.ts
|
|
@@ -1184,24 +1387,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
1184
1387
|
"Map",
|
|
1185
1388
|
"Set"
|
|
1186
1389
|
]);
|
|
1187
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
1390
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
1188
1391
|
if (depth <= 0) return "unknown";
|
|
1189
1392
|
if (Node5.isArrayTypeNode(typeNode)) {
|
|
1190
1393
|
const elementType = typeNode.getElementTypeNode();
|
|
1191
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
1394
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
1192
1395
|
}
|
|
1193
1396
|
if (Node5.isUnionTypeNode(typeNode)) {
|
|
1194
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
1397
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
1195
1398
|
}
|
|
1196
1399
|
if (Node5.isIntersectionTypeNode(typeNode)) {
|
|
1197
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
1400
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
1198
1401
|
}
|
|
1199
1402
|
if (Node5.isParenthesizedTypeNode(typeNode)) {
|
|
1200
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
1403
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
1201
1404
|
}
|
|
1202
1405
|
if (Node5.isTypeReference(typeNode)) {
|
|
1203
1406
|
const typeName = typeNode.getTypeName();
|
|
1204
1407
|
const name = Node5.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
1408
|
+
const bound = subst.get(name);
|
|
1409
|
+
if (bound !== void 0) return bound;
|
|
1205
1410
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
1206
1411
|
if (name === "Date") return "string";
|
|
1207
1412
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -1209,14 +1414,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1209
1414
|
return "unknown";
|
|
1210
1415
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
1211
1416
|
if (wrapperMode) {
|
|
1212
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
1417
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
1213
1418
|
}
|
|
1214
1419
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
1215
1420
|
return typeNode.getText();
|
|
1216
1421
|
}
|
|
1217
1422
|
const resolved = findType(name, sourceFile, project);
|
|
1218
1423
|
if (resolved) {
|
|
1219
|
-
|
|
1424
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
1425
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
1220
1426
|
}
|
|
1221
1427
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
1222
1428
|
return "unknown";
|
|
@@ -1229,32 +1435,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1229
1435
|
if (kind === SyntaxKind2.AnyKeyword) return "unknown";
|
|
1230
1436
|
return typeNode.getText();
|
|
1231
1437
|
}
|
|
1232
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
1438
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
1233
1439
|
const typeArgs = typeNode.getTypeArguments();
|
|
1234
1440
|
const firstTypeArg = typeArgs[0];
|
|
1235
1441
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1236
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1442
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
1237
1443
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
1238
1444
|
}
|
|
1239
1445
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
1240
1446
|
}
|
|
1241
|
-
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()) {
|
|
1242
1461
|
if (depth < 0) return "unknown";
|
|
1243
1462
|
switch (result.kind) {
|
|
1244
1463
|
case "class":
|
|
1245
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
1464
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
1246
1465
|
case "interface":
|
|
1247
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
1466
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
1248
1467
|
case "typeAlias":
|
|
1249
1468
|
if (result.typeNode) {
|
|
1250
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
1469
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
1251
1470
|
}
|
|
1252
1471
|
return result.text;
|
|
1253
1472
|
case "enum":
|
|
1254
1473
|
return result.members.join(" | ");
|
|
1255
1474
|
}
|
|
1256
1475
|
}
|
|
1257
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
1476
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
1258
1477
|
if (depth < 0) return "unknown";
|
|
1259
1478
|
const lines = [];
|
|
1260
1479
|
for (const prop of decl.getProperties()) {
|
|
@@ -1263,7 +1482,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
1263
1482
|
const propTypeNode = prop.getTypeNode();
|
|
1264
1483
|
let propType = "unknown";
|
|
1265
1484
|
if (propTypeNode) {
|
|
1266
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
1485
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
1267
1486
|
}
|
|
1268
1487
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
1269
1488
|
}
|
|
@@ -1312,7 +1531,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
1312
1531
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
1313
1532
|
}
|
|
1314
1533
|
function extractResponseType(method, sourceFile, project) {
|
|
1315
|
-
const apiResponseDecorator = method.
|
|
1534
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
1316
1535
|
if (apiResponseDecorator) {
|
|
1317
1536
|
const args = apiResponseDecorator.getArguments();
|
|
1318
1537
|
const optsArg = args[0];
|
|
@@ -1341,6 +1560,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1341
1560
|
}
|
|
1342
1561
|
return "unknown";
|
|
1343
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
|
+
}
|
|
1344
1616
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
1345
1617
|
if (!Node5.isIdentifier(node)) return "unknown";
|
|
1346
1618
|
const name = node.getText();
|
|
@@ -1356,17 +1628,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
1356
1628
|
unwrapContainers: true
|
|
1357
1629
|
});
|
|
1358
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
|
+
}
|
|
1359
1663
|
function extractDtoContract(method, sourceFile, project) {
|
|
1360
1664
|
let body = extractBodyType(method, sourceFile, project);
|
|
1361
1665
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
1362
1666
|
const query = extractQueryType(method, sourceFile, project);
|
|
1667
|
+
const streamElement = detectStreamElement(method);
|
|
1668
|
+
const isStream = streamElement !== null;
|
|
1363
1669
|
if (filterInfo && filterInfo.source === "body") {
|
|
1364
1670
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
1365
1671
|
body = body ?? bodyType;
|
|
1366
1672
|
}
|
|
1367
1673
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
1368
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
1369
|
-
|
|
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) {
|
|
1370
1677
|
return null;
|
|
1371
1678
|
}
|
|
1372
1679
|
let bodyRef = null;
|
|
@@ -1380,12 +1687,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
1380
1687
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
1381
1688
|
}
|
|
1382
1689
|
}
|
|
1383
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
1690
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
1384
1691
|
if (returnTypeNode) {
|
|
1385
1692
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
1386
1693
|
}
|
|
1387
|
-
if (!responseRef) {
|
|
1388
|
-
const apiResp = method.
|
|
1694
|
+
if (!responseRef && !isStream) {
|
|
1695
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
1389
1696
|
if (apiResp) {
|
|
1390
1697
|
const args = apiResp.getArguments();
|
|
1391
1698
|
const optsArg = args[0];
|
|
@@ -1427,16 +1734,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
1427
1734
|
query,
|
|
1428
1735
|
body,
|
|
1429
1736
|
response,
|
|
1737
|
+
error: errorInfo?.type ?? null,
|
|
1430
1738
|
params: paramsType,
|
|
1431
1739
|
queryRef,
|
|
1432
1740
|
bodyRef,
|
|
1433
1741
|
responseRef,
|
|
1742
|
+
errorRef: errorInfo?.ref ?? null,
|
|
1434
1743
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
1435
1744
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
1436
1745
|
filterSource: filterInfo?.source ?? null,
|
|
1437
1746
|
formWarnings,
|
|
1438
1747
|
bodySchema,
|
|
1439
|
-
querySchema
|
|
1748
|
+
querySchema,
|
|
1749
|
+
stream: isStream
|
|
1440
1750
|
};
|
|
1441
1751
|
}
|
|
1442
1752
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -1554,6 +1864,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
1554
1864
|
let query = null;
|
|
1555
1865
|
let body = null;
|
|
1556
1866
|
let response = "unknown";
|
|
1867
|
+
let error = null;
|
|
1557
1868
|
let bodyZodText = null;
|
|
1558
1869
|
let queryZodText = null;
|
|
1559
1870
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -1569,25 +1880,27 @@ function parseDefineContractCall(callExpr) {
|
|
|
1569
1880
|
bodyZodText = val.getText();
|
|
1570
1881
|
} else if (propName === "response") {
|
|
1571
1882
|
response = zodAstToTs(val);
|
|
1883
|
+
} else if (propName === "error") {
|
|
1884
|
+
error = zodAstToTs(val);
|
|
1572
1885
|
}
|
|
1573
1886
|
}
|
|
1574
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
1887
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
1575
1888
|
}
|
|
1576
1889
|
|
|
1577
1890
|
// src/discovery/contracts-fast.ts
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1891
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
1892
|
+
return tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
|
|
1893
|
+
}
|
|
1894
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
1582
1895
|
try {
|
|
1583
|
-
|
|
1896
|
+
return new Project({
|
|
1584
1897
|
tsConfigFilePath: tsconfigPath,
|
|
1585
1898
|
skipAddingFilesFromTsConfig: true,
|
|
1586
1899
|
skipLoadingLibFiles: true,
|
|
1587
1900
|
skipFileDependencyResolution: true
|
|
1588
1901
|
});
|
|
1589
1902
|
} catch {
|
|
1590
|
-
|
|
1903
|
+
return new Project({
|
|
1591
1904
|
skipAddingFilesFromTsConfig: true,
|
|
1592
1905
|
skipLoadingLibFiles: true,
|
|
1593
1906
|
skipFileDependencyResolution: true,
|
|
@@ -1598,20 +1911,98 @@ async function discoverContractsFast(opts) {
|
|
|
1598
1911
|
}
|
|
1599
1912
|
});
|
|
1600
1913
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
project.addSourceFileAtPath(f);
|
|
1604
|
-
}
|
|
1605
|
-
const routes = [];
|
|
1914
|
+
}
|
|
1915
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
1606
1916
|
setDiscoveryContext(project, {
|
|
1607
1917
|
projectRoot: cwd,
|
|
1608
1918
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
1609
1919
|
});
|
|
1610
|
-
|
|
1611
|
-
|
|
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));
|
|
1612
1926
|
}
|
|
1613
1927
|
return routes;
|
|
1614
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
|
+
};
|
|
1615
2006
|
function decoratorStringArg(decoratorExpr) {
|
|
1616
2007
|
if (!decoratorExpr) return void 0;
|
|
1617
2008
|
if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -1667,6 +2058,11 @@ function resolveVerb(method) {
|
|
|
1667
2058
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
1668
2059
|
}
|
|
1669
2060
|
}
|
|
2061
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
2062
|
+
if (sseDecorator) {
|
|
2063
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
2064
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
2065
|
+
}
|
|
1670
2066
|
return null;
|
|
1671
2067
|
}
|
|
1672
2068
|
function readAsDecorator(node, label) {
|
|
@@ -1709,7 +2105,17 @@ function buildRoute(args) {
|
|
|
1709
2105
|
};
|
|
1710
2106
|
}
|
|
1711
2107
|
function extractContractRoute(args) {
|
|
1712
|
-
const {
|
|
2108
|
+
const {
|
|
2109
|
+
cls,
|
|
2110
|
+
method,
|
|
2111
|
+
applyContractDecorator,
|
|
2112
|
+
verb,
|
|
2113
|
+
prefix,
|
|
2114
|
+
className,
|
|
2115
|
+
sourceFile,
|
|
2116
|
+
project,
|
|
2117
|
+
seenNames
|
|
2118
|
+
} = args;
|
|
1713
2119
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
1714
2120
|
if (!firstDecoratorArg) return null;
|
|
1715
2121
|
let contractDef = null;
|
|
@@ -1719,18 +2125,19 @@ function extractContractRoute(args) {
|
|
|
1719
2125
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
1720
2126
|
} else if (Node7.isIdentifier(firstDecoratorArg)) {
|
|
1721
2127
|
const identName = firstDecoratorArg.getText();
|
|
1722
|
-
const
|
|
1723
|
-
if (!
|
|
2128
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
2129
|
+
if (!resolvedVar) {
|
|
1724
2130
|
console.warn(
|
|
1725
|
-
`[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`
|
|
1726
2132
|
);
|
|
1727
2133
|
return null;
|
|
1728
2134
|
}
|
|
2135
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
1729
2136
|
const initializer = varDecl.getInitializer();
|
|
1730
2137
|
if (!initializer) return null;
|
|
1731
2138
|
contractDef = parseDefineContractCall(initializer);
|
|
1732
2139
|
if (contractDef && varDecl.isExported()) {
|
|
1733
|
-
const filePath =
|
|
2140
|
+
const filePath = declFile.getFilePath();
|
|
1734
2141
|
if (contractDef.body !== null) {
|
|
1735
2142
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
1736
2143
|
}
|
|
@@ -1763,6 +2170,7 @@ function extractContractRoute(args) {
|
|
|
1763
2170
|
query: contractDef.query,
|
|
1764
2171
|
body: contractDef.body,
|
|
1765
2172
|
response: contractDef.response,
|
|
2173
|
+
error: contractDef.error,
|
|
1766
2174
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
1767
2175
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
1768
2176
|
// would drag server-only deps into the client bundle).
|
|
@@ -1794,15 +2202,18 @@ function extractDtoRoute(args) {
|
|
|
1794
2202
|
query: dtoContract?.query ?? null,
|
|
1795
2203
|
body: dtoContract?.body ?? null,
|
|
1796
2204
|
response: dtoContract?.response ?? "unknown",
|
|
2205
|
+
error: dtoContract?.error ?? null,
|
|
1797
2206
|
queryRef: dtoContract?.queryRef ?? null,
|
|
1798
2207
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
1799
2208
|
responseRef: dtoContract?.responseRef ?? null,
|
|
2209
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
1800
2210
|
filterFields: dtoContract?.filterFields ?? null,
|
|
1801
2211
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
1802
2212
|
filterSource: dtoContract?.filterSource ?? null,
|
|
1803
2213
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
1804
2214
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
1805
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
2215
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
2216
|
+
stream: dtoContract?.stream ?? false
|
|
1806
2217
|
}
|
|
1807
2218
|
});
|
|
1808
2219
|
}
|
|
@@ -1826,6 +2237,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
1826
2237
|
prefix,
|
|
1827
2238
|
className,
|
|
1828
2239
|
sourceFile,
|
|
2240
|
+
project,
|
|
1829
2241
|
seenNames
|
|
1830
2242
|
}) : extractDtoRoute({
|
|
1831
2243
|
cls,
|
|
@@ -1844,8 +2256,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
1844
2256
|
}
|
|
1845
2257
|
|
|
1846
2258
|
// src/generate.ts
|
|
1847
|
-
import { mkdir as
|
|
1848
|
-
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";
|
|
1849
2261
|
|
|
1850
2262
|
// src/discovery/pages.ts
|
|
1851
2263
|
import { readFile } from "fs/promises";
|
|
@@ -2374,17 +2786,28 @@ function emitFilterQueryType(c) {
|
|
|
2374
2786
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
2375
2787
|
}
|
|
2376
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
|
+
}
|
|
2377
2794
|
if (c.controllerRef) {
|
|
2378
2795
|
let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
2379
2796
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
2380
2797
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
2381
2798
|
}
|
|
2382
|
-
const respRef = c.contractSource.responseRef;
|
|
2383
2799
|
if (respRef) {
|
|
2384
2800
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
2385
2801
|
}
|
|
2386
2802
|
return c.contractSource.response;
|
|
2387
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
|
+
}
|
|
2388
2811
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
2389
2812
|
const pad = " ".repeat(indent);
|
|
2390
2813
|
const lines = [];
|
|
@@ -2399,12 +2822,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2399
2822
|
const bodyRef = c.contractSource.bodyRef;
|
|
2400
2823
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
2401
2824
|
const response = buildResponseType(c, outDir);
|
|
2825
|
+
const error = buildErrorType(c);
|
|
2402
2826
|
const params = buildParamsType(c.params);
|
|
2403
2827
|
const safeMethod = JSON.stringify(method);
|
|
2404
2828
|
const safeUrl = JSON.stringify(c.path);
|
|
2405
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";
|
|
2406
2831
|
lines.push(
|
|
2407
|
-
`${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} };`
|
|
2408
2833
|
);
|
|
2409
2834
|
} else {
|
|
2410
2835
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -2476,15 +2901,21 @@ function emitReqHelper() {
|
|
|
2476
2901
|
""
|
|
2477
2902
|
];
|
|
2478
2903
|
}
|
|
2479
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
2904
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
2480
2905
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
2481
2906
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
2907
|
+
if (streamExpr) {
|
|
2908
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
2909
|
+
}
|
|
2482
2910
|
for (const [name, value] of Object.entries(members)) {
|
|
2483
2911
|
lines.push(`${pad} ${name}: ${value},`);
|
|
2484
2912
|
}
|
|
2485
2913
|
lines.push(`${pad}}),`);
|
|
2486
2914
|
return lines;
|
|
2487
2915
|
}
|
|
2916
|
+
function renderStreamExpr(req) {
|
|
2917
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
2918
|
+
}
|
|
2488
2919
|
function emitApiObjectBlock(tree, indent, p) {
|
|
2489
2920
|
const pad = " ".repeat(indent);
|
|
2490
2921
|
const lines = [];
|
|
@@ -2519,7 +2950,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2519
2950
|
}
|
|
2520
2951
|
const members = {};
|
|
2521
2952
|
for (const [name, { value }] of owned) members[name] = value;
|
|
2522
|
-
|
|
2953
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
2954
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
2523
2955
|
}
|
|
2524
2956
|
return lines;
|
|
2525
2957
|
}
|
|
@@ -2557,6 +2989,8 @@ var ROUTE_NAMESPACE = [
|
|
|
2557
2989
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
2558
2990
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
2559
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">;',
|
|
2560
2994
|
" export type Request<K extends string> = {",
|
|
2561
2995
|
" body: Body<K>;",
|
|
2562
2996
|
" query: Query<K>;",
|
|
@@ -2573,6 +3007,7 @@ var PATH_NAMESPACE = [
|
|
|
2573
3007
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
2574
3008
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
2575
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">;',
|
|
2576
3011
|
"}",
|
|
2577
3012
|
""
|
|
2578
3013
|
];
|
|
@@ -2584,6 +3019,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
2584
3019
|
" export type Params<K extends string> = never;",
|
|
2585
3020
|
" export type Error<K extends string> = never;",
|
|
2586
3021
|
" export type FilterFields<K extends string> = never;",
|
|
3022
|
+
" export type Stream<K extends string> = never;",
|
|
2587
3023
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
2588
3024
|
"}",
|
|
2589
3025
|
""
|
|
@@ -2596,6 +3032,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
2596
3032
|
" export type Params<M extends string, U extends string> = never;",
|
|
2597
3033
|
" export type Error<M extends string, U extends string> = never;",
|
|
2598
3034
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
3035
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
2599
3036
|
"}",
|
|
2600
3037
|
""
|
|
2601
3038
|
];
|
|
@@ -2619,7 +3056,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
2619
3056
|
for (const r of contracted) {
|
|
2620
3057
|
const cs = r.contract?.contractSource;
|
|
2621
3058
|
if (!cs) continue;
|
|
2622
|
-
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];
|
|
2623
3060
|
for (const ref of refs) {
|
|
2624
3061
|
if (!ref) continue;
|
|
2625
3062
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -3052,11 +3489,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
3052
3489
|
await writeFile4(join8(outDir, "index.d.ts"), content, "utf8");
|
|
3053
3490
|
}
|
|
3054
3491
|
|
|
3055
|
-
// src/emit/emit-
|
|
3492
|
+
// src/emit/emit-mocks.ts
|
|
3056
3493
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
3057
|
-
import { join as join9
|
|
3058
|
-
|
|
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 = {}) {
|
|
3059
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 });
|
|
3060
3953
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
3061
3954
|
const augBody = pages.map((p) => {
|
|
3062
3955
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -3075,7 +3968,7 @@ ${augBody}
|
|
|
3075
3968
|
}
|
|
3076
3969
|
${sharedPropsBlock}}
|
|
3077
3970
|
`;
|
|
3078
|
-
await
|
|
3971
|
+
await writeFile7(join11(outDir, "pages.d.ts"), content, "utf8");
|
|
3079
3972
|
}
|
|
3080
3973
|
function buildSharedPropsBlock(sharedProps) {
|
|
3081
3974
|
if (!sharedProps) return "";
|
|
@@ -3105,12 +3998,12 @@ function needsQuotes(name) {
|
|
|
3105
3998
|
}
|
|
3106
3999
|
|
|
3107
4000
|
// src/emit/emit-routes.ts
|
|
3108
|
-
import { mkdir as
|
|
3109
|
-
import { join as
|
|
4001
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
4002
|
+
import { join as join12 } from "path";
|
|
3110
4003
|
async function emitRoutes(routes, outDir) {
|
|
3111
|
-
await
|
|
4004
|
+
await mkdir8(outDir, { recursive: true });
|
|
3112
4005
|
const content = buildRoutesFile(routes);
|
|
3113
|
-
await
|
|
4006
|
+
await writeFile8(join12(outDir, "routes.ts"), content, "utf8");
|
|
3114
4007
|
}
|
|
3115
4008
|
function buildRoutesFile(routes) {
|
|
3116
4009
|
if (routes.length === 0) {
|
|
@@ -3258,21 +4151,38 @@ async function generate(config, inputRoutes = []) {
|
|
|
3258
4151
|
});
|
|
3259
4152
|
}
|
|
3260
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
|
+
}
|
|
3261
4171
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
3262
4172
|
if (extensions.length > 0) {
|
|
3263
4173
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
3264
4174
|
for (const file of extraFiles) {
|
|
3265
|
-
const dest =
|
|
3266
|
-
await
|
|
3267
|
-
await
|
|
4175
|
+
const dest = join13(config.codegen.outDir, file.path);
|
|
4176
|
+
await mkdir9(dirname3(dest), { recursive: true });
|
|
4177
|
+
await writeFile9(dest, file.contents, "utf8");
|
|
3268
4178
|
}
|
|
3269
4179
|
}
|
|
3270
4180
|
}
|
|
3271
4181
|
|
|
3272
4182
|
// src/watch/lock-file.ts
|
|
3273
4183
|
import { open } from "fs/promises";
|
|
3274
|
-
import { mkdir as
|
|
3275
|
-
import { join as
|
|
4184
|
+
import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
|
|
4185
|
+
import { join as join14 } from "path";
|
|
3276
4186
|
var LOCK_FILE = ".watcher.lock";
|
|
3277
4187
|
function isProcessAlive(pid) {
|
|
3278
4188
|
try {
|
|
@@ -3283,8 +4193,8 @@ function isProcessAlive(pid) {
|
|
|
3283
4193
|
}
|
|
3284
4194
|
}
|
|
3285
4195
|
async function acquireLock(outDir) {
|
|
3286
|
-
await
|
|
3287
|
-
const lockPath =
|
|
4196
|
+
await mkdir10(outDir, { recursive: true });
|
|
4197
|
+
const lockPath = join14(outDir, LOCK_FILE);
|
|
3288
4198
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3289
4199
|
try {
|
|
3290
4200
|
const fd = await open(lockPath, "wx");
|
|
@@ -3324,7 +4234,7 @@ async function watch(config, onChange) {
|
|
|
3324
4234
|
if (lock === null) {
|
|
3325
4235
|
let holderPid = "unknown";
|
|
3326
4236
|
try {
|
|
3327
|
-
const raw = await readFile3(
|
|
4237
|
+
const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3328
4238
|
const data = JSON.parse(raw);
|
|
3329
4239
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3330
4240
|
} catch {
|
|
@@ -3334,12 +4244,20 @@ async function watch(config, onChange) {
|
|
|
3334
4244
|
);
|
|
3335
4245
|
return NO_OP_WATCHER;
|
|
3336
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
|
+
}
|
|
3337
4259
|
try {
|
|
3338
|
-
const initialRoutes = await
|
|
3339
|
-
cwd: config.codegen.cwd,
|
|
3340
|
-
glob: config.contracts.glob,
|
|
3341
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3342
|
-
});
|
|
4260
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3343
4261
|
await generate(config, initialRoutes);
|
|
3344
4262
|
} catch (err) {
|
|
3345
4263
|
console.warn(
|
|
@@ -3352,7 +4270,7 @@ async function watch(config, onChange) {
|
|
|
3352
4270
|
}
|
|
3353
4271
|
let pagesDebounceTimer;
|
|
3354
4272
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3355
|
-
const pagesWatcher = chokidar.watch(
|
|
4273
|
+
const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
|
|
3356
4274
|
ignoreInitial: true,
|
|
3357
4275
|
persistent: true,
|
|
3358
4276
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3378,23 +4296,23 @@ async function watch(config, onChange) {
|
|
|
3378
4296
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3379
4297
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3380
4298
|
let contractsDebounceTimer;
|
|
3381
|
-
const
|
|
4299
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4300
|
+
const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
|
|
3382
4301
|
ignoreInitial: true,
|
|
3383
4302
|
persistent: true,
|
|
3384
4303
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3385
4304
|
});
|
|
3386
|
-
function scheduleContractsRegenerate() {
|
|
4305
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4306
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3387
4307
|
if (contractsDebounceTimer !== void 0) {
|
|
3388
4308
|
clearTimeout(contractsDebounceTimer);
|
|
3389
4309
|
}
|
|
3390
4310
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3391
4311
|
contractsDebounceTimer = void 0;
|
|
4312
|
+
const changed = [...pendingChangedPaths];
|
|
4313
|
+
pendingChangedPaths.clear();
|
|
3392
4314
|
try {
|
|
3393
|
-
const routes = await
|
|
3394
|
-
cwd: config.codegen.cwd,
|
|
3395
|
-
glob: config.contracts.glob,
|
|
3396
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3397
|
-
});
|
|
4315
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3398
4316
|
await generate(config, routes);
|
|
3399
4317
|
} catch (err) {
|
|
3400
4318
|
console.error(
|
|
@@ -3405,17 +4323,17 @@ async function watch(config, onChange) {
|
|
|
3405
4323
|
onChange?.();
|
|
3406
4324
|
}, config.contracts.debounceMs);
|
|
3407
4325
|
}
|
|
3408
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3409
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3410
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3411
|
-
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), {
|
|
3412
4330
|
ignoreInitial: true,
|
|
3413
4331
|
persistent: true,
|
|
3414
4332
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3415
4333
|
});
|
|
3416
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3417
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3418
|
-
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));
|
|
3419
4337
|
return {
|
|
3420
4338
|
close: async () => {
|
|
3421
4339
|
if (pagesDebounceTimer !== void 0) {
|