@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.cjs
CHANGED
|
@@ -154,13 +154,26 @@ function applyDefaults(userConfig, cwd) {
|
|
|
154
154
|
enabled: userConfig.forms?.enabled ?? true,
|
|
155
155
|
watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
|
|
156
156
|
zodImport: userConfig.forms?.zodImport ?? "zod"
|
|
157
|
+
},
|
|
158
|
+
openapi: {
|
|
159
|
+
enabled: userConfig.openapi?.enabled ?? false,
|
|
160
|
+
fileName: userConfig.openapi?.fileName ?? "openapi.json",
|
|
161
|
+
title: userConfig.openapi?.title ?? "NestJS API",
|
|
162
|
+
version: userConfig.openapi?.version ?? "1.0.0",
|
|
163
|
+
description: userConfig.openapi?.description ?? null
|
|
164
|
+
},
|
|
165
|
+
mocks: {
|
|
166
|
+
enabled: userConfig.mocks?.enabled ?? false,
|
|
167
|
+
fileName: userConfig.mocks?.fileName ?? "mocks.ts",
|
|
168
|
+
seed: userConfig.mocks?.seed ?? 1,
|
|
169
|
+
baseUrl: userConfig.mocks?.baseUrl ?? ""
|
|
157
170
|
}
|
|
158
171
|
};
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
// src/watch/watcher.ts
|
|
162
|
-
var
|
|
163
|
-
var
|
|
175
|
+
var import_promises14 = require("fs/promises");
|
|
176
|
+
var import_node_path16 = require("path");
|
|
164
177
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
165
178
|
|
|
166
179
|
// src/discovery/contracts-fast.ts
|
|
@@ -336,10 +349,85 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
|
|
|
336
349
|
}
|
|
337
350
|
return null;
|
|
338
351
|
}
|
|
352
|
+
function resolveImportedVariable(name, sourceFile, project) {
|
|
353
|
+
const local = sourceFile.getVariableDeclaration(name);
|
|
354
|
+
if (local) return { decl: local, file: sourceFile };
|
|
355
|
+
return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
|
|
356
|
+
}
|
|
357
|
+
function resolveVariableViaImports(name, sourceFile, project, seen) {
|
|
358
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
359
|
+
const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
|
|
360
|
+
if (!namedImport) continue;
|
|
361
|
+
const sourceName = namedImport.getName();
|
|
362
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
363
|
+
const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
|
|
364
|
+
if (found) return found;
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
|
|
369
|
+
const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
|
|
370
|
+
for (const candidate of candidates) {
|
|
371
|
+
let importedFile = project.getSourceFile(candidate);
|
|
372
|
+
if (!importedFile) {
|
|
373
|
+
try {
|
|
374
|
+
importedFile = project.addSourceFileAtPath(candidate);
|
|
375
|
+
} catch {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const found = resolveVariableInFile(name, importedFile, project, seen);
|
|
380
|
+
if (found) return found;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
function resolveVariableInFile(name, file, project, seen) {
|
|
385
|
+
const filePath = file.getFilePath();
|
|
386
|
+
if (seen.has(filePath)) return null;
|
|
387
|
+
seen.add(filePath);
|
|
388
|
+
const local = file.getVariableDeclaration(name);
|
|
389
|
+
if (local) return { decl: local, file };
|
|
390
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
391
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
392
|
+
const namedExports = exportDecl.getNamedExports();
|
|
393
|
+
if (moduleSpecifier) {
|
|
394
|
+
const hasStar = namedExports.length === 0;
|
|
395
|
+
const reExport2 = namedExports.find(
|
|
396
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
397
|
+
);
|
|
398
|
+
if (!hasStar && !reExport2) continue;
|
|
399
|
+
const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
|
|
400
|
+
const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
|
|
401
|
+
if (found) return found;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const reExport = namedExports.find(
|
|
405
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
406
|
+
);
|
|
407
|
+
if (!reExport) continue;
|
|
408
|
+
const sourceName = reExport.getName();
|
|
409
|
+
const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
|
|
410
|
+
if (viaImports) return viaImports;
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
415
|
+
function clearTypeResolutionCaches(project) {
|
|
416
|
+
_findTypeCache.delete(project);
|
|
417
|
+
_resolveNamedRefCache.delete(project);
|
|
418
|
+
}
|
|
339
419
|
function findType(name, sourceFile, project) {
|
|
420
|
+
let byKey = _findTypeCache.get(project);
|
|
421
|
+
if (byKey === void 0) {
|
|
422
|
+
byKey = /* @__PURE__ */ new Map();
|
|
423
|
+
_findTypeCache.set(project, byKey);
|
|
424
|
+
}
|
|
425
|
+
const key = `${sourceFile.getFilePath()}\0${name}`;
|
|
426
|
+
if (byKey.has(key)) return byKey.get(key) ?? null;
|
|
340
427
|
const local = findTypeInFile(name, sourceFile);
|
|
341
|
-
|
|
342
|
-
|
|
428
|
+
const result = local ?? resolveImportedType(name, sourceFile, project);
|
|
429
|
+
byKey.set(key, result);
|
|
430
|
+
return result;
|
|
343
431
|
}
|
|
344
432
|
var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
|
|
345
433
|
function _localDeclForKinds(name, file, kinds) {
|
|
@@ -376,6 +464,26 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
376
464
|
if (_NON_REF_NAMES.has(refName)) return null;
|
|
377
465
|
name = refName;
|
|
378
466
|
}
|
|
467
|
+
return _resolveNamedRef(name, sourceFile, project, opts);
|
|
468
|
+
}
|
|
469
|
+
var _resolveNamedRefCache = /* @__PURE__ */ new WeakMap();
|
|
470
|
+
function _resolveNamedRef(name, sourceFile, project, opts) {
|
|
471
|
+
let byKey = _resolveNamedRefCache.get(project);
|
|
472
|
+
if (byKey === void 0) {
|
|
473
|
+
byKey = /* @__PURE__ */ new Map();
|
|
474
|
+
_resolveNamedRefCache.set(project, byKey);
|
|
475
|
+
}
|
|
476
|
+
const kindsKey = [...opts.kinds].sort().join(",");
|
|
477
|
+
const key = `${sourceFile.getFilePath()}\0${name}\0${kindsKey}\0${opts.allowBareSpecifier ? 1 : 0}`;
|
|
478
|
+
if (byKey.has(key)) {
|
|
479
|
+
const cached = byKey.get(key) ?? null;
|
|
480
|
+
return cached ? { ...cached } : null;
|
|
481
|
+
}
|
|
482
|
+
const computed = _computeNamedRef(name, sourceFile, project, opts);
|
|
483
|
+
byKey.set(key, computed);
|
|
484
|
+
return computed ? { ...computed } : null;
|
|
485
|
+
}
|
|
486
|
+
function _computeNamedRef(name, sourceFile, project, opts) {
|
|
379
487
|
if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
|
|
380
488
|
return { name, filePath: sourceFile.getFilePath() };
|
|
381
489
|
}
|
|
@@ -450,7 +558,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
450
558
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
451
559
|
visiting: /* @__PURE__ */ new Set(),
|
|
452
560
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
453
|
-
depth: 0
|
|
561
|
+
depth: 0,
|
|
562
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
454
563
|
};
|
|
455
564
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
456
565
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -474,11 +583,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
474
583
|
const typeNode = prop.getTypeNode();
|
|
475
584
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
476
585
|
const isArrayType = !!typeNode && import_ts_morph2.Node.isArrayTypeNode(typeNode);
|
|
586
|
+
const discriminator = resolveDiscriminator(dec("Type"));
|
|
587
|
+
if (discriminator) {
|
|
588
|
+
const options = discriminator.subTypes.map(
|
|
589
|
+
(name) => buildNestedReference(name, classFile, ctx)
|
|
590
|
+
);
|
|
591
|
+
const unionNode = {
|
|
592
|
+
kind: "union",
|
|
593
|
+
options,
|
|
594
|
+
discriminator: discriminator.property
|
|
595
|
+
};
|
|
596
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
597
|
+
const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
|
|
598
|
+
return applyPresence(node2, decorators);
|
|
599
|
+
}
|
|
600
|
+
const propTypeParam = singularClassName(typeText);
|
|
601
|
+
if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
|
|
602
|
+
const bound = ctx.typeBindings.get(propTypeParam);
|
|
603
|
+
const childNode = buildNestedReference(bound, classFile, ctx);
|
|
604
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
605
|
+
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
606
|
+
return applyPresence(node2, decorators);
|
|
607
|
+
}
|
|
477
608
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
478
609
|
if (has("ValidateNested") || typeRefName) {
|
|
610
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
479
611
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
480
612
|
if (childName) {
|
|
481
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
613
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
482
614
|
const wrapArray = has("IsArray") || isArrayType;
|
|
483
615
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
484
616
|
return applyPresence(node2, decorators);
|
|
@@ -603,10 +735,12 @@ function baseFromType(typeText, isArrayType) {
|
|
|
603
735
|
return { kind: "unknown" };
|
|
604
736
|
}
|
|
605
737
|
}
|
|
606
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
738
|
+
function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
|
|
739
|
+
const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
|
|
740
|
+
const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
|
|
741
|
+
if (ctx.visiting.has(cacheKey)) {
|
|
742
|
+
const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
|
|
743
|
+
ctx.emittedClasses.set(cacheKey, reserved);
|
|
610
744
|
ctx.recursiveSchemas.add(reserved);
|
|
611
745
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
612
746
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -625,19 +759,27 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
625
759
|
}
|
|
626
760
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
627
761
|
}
|
|
628
|
-
const existing = ctx.emittedClasses.get(
|
|
762
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
629
763
|
if (existing) return { kind: "ref", name: existing };
|
|
630
|
-
const schemaName = aliasFor(
|
|
764
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
631
765
|
const resolved = findType(className, fromFile, ctx.project);
|
|
632
766
|
if (!resolved || resolved.kind !== "class") {
|
|
633
767
|
return { kind: "object", fields: [], passthrough: true };
|
|
634
768
|
}
|
|
635
|
-
|
|
636
|
-
|
|
769
|
+
const params = resolved.decl.getTypeParameters().map((p) => p.getName());
|
|
770
|
+
const newBindings = [];
|
|
771
|
+
params.forEach((param, i) => {
|
|
772
|
+
const arg = typeArgs[i];
|
|
773
|
+
if (arg) newBindings.push([param, arg]);
|
|
774
|
+
});
|
|
775
|
+
for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
|
|
776
|
+
ctx.emittedClasses.set(cacheKey, schemaName);
|
|
777
|
+
ctx.visiting.add(cacheKey);
|
|
637
778
|
ctx.depth += 1;
|
|
638
779
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
639
780
|
ctx.depth -= 1;
|
|
640
|
-
ctx.visiting.delete(
|
|
781
|
+
ctx.visiting.delete(cacheKey);
|
|
782
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
641
783
|
ctx.named.set(schemaName, childNode);
|
|
642
784
|
return { kind: "ref", name: schemaName };
|
|
643
785
|
}
|
|
@@ -684,6 +826,39 @@ function messageRaw(decorator) {
|
|
|
684
826
|
}
|
|
685
827
|
return void 0;
|
|
686
828
|
}
|
|
829
|
+
function resolveDiscriminator(decorator) {
|
|
830
|
+
const optsArg = decorator?.getArguments()[1];
|
|
831
|
+
if (!optsArg || !import_ts_morph2.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
832
|
+
let discProp;
|
|
833
|
+
for (const prop of optsArg.getProperties()) {
|
|
834
|
+
if (import_ts_morph2.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
|
|
835
|
+
discProp = prop.getInitializer();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (!discProp || !import_ts_morph2.Node.isObjectLiteralExpression(discProp)) return null;
|
|
839
|
+
let property = null;
|
|
840
|
+
const subTypes = [];
|
|
841
|
+
for (const prop of discProp.getProperties()) {
|
|
842
|
+
if (!import_ts_morph2.Node.isPropertyAssignment(prop)) continue;
|
|
843
|
+
const name = prop.getName();
|
|
844
|
+
const init = prop.getInitializer();
|
|
845
|
+
if (!init) continue;
|
|
846
|
+
if (name === "property" && import_ts_morph2.Node.isStringLiteral(init)) {
|
|
847
|
+
property = init.getLiteralValue();
|
|
848
|
+
} else if (name === "subTypes" && import_ts_morph2.Node.isArrayLiteralExpression(init)) {
|
|
849
|
+
for (const el of init.getElements()) {
|
|
850
|
+
if (!import_ts_morph2.Node.isObjectLiteralExpression(el)) continue;
|
|
851
|
+
for (const p of el.getProperties()) {
|
|
852
|
+
if (!import_ts_morph2.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
|
|
853
|
+
const nameInit = p.getInitializer();
|
|
854
|
+
if (nameInit && import_ts_morph2.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (!property || subTypes.length === 0) return null;
|
|
860
|
+
return { property, subTypes };
|
|
861
|
+
}
|
|
687
862
|
function resolveTypeFactoryName(decorator) {
|
|
688
863
|
const arg = firstArg(decorator);
|
|
689
864
|
if (!arg) return null;
|
|
@@ -697,6 +872,17 @@ function singularClassName(typeText) {
|
|
|
697
872
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
698
873
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
699
874
|
}
|
|
875
|
+
function genericTypeArgNames(typeNode) {
|
|
876
|
+
if (!typeNode || !import_ts_morph2.Node.isTypeReference(typeNode)) return [];
|
|
877
|
+
const names = [];
|
|
878
|
+
for (const arg of typeNode.getTypeArguments()) {
|
|
879
|
+
if (!import_ts_morph2.Node.isTypeReference(arg)) return [];
|
|
880
|
+
const tn = arg.getTypeName();
|
|
881
|
+
if (!import_ts_morph2.Node.isIdentifier(tn)) return [];
|
|
882
|
+
names.push(tn.getText());
|
|
883
|
+
}
|
|
884
|
+
return names;
|
|
885
|
+
}
|
|
700
886
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
701
887
|
const arg = firstArg(decorator);
|
|
702
888
|
if (!arg) return null;
|
|
@@ -750,17 +936,34 @@ var import_ts_morph4 = require("ts-morph");
|
|
|
750
936
|
var import_ts_morph3 = require("ts-morph");
|
|
751
937
|
|
|
752
938
|
// src/discovery/enum-resolution.ts
|
|
939
|
+
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
940
|
+
function clearEnumCache(project) {
|
|
941
|
+
_enumCache.delete(project);
|
|
942
|
+
}
|
|
753
943
|
function resolveEnumValues(name, sourceFile, project) {
|
|
944
|
+
let byKey = _enumCache.get(project);
|
|
945
|
+
if (byKey === void 0) {
|
|
946
|
+
byKey = /* @__PURE__ */ new Map();
|
|
947
|
+
_enumCache.set(project, byKey);
|
|
948
|
+
}
|
|
949
|
+
const key = `${sourceFile.getFilePath()}\0${name}`;
|
|
950
|
+
if (byKey.has(key)) {
|
|
951
|
+
const cached = byKey.get(key) ?? null;
|
|
952
|
+
return cached ? { values: [...cached.values], numeric: cached.numeric } : null;
|
|
953
|
+
}
|
|
754
954
|
const resolved = findType(name, sourceFile, project);
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
955
|
+
let result = null;
|
|
956
|
+
if (resolved && resolved.kind === "enum") {
|
|
957
|
+
let numeric = true;
|
|
958
|
+
const values = resolved.members.map((m) => {
|
|
959
|
+
const parsed = JSON.parse(m);
|
|
960
|
+
if (typeof parsed === "string") numeric = false;
|
|
961
|
+
return String(parsed);
|
|
962
|
+
});
|
|
963
|
+
if (values.length > 0) result = { values, numeric };
|
|
964
|
+
}
|
|
965
|
+
byKey.set(key, result);
|
|
966
|
+
return result ? { values: [...result.values], numeric: result.numeric } : null;
|
|
764
967
|
}
|
|
765
968
|
|
|
766
969
|
// src/discovery/filter-field-types.ts
|
|
@@ -1205,24 +1408,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
1205
1408
|
"Map",
|
|
1206
1409
|
"Set"
|
|
1207
1410
|
]);
|
|
1208
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
1411
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
1209
1412
|
if (depth <= 0) return "unknown";
|
|
1210
1413
|
if (import_ts_morph5.Node.isArrayTypeNode(typeNode)) {
|
|
1211
1414
|
const elementType = typeNode.getElementTypeNode();
|
|
1212
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
1415
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
1213
1416
|
}
|
|
1214
1417
|
if (import_ts_morph5.Node.isUnionTypeNode(typeNode)) {
|
|
1215
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
1418
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
1216
1419
|
}
|
|
1217
1420
|
if (import_ts_morph5.Node.isIntersectionTypeNode(typeNode)) {
|
|
1218
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
1421
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
1219
1422
|
}
|
|
1220
1423
|
if (import_ts_morph5.Node.isParenthesizedTypeNode(typeNode)) {
|
|
1221
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
1424
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
1222
1425
|
}
|
|
1223
1426
|
if (import_ts_morph5.Node.isTypeReference(typeNode)) {
|
|
1224
1427
|
const typeName = typeNode.getTypeName();
|
|
1225
1428
|
const name = import_ts_morph5.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
1429
|
+
const bound = subst.get(name);
|
|
1430
|
+
if (bound !== void 0) return bound;
|
|
1226
1431
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
1227
1432
|
if (name === "Date") return "string";
|
|
1228
1433
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -1230,14 +1435,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1230
1435
|
return "unknown";
|
|
1231
1436
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
1232
1437
|
if (wrapperMode) {
|
|
1233
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
1438
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
1234
1439
|
}
|
|
1235
1440
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
1236
1441
|
return typeNode.getText();
|
|
1237
1442
|
}
|
|
1238
1443
|
const resolved = findType(name, sourceFile, project);
|
|
1239
1444
|
if (resolved) {
|
|
1240
|
-
|
|
1445
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
1446
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
1241
1447
|
}
|
|
1242
1448
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
1243
1449
|
return "unknown";
|
|
@@ -1250,32 +1456,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1250
1456
|
if (kind === import_ts_morph5.SyntaxKind.AnyKeyword) return "unknown";
|
|
1251
1457
|
return typeNode.getText();
|
|
1252
1458
|
}
|
|
1253
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
1459
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
1254
1460
|
const typeArgs = typeNode.getTypeArguments();
|
|
1255
1461
|
const firstTypeArg = typeArgs[0];
|
|
1256
1462
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1257
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1463
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
1258
1464
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
1259
1465
|
}
|
|
1260
1466
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
1261
1467
|
}
|
|
1262
|
-
function
|
|
1468
|
+
function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
|
|
1469
|
+
if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
|
|
1470
|
+
const params = result.decl.getTypeParameters().map((p) => p.getName());
|
|
1471
|
+
if (params.length === 0) return /* @__PURE__ */ new Map();
|
|
1472
|
+
const args = typeNode.getTypeArguments();
|
|
1473
|
+
const subst = /* @__PURE__ */ new Map();
|
|
1474
|
+
params.forEach((param, i) => {
|
|
1475
|
+
const arg = args[i];
|
|
1476
|
+
if (arg)
|
|
1477
|
+
subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
|
|
1478
|
+
});
|
|
1479
|
+
return subst;
|
|
1480
|
+
}
|
|
1481
|
+
function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
1263
1482
|
if (depth < 0) return "unknown";
|
|
1264
1483
|
switch (result.kind) {
|
|
1265
1484
|
case "class":
|
|
1266
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
1485
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
1267
1486
|
case "interface":
|
|
1268
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
1487
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
1269
1488
|
case "typeAlias":
|
|
1270
1489
|
if (result.typeNode) {
|
|
1271
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
1490
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
1272
1491
|
}
|
|
1273
1492
|
return result.text;
|
|
1274
1493
|
case "enum":
|
|
1275
1494
|
return result.members.join(" | ");
|
|
1276
1495
|
}
|
|
1277
1496
|
}
|
|
1278
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
1497
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
1279
1498
|
if (depth < 0) return "unknown";
|
|
1280
1499
|
const lines = [];
|
|
1281
1500
|
for (const prop of decl.getProperties()) {
|
|
@@ -1284,7 +1503,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
1284
1503
|
const propTypeNode = prop.getTypeNode();
|
|
1285
1504
|
let propType = "unknown";
|
|
1286
1505
|
if (propTypeNode) {
|
|
1287
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
1506
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
1288
1507
|
}
|
|
1289
1508
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
1290
1509
|
}
|
|
@@ -1333,7 +1552,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
1333
1552
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
1334
1553
|
}
|
|
1335
1554
|
function extractResponseType(method, sourceFile, project) {
|
|
1336
|
-
const apiResponseDecorator = method.
|
|
1555
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
1337
1556
|
if (apiResponseDecorator) {
|
|
1338
1557
|
const args = apiResponseDecorator.getArguments();
|
|
1339
1558
|
const optsArg = args[0];
|
|
@@ -1362,6 +1581,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1362
1581
|
}
|
|
1363
1582
|
return "unknown";
|
|
1364
1583
|
}
|
|
1584
|
+
function apiResponseStatus(decorator) {
|
|
1585
|
+
const optsArg = decorator.getArguments()[0];
|
|
1586
|
+
if (!optsArg || !import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
1587
|
+
for (const prop of optsArg.getProperties()) {
|
|
1588
|
+
if (!import_ts_morph5.Node.isPropertyAssignment(prop)) continue;
|
|
1589
|
+
if (prop.getName() !== "status") continue;
|
|
1590
|
+
const val = prop.getInitializer();
|
|
1591
|
+
if (val && import_ts_morph5.Node.isNumericLiteral(val)) return Number(val.getLiteralValue());
|
|
1592
|
+
}
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
function apiResponseTypeNode(decorator) {
|
|
1596
|
+
const optsArg = decorator.getArguments()[0];
|
|
1597
|
+
if (!optsArg || !import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
1598
|
+
for (const prop of optsArg.getProperties()) {
|
|
1599
|
+
if (!import_ts_morph5.Node.isPropertyAssignment(prop)) continue;
|
|
1600
|
+
if (prop.getName() !== "type") continue;
|
|
1601
|
+
const val = prop.getInitializer();
|
|
1602
|
+
if (!val) return null;
|
|
1603
|
+
if (import_ts_morph5.Node.isArrayLiteralExpression(val)) {
|
|
1604
|
+
const first = val.getElements()[0];
|
|
1605
|
+
return first ? { node: first, isArray: true } : null;
|
|
1606
|
+
}
|
|
1607
|
+
return { node: val, isArray: false };
|
|
1608
|
+
}
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
function extractErrorType(method, sourceFile, project) {
|
|
1612
|
+
for (const decorator of method.getDecorators()) {
|
|
1613
|
+
if (decorator.getName() !== "ApiResponse") continue;
|
|
1614
|
+
const status = apiResponseStatus(decorator);
|
|
1615
|
+
if (status === null || status < 400) continue;
|
|
1616
|
+
const typeInfo = apiResponseTypeNode(decorator);
|
|
1617
|
+
if (!typeInfo) continue;
|
|
1618
|
+
const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
|
|
1619
|
+
const type = typeInfo.isArray ? `Array<${inner}>` : inner;
|
|
1620
|
+
let ref = null;
|
|
1621
|
+
if (import_ts_morph5.Node.isIdentifier(typeInfo.node)) {
|
|
1622
|
+
const name = typeInfo.node.getText();
|
|
1623
|
+
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
1624
|
+
if (localDecl?.isExported()) {
|
|
1625
|
+
ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
|
|
1626
|
+
} else {
|
|
1627
|
+
const resolved = resolveImportedType(name, sourceFile, project);
|
|
1628
|
+
if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
|
|
1629
|
+
ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return { type, ref };
|
|
1634
|
+
}
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1365
1637
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
1366
1638
|
if (!import_ts_morph5.Node.isIdentifier(node)) return "unknown";
|
|
1367
1639
|
const name = node.getText();
|
|
@@ -1377,17 +1649,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
1377
1649
|
unwrapContainers: true
|
|
1378
1650
|
});
|
|
1379
1651
|
}
|
|
1652
|
+
var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
|
|
1653
|
+
var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
|
|
1654
|
+
var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
|
|
1655
|
+
function detectStreamElement(method) {
|
|
1656
|
+
const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
|
|
1657
|
+
let node = method.getReturnTypeNode();
|
|
1658
|
+
node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
|
|
1659
|
+
const containerEl = streamContainerElement(node);
|
|
1660
|
+
if (containerEl) {
|
|
1661
|
+
return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
|
|
1662
|
+
}
|
|
1663
|
+
if (hasSse) return node ?? null;
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
function streamContainerElement(node) {
|
|
1667
|
+
if (!node || !import_ts_morph5.Node.isTypeReference(node)) return null;
|
|
1668
|
+
const typeName = node.getTypeName();
|
|
1669
|
+
const name = import_ts_morph5.Node.isIdentifier(typeName) ? typeName.getText() : "";
|
|
1670
|
+
if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
|
|
1671
|
+
return node.getTypeArguments()[0] ?? null;
|
|
1672
|
+
}
|
|
1673
|
+
return null;
|
|
1674
|
+
}
|
|
1675
|
+
function unwrapNamedContainer(node, names) {
|
|
1676
|
+
if (!node || !import_ts_morph5.Node.isTypeReference(node)) return node;
|
|
1677
|
+
const typeName = node.getTypeName();
|
|
1678
|
+
const name = import_ts_morph5.Node.isIdentifier(typeName) ? typeName.getText() : "";
|
|
1679
|
+
if (names.has(name)) {
|
|
1680
|
+
return node.getTypeArguments()[0] ?? node;
|
|
1681
|
+
}
|
|
1682
|
+
return node;
|
|
1683
|
+
}
|
|
1380
1684
|
function extractDtoContract(method, sourceFile, project) {
|
|
1381
1685
|
let body = extractBodyType(method, sourceFile, project);
|
|
1382
1686
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
1383
1687
|
const query = extractQueryType(method, sourceFile, project);
|
|
1688
|
+
const streamElement = detectStreamElement(method);
|
|
1689
|
+
const isStream = streamElement !== null;
|
|
1384
1690
|
if (filterInfo && filterInfo.source === "body") {
|
|
1385
1691
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
1386
1692
|
body = body ?? bodyType;
|
|
1387
1693
|
}
|
|
1388
1694
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
1389
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
1390
|
-
|
|
1695
|
+
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
1696
|
+
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
1697
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
1391
1698
|
return null;
|
|
1392
1699
|
}
|
|
1393
1700
|
let bodyRef = null;
|
|
@@ -1401,12 +1708,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
1401
1708
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
1402
1709
|
}
|
|
1403
1710
|
}
|
|
1404
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
1711
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
1405
1712
|
if (returnTypeNode) {
|
|
1406
1713
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
1407
1714
|
}
|
|
1408
|
-
if (!responseRef) {
|
|
1409
|
-
const apiResp = method.
|
|
1715
|
+
if (!responseRef && !isStream) {
|
|
1716
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
1410
1717
|
if (apiResp) {
|
|
1411
1718
|
const args = apiResp.getArguments();
|
|
1412
1719
|
const optsArg = args[0];
|
|
@@ -1448,16 +1755,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
1448
1755
|
query,
|
|
1449
1756
|
body,
|
|
1450
1757
|
response,
|
|
1758
|
+
error: errorInfo?.type ?? null,
|
|
1451
1759
|
params: paramsType,
|
|
1452
1760
|
queryRef,
|
|
1453
1761
|
bodyRef,
|
|
1454
1762
|
responseRef,
|
|
1763
|
+
errorRef: errorInfo?.ref ?? null,
|
|
1455
1764
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
1456
1765
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
1457
1766
|
filterSource: filterInfo?.source ?? null,
|
|
1458
1767
|
formWarnings,
|
|
1459
1768
|
bodySchema,
|
|
1460
|
-
querySchema
|
|
1769
|
+
querySchema,
|
|
1770
|
+
stream: isStream
|
|
1461
1771
|
};
|
|
1462
1772
|
}
|
|
1463
1773
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -1575,6 +1885,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
1575
1885
|
let query = null;
|
|
1576
1886
|
let body = null;
|
|
1577
1887
|
let response = "unknown";
|
|
1888
|
+
let error = null;
|
|
1578
1889
|
let bodyZodText = null;
|
|
1579
1890
|
let queryZodText = null;
|
|
1580
1891
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -1590,25 +1901,27 @@ function parseDefineContractCall(callExpr) {
|
|
|
1590
1901
|
bodyZodText = val.getText();
|
|
1591
1902
|
} else if (propName === "response") {
|
|
1592
1903
|
response = zodAstToTs(val);
|
|
1904
|
+
} else if (propName === "error") {
|
|
1905
|
+
error = zodAstToTs(val);
|
|
1593
1906
|
}
|
|
1594
1907
|
}
|
|
1595
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
1908
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
1596
1909
|
}
|
|
1597
1910
|
|
|
1598
1911
|
// src/discovery/contracts-fast.ts
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1912
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
1913
|
+
return tsconfig ? (0, import_node_path3.resolve)(tsconfig) : (0, import_node_path3.join)(cwd, "tsconfig.json");
|
|
1914
|
+
}
|
|
1915
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
1603
1916
|
try {
|
|
1604
|
-
|
|
1917
|
+
return new import_ts_morph7.Project({
|
|
1605
1918
|
tsConfigFilePath: tsconfigPath,
|
|
1606
1919
|
skipAddingFilesFromTsConfig: true,
|
|
1607
1920
|
skipLoadingLibFiles: true,
|
|
1608
1921
|
skipFileDependencyResolution: true
|
|
1609
1922
|
});
|
|
1610
1923
|
} catch {
|
|
1611
|
-
|
|
1924
|
+
return new import_ts_morph7.Project({
|
|
1612
1925
|
skipAddingFilesFromTsConfig: true,
|
|
1613
1926
|
skipLoadingLibFiles: true,
|
|
1614
1927
|
skipFileDependencyResolution: true,
|
|
@@ -1619,20 +1932,98 @@ async function discoverContractsFast(opts) {
|
|
|
1619
1932
|
}
|
|
1620
1933
|
});
|
|
1621
1934
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
project.addSourceFileAtPath(f);
|
|
1625
|
-
}
|
|
1626
|
-
const routes = [];
|
|
1935
|
+
}
|
|
1936
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
1627
1937
|
setDiscoveryContext(project, {
|
|
1628
1938
|
projectRoot: cwd,
|
|
1629
1939
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
1630
1940
|
});
|
|
1631
|
-
|
|
1632
|
-
|
|
1941
|
+
}
|
|
1942
|
+
function extractRoutesFrom(project, controllerPaths) {
|
|
1943
|
+
const routes = [];
|
|
1944
|
+
for (const path of controllerPaths) {
|
|
1945
|
+
const sourceFile = project.getSourceFile(path);
|
|
1946
|
+
if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
|
|
1633
1947
|
}
|
|
1634
1948
|
return routes;
|
|
1635
1949
|
}
|
|
1950
|
+
var PersistentDiscovery = class _PersistentDiscovery {
|
|
1951
|
+
project;
|
|
1952
|
+
cwd;
|
|
1953
|
+
glob;
|
|
1954
|
+
/** Absolute paths of the controllers currently loaded as extraction roots. */
|
|
1955
|
+
controllerPaths = /* @__PURE__ */ new Set();
|
|
1956
|
+
constructor(project, cwd, glob) {
|
|
1957
|
+
this.project = project;
|
|
1958
|
+
this.cwd = cwd;
|
|
1959
|
+
this.glob = glob;
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Build the initial persistent Project: create it, glob + add all controllers,
|
|
1963
|
+
* bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
|
|
1964
|
+
*/
|
|
1965
|
+
static async create(opts) {
|
|
1966
|
+
const { cwd, glob, tsconfig } = opts;
|
|
1967
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
1968
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
1969
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
1970
|
+
const instance = new _PersistentDiscovery(project, cwd, glob);
|
|
1971
|
+
const files = await (0, import_fast_glob.default)(glob, { cwd, absolute: true, onlyFiles: true });
|
|
1972
|
+
for (const f of files) {
|
|
1973
|
+
project.addSourceFileAtPath(f);
|
|
1974
|
+
instance.controllerPaths.add(f);
|
|
1975
|
+
}
|
|
1976
|
+
return instance;
|
|
1977
|
+
}
|
|
1978
|
+
/** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
|
|
1979
|
+
discover() {
|
|
1980
|
+
return this.runExtraction();
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Re-discover after one or more files changed. Refreshes the changed file(s)
|
|
1984
|
+
* from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
|
|
1985
|
+
* to pick up added/removed controllers, clears the per-Project caches, then
|
|
1986
|
+
* re-extracts. `changedPaths` is a hint; correctness does not depend on it
|
|
1987
|
+
* being exhaustive because re-globbing + refresh-on-presence covers the set.
|
|
1988
|
+
*/
|
|
1989
|
+
async rediscover(changedPaths) {
|
|
1990
|
+
if (changedPaths) {
|
|
1991
|
+
for (const p of changedPaths) {
|
|
1992
|
+
const abs = (0, import_node_path3.resolve)(p);
|
|
1993
|
+
const sf = this.project.getSourceFile(abs);
|
|
1994
|
+
if (sf) {
|
|
1995
|
+
await sf.refreshFromFileSystem();
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
const globbed = new Set(
|
|
2000
|
+
await (0, import_fast_glob.default)(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
|
|
2001
|
+
);
|
|
2002
|
+
for (const f of globbed) {
|
|
2003
|
+
if (!this.controllerPaths.has(f)) {
|
|
2004
|
+
try {
|
|
2005
|
+
this.project.addSourceFileAtPath(f);
|
|
2006
|
+
this.controllerPaths.add(f);
|
|
2007
|
+
} catch {
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
for (const f of this.controllerPaths) {
|
|
2012
|
+
if (!globbed.has(f)) {
|
|
2013
|
+
const sf = this.project.getSourceFile(f);
|
|
2014
|
+
if (sf) this.project.removeSourceFile(sf);
|
|
2015
|
+
this.controllerPaths.delete(f);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return this.runExtraction();
|
|
2019
|
+
}
|
|
2020
|
+
/** Clear stale per-Project caches, then extract over the controller set. */
|
|
2021
|
+
runExtraction() {
|
|
2022
|
+
clearTypeResolutionCaches(this.project);
|
|
2023
|
+
clearEnumCache(this.project);
|
|
2024
|
+
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
1636
2027
|
function decoratorStringArg(decoratorExpr) {
|
|
1637
2028
|
if (!decoratorExpr) return void 0;
|
|
1638
2029
|
if (import_ts_morph7.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -1688,6 +2079,11 @@ function resolveVerb(method) {
|
|
|
1688
2079
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
1689
2080
|
}
|
|
1690
2081
|
}
|
|
2082
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
2083
|
+
if (sseDecorator) {
|
|
2084
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
2085
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
2086
|
+
}
|
|
1691
2087
|
return null;
|
|
1692
2088
|
}
|
|
1693
2089
|
function readAsDecorator(node, label) {
|
|
@@ -1730,7 +2126,17 @@ function buildRoute(args) {
|
|
|
1730
2126
|
};
|
|
1731
2127
|
}
|
|
1732
2128
|
function extractContractRoute(args) {
|
|
1733
|
-
const {
|
|
2129
|
+
const {
|
|
2130
|
+
cls,
|
|
2131
|
+
method,
|
|
2132
|
+
applyContractDecorator,
|
|
2133
|
+
verb,
|
|
2134
|
+
prefix,
|
|
2135
|
+
className,
|
|
2136
|
+
sourceFile,
|
|
2137
|
+
project,
|
|
2138
|
+
seenNames
|
|
2139
|
+
} = args;
|
|
1734
2140
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
1735
2141
|
if (!firstDecoratorArg) return null;
|
|
1736
2142
|
let contractDef = null;
|
|
@@ -1740,18 +2146,19 @@ function extractContractRoute(args) {
|
|
|
1740
2146
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
1741
2147
|
} else if (import_ts_morph7.Node.isIdentifier(firstDecoratorArg)) {
|
|
1742
2148
|
const identName = firstDecoratorArg.getText();
|
|
1743
|
-
const
|
|
1744
|
-
if (!
|
|
2149
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
2150
|
+
if (!resolvedVar) {
|
|
1745
2151
|
console.warn(
|
|
1746
|
-
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()}
|
|
2152
|
+
`[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
|
|
1747
2153
|
);
|
|
1748
2154
|
return null;
|
|
1749
2155
|
}
|
|
2156
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
1750
2157
|
const initializer = varDecl.getInitializer();
|
|
1751
2158
|
if (!initializer) return null;
|
|
1752
2159
|
contractDef = parseDefineContractCall(initializer);
|
|
1753
2160
|
if (contractDef && varDecl.isExported()) {
|
|
1754
|
-
const filePath =
|
|
2161
|
+
const filePath = declFile.getFilePath();
|
|
1755
2162
|
if (contractDef.body !== null) {
|
|
1756
2163
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
1757
2164
|
}
|
|
@@ -1784,6 +2191,7 @@ function extractContractRoute(args) {
|
|
|
1784
2191
|
query: contractDef.query,
|
|
1785
2192
|
body: contractDef.body,
|
|
1786
2193
|
response: contractDef.response,
|
|
2194
|
+
error: contractDef.error,
|
|
1787
2195
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
1788
2196
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
1789
2197
|
// would drag server-only deps into the client bundle).
|
|
@@ -1815,15 +2223,18 @@ function extractDtoRoute(args) {
|
|
|
1815
2223
|
query: dtoContract?.query ?? null,
|
|
1816
2224
|
body: dtoContract?.body ?? null,
|
|
1817
2225
|
response: dtoContract?.response ?? "unknown",
|
|
2226
|
+
error: dtoContract?.error ?? null,
|
|
1818
2227
|
queryRef: dtoContract?.queryRef ?? null,
|
|
1819
2228
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
1820
2229
|
responseRef: dtoContract?.responseRef ?? null,
|
|
2230
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
1821
2231
|
filterFields: dtoContract?.filterFields ?? null,
|
|
1822
2232
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
1823
2233
|
filterSource: dtoContract?.filterSource ?? null,
|
|
1824
2234
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
1825
2235
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
1826
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
2236
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
2237
|
+
stream: dtoContract?.stream ?? false
|
|
1827
2238
|
}
|
|
1828
2239
|
});
|
|
1829
2240
|
}
|
|
@@ -1847,6 +2258,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
1847
2258
|
prefix,
|
|
1848
2259
|
className,
|
|
1849
2260
|
sourceFile,
|
|
2261
|
+
project,
|
|
1850
2262
|
seenNames
|
|
1851
2263
|
}) : extractDtoRoute({
|
|
1852
2264
|
cls,
|
|
@@ -1865,8 +2277,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
1865
2277
|
}
|
|
1866
2278
|
|
|
1867
2279
|
// src/generate.ts
|
|
1868
|
-
var
|
|
1869
|
-
var
|
|
2280
|
+
var import_promises11 = require("fs/promises");
|
|
2281
|
+
var import_node_path14 = require("path");
|
|
1870
2282
|
|
|
1871
2283
|
// src/discovery/pages.ts
|
|
1872
2284
|
var import_promises2 = require("fs/promises");
|
|
@@ -2395,17 +2807,28 @@ function emitFilterQueryType(c) {
|
|
|
2395
2807
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
2396
2808
|
}
|
|
2397
2809
|
function buildResponseType(c, outDir) {
|
|
2810
|
+
const respRef = c.contractSource.responseRef;
|
|
2811
|
+
if (c.contractSource.stream) {
|
|
2812
|
+
if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
2813
|
+
return c.contractSource.response;
|
|
2814
|
+
}
|
|
2398
2815
|
if (c.controllerRef) {
|
|
2399
2816
|
let relPath = (0, import_node_path6.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
2400
2817
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
2401
2818
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
2402
2819
|
}
|
|
2403
|
-
const respRef = c.contractSource.responseRef;
|
|
2404
2820
|
if (respRef) {
|
|
2405
2821
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
2406
2822
|
}
|
|
2407
2823
|
return c.contractSource.response;
|
|
2408
2824
|
}
|
|
2825
|
+
function buildErrorType(c) {
|
|
2826
|
+
const errRef = c.contractSource.errorRef;
|
|
2827
|
+
if (errRef) {
|
|
2828
|
+
return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
|
|
2829
|
+
}
|
|
2830
|
+
return c.contractSource.error ?? "unknown";
|
|
2831
|
+
}
|
|
2409
2832
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
2410
2833
|
const pad = " ".repeat(indent);
|
|
2411
2834
|
const lines = [];
|
|
@@ -2420,12 +2843,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2420
2843
|
const bodyRef = c.contractSource.bodyRef;
|
|
2421
2844
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
2422
2845
|
const response = buildResponseType(c, outDir);
|
|
2846
|
+
const error = buildErrorType(c);
|
|
2423
2847
|
const params = buildParamsType(c.params);
|
|
2424
2848
|
const safeMethod = JSON.stringify(method);
|
|
2425
2849
|
const safeUrl = JSON.stringify(c.path);
|
|
2426
2850
|
const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
|
|
2851
|
+
const stream = c.contractSource.stream ? "true" : "false";
|
|
2427
2852
|
lines.push(
|
|
2428
|
-
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
|
|
2853
|
+
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
|
|
2429
2854
|
);
|
|
2430
2855
|
} else {
|
|
2431
2856
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -2497,15 +2922,21 @@ function emitReqHelper() {
|
|
|
2497
2922
|
""
|
|
2498
2923
|
];
|
|
2499
2924
|
}
|
|
2500
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
2925
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
2501
2926
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
2502
2927
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
2928
|
+
if (streamExpr) {
|
|
2929
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
2930
|
+
}
|
|
2503
2931
|
for (const [name, value] of Object.entries(members)) {
|
|
2504
2932
|
lines.push(`${pad} ${name}: ${value},`);
|
|
2505
2933
|
}
|
|
2506
2934
|
lines.push(`${pad}}),`);
|
|
2507
2935
|
return lines;
|
|
2508
2936
|
}
|
|
2937
|
+
function renderStreamExpr(req) {
|
|
2938
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
2939
|
+
}
|
|
2509
2940
|
function emitApiObjectBlock(tree, indent, p) {
|
|
2510
2941
|
const pad = " ".repeat(indent);
|
|
2511
2942
|
const lines = [];
|
|
@@ -2540,7 +2971,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2540
2971
|
}
|
|
2541
2972
|
const members = {};
|
|
2542
2973
|
for (const [name, { value }] of owned) members[name] = value;
|
|
2543
|
-
|
|
2974
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
2975
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
2544
2976
|
}
|
|
2545
2977
|
return lines;
|
|
2546
2978
|
}
|
|
@@ -2578,6 +3010,8 @@ var ROUTE_NAMESPACE = [
|
|
|
2578
3010
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
2579
3011
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
2580
3012
|
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
3013
|
+
" /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
|
|
3014
|
+
' export type Stream<K extends string> = ResolveByName<K, "response">;',
|
|
2581
3015
|
" export type Request<K extends string> = {",
|
|
2582
3016
|
" body: Body<K>;",
|
|
2583
3017
|
" query: Query<K>;",
|
|
@@ -2594,6 +3028,7 @@ var PATH_NAMESPACE = [
|
|
|
2594
3028
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
2595
3029
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
2596
3030
|
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
3031
|
+
' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
2597
3032
|
"}",
|
|
2598
3033
|
""
|
|
2599
3034
|
];
|
|
@@ -2605,6 +3040,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
2605
3040
|
" export type Params<K extends string> = never;",
|
|
2606
3041
|
" export type Error<K extends string> = never;",
|
|
2607
3042
|
" export type FilterFields<K extends string> = never;",
|
|
3043
|
+
" export type Stream<K extends string> = never;",
|
|
2608
3044
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
2609
3045
|
"}",
|
|
2610
3046
|
""
|
|
@@ -2617,6 +3053,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
2617
3053
|
" export type Params<M extends string, U extends string> = never;",
|
|
2618
3054
|
" export type Error<M extends string, U extends string> = never;",
|
|
2619
3055
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
3056
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
2620
3057
|
"}",
|
|
2621
3058
|
""
|
|
2622
3059
|
];
|
|
@@ -2640,7 +3077,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
2640
3077
|
for (const r of contracted) {
|
|
2641
3078
|
const cs = r.contract?.contractSource;
|
|
2642
3079
|
if (!cs) continue;
|
|
2643
|
-
const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
|
|
3080
|
+
const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
|
|
2644
3081
|
for (const ref of refs) {
|
|
2645
3082
|
if (!ref) continue;
|
|
2646
3083
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -3073,11 +3510,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
3073
3510
|
await (0, import_promises6.writeFile)((0, import_node_path9.join)(outDir, "index.d.ts"), content, "utf8");
|
|
3074
3511
|
}
|
|
3075
3512
|
|
|
3076
|
-
// src/emit/emit-
|
|
3513
|
+
// src/emit/emit-mocks.ts
|
|
3077
3514
|
var import_promises7 = require("fs/promises");
|
|
3078
3515
|
var import_node_path10 = require("path");
|
|
3079
|
-
|
|
3516
|
+
|
|
3517
|
+
// src/ir/schema-node-to-json-schema.ts
|
|
3518
|
+
var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
|
|
3519
|
+
function parseLiteral(raw) {
|
|
3520
|
+
const t = raw.trim();
|
|
3521
|
+
if (t === "true") return true;
|
|
3522
|
+
if (t === "false") return false;
|
|
3523
|
+
if (t === "null") return null;
|
|
3524
|
+
const q = t[0];
|
|
3525
|
+
if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
|
|
3526
|
+
return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
|
|
3527
|
+
}
|
|
3528
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
|
|
3529
|
+
return Number(t);
|
|
3530
|
+
}
|
|
3531
|
+
return t;
|
|
3532
|
+
}
|
|
3533
|
+
function literalsType(values) {
|
|
3534
|
+
const types = new Set(values.map((v) => v === null ? "null" : typeof v));
|
|
3535
|
+
if (types.size === 1) {
|
|
3536
|
+
const only = [...types][0];
|
|
3537
|
+
if (only === "string") return "string";
|
|
3538
|
+
if (only === "number") return "number";
|
|
3539
|
+
if (only === "boolean") return "boolean";
|
|
3540
|
+
}
|
|
3541
|
+
return void 0;
|
|
3542
|
+
}
|
|
3543
|
+
function convert(node, ctx) {
|
|
3544
|
+
switch (node.kind) {
|
|
3545
|
+
case "string": {
|
|
3546
|
+
const out = { type: "string" };
|
|
3547
|
+
for (const c of node.checks) {
|
|
3548
|
+
if (c.check === "email") out.format = "email";
|
|
3549
|
+
else if (c.check === "url") out.format = "uri";
|
|
3550
|
+
else if (c.check === "uuid") out.format = "uuid";
|
|
3551
|
+
else if (c.check === "min") out.minLength = Number(c.value);
|
|
3552
|
+
else if (c.check === "max") out.maxLength = Number(c.value);
|
|
3553
|
+
else if (c.check === "regex") {
|
|
3554
|
+
const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
|
|
3555
|
+
out.pattern = m ? m[1] : c.pattern;
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
return out;
|
|
3559
|
+
}
|
|
3560
|
+
case "number": {
|
|
3561
|
+
const out = { type: "number" };
|
|
3562
|
+
for (const c of node.checks) {
|
|
3563
|
+
if (c.check === "int") out.type = "integer";
|
|
3564
|
+
else if (c.check === "min") out.minimum = Number(c.value);
|
|
3565
|
+
else if (c.check === "max") out.maximum = Number(c.value);
|
|
3566
|
+
else if (c.check === "positive") out.exclusiveMinimum = 0;
|
|
3567
|
+
else if (c.check === "negative") out.exclusiveMaximum = 0;
|
|
3568
|
+
}
|
|
3569
|
+
return out;
|
|
3570
|
+
}
|
|
3571
|
+
case "boolean":
|
|
3572
|
+
return { type: "boolean" };
|
|
3573
|
+
case "date":
|
|
3574
|
+
return { type: "string", format: "date-time" };
|
|
3575
|
+
case "unknown":
|
|
3576
|
+
return node.note ? { description: node.note } : {};
|
|
3577
|
+
case "instanceof":
|
|
3578
|
+
return { type: "object", description: `instanceof ${node.ctor}` };
|
|
3579
|
+
case "enum": {
|
|
3580
|
+
const values = node.literals.map(parseLiteral);
|
|
3581
|
+
const t = literalsType(values);
|
|
3582
|
+
const out = { enum: values };
|
|
3583
|
+
if (t) out.type = t;
|
|
3584
|
+
return out;
|
|
3585
|
+
}
|
|
3586
|
+
case "literal": {
|
|
3587
|
+
const value = parseLiteral(node.raw);
|
|
3588
|
+
const out = { const: value };
|
|
3589
|
+
const t = literalsType([value]);
|
|
3590
|
+
if (t) out.type = t;
|
|
3591
|
+
return out;
|
|
3592
|
+
}
|
|
3593
|
+
case "union": {
|
|
3594
|
+
const options = node.options.map((o) => convert(o, ctx));
|
|
3595
|
+
const out = { oneOf: options };
|
|
3596
|
+
if (node.discriminator) {
|
|
3597
|
+
out.discriminator = { propertyName: node.discriminator };
|
|
3598
|
+
}
|
|
3599
|
+
return out;
|
|
3600
|
+
}
|
|
3601
|
+
case "object": {
|
|
3602
|
+
const properties = {};
|
|
3603
|
+
const required = [];
|
|
3604
|
+
for (const f of node.fields) {
|
|
3605
|
+
if (f.value.kind === "optional") {
|
|
3606
|
+
properties[f.key] = convert(f.value.inner, ctx);
|
|
3607
|
+
} else {
|
|
3608
|
+
properties[f.key] = convert(f.value, ctx);
|
|
3609
|
+
required.push(f.key);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
const out = { type: "object", properties };
|
|
3613
|
+
if (required.length > 0) out.required = required;
|
|
3614
|
+
out.additionalProperties = node.passthrough;
|
|
3615
|
+
return out;
|
|
3616
|
+
}
|
|
3617
|
+
case "array":
|
|
3618
|
+
return { type: "array", items: convert(node.element, ctx) };
|
|
3619
|
+
case "optional":
|
|
3620
|
+
return widenNullable(convert(node.inner, ctx));
|
|
3621
|
+
case "ref":
|
|
3622
|
+
case "lazyRef":
|
|
3623
|
+
return { $ref: `${ctx.refPrefix}${node.name}` };
|
|
3624
|
+
case "annotated":
|
|
3625
|
+
return convert(node.inner, ctx);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
function widenNullable(schema) {
|
|
3629
|
+
if (schema.$ref) {
|
|
3630
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
3631
|
+
}
|
|
3632
|
+
if (typeof schema.type === "string") {
|
|
3633
|
+
return { ...schema, type: [schema.type, "null"] };
|
|
3634
|
+
}
|
|
3635
|
+
if (Array.isArray(schema.type)) {
|
|
3636
|
+
return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
|
|
3637
|
+
}
|
|
3638
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
3639
|
+
}
|
|
3640
|
+
function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
|
|
3641
|
+
const named = {};
|
|
3642
|
+
for (const [name, node] of mod.named) {
|
|
3643
|
+
named[name] = convert(node, ctx);
|
|
3644
|
+
}
|
|
3645
|
+
return { root: convert(mod.root, ctx), named };
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
// src/emit/mock-gen-runtime.ts
|
|
3649
|
+
var MOCK_GEN_RUNTIME = `
|
|
3650
|
+
/** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
|
|
3651
|
+
function makeRng(seed) {
|
|
3652
|
+
let a = seed >>> 0;
|
|
3653
|
+
return {
|
|
3654
|
+
next() {
|
|
3655
|
+
a |= 0;
|
|
3656
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
3657
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
3658
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
3659
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
3660
|
+
},
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
function __pick(rng, items) {
|
|
3665
|
+
return items[Math.floor(rng.next() * items.length)];
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
function __intBetween(rng, min, max) {
|
|
3669
|
+
return Math.floor(rng.next() * (max - min + 1)) + min;
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
|
|
3673
|
+
const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
|
|
3674
|
+
const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
|
|
3675
|
+
|
|
3676
|
+
function __fakeWords(rng, count) {
|
|
3677
|
+
let out = [];
|
|
3678
|
+
for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
|
|
3679
|
+
return out.join(' ');
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
function __hex(rng, len) {
|
|
3683
|
+
let s = '';
|
|
3684
|
+
for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
|
|
3685
|
+
return s;
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
function __fakeUuid(rng) {
|
|
3689
|
+
return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
function __fakeString(rng, schema) {
|
|
3693
|
+
switch (schema.format) {
|
|
3694
|
+
case 'email':
|
|
3695
|
+
return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
|
|
3696
|
+
case 'uri':
|
|
3697
|
+
case 'url':
|
|
3698
|
+
return 'https://example.com/' + __pick(rng, __WORDS);
|
|
3699
|
+
case 'uuid':
|
|
3700
|
+
return __fakeUuid(rng);
|
|
3701
|
+
case 'date-time':
|
|
3702
|
+
return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
|
|
3703
|
+
default:
|
|
3704
|
+
return __fakeWords(rng, __intBetween(rng, 1, 3));
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
/** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
|
|
3709
|
+
function generateMock(schema, rng, defs, depth) {
|
|
3710
|
+
defs = defs || {};
|
|
3711
|
+
depth = depth || 0;
|
|
3712
|
+
if (schema.$ref) {
|
|
3713
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
3714
|
+
const target = defs[name];
|
|
3715
|
+
if (!target || depth > 4) return null;
|
|
3716
|
+
return generateMock(target, rng, defs, depth + 1);
|
|
3717
|
+
}
|
|
3718
|
+
if ('const' in schema) return schema.const;
|
|
3719
|
+
if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
|
|
3720
|
+
if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
|
|
3721
|
+
if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
|
|
3722
|
+
let type = Array.isArray(schema.type)
|
|
3723
|
+
? (schema.type.filter((t) => t !== 'null')[0] || 'null')
|
|
3724
|
+
: schema.type;
|
|
3725
|
+
switch (type) {
|
|
3726
|
+
case 'string':
|
|
3727
|
+
return __fakeString(rng, schema);
|
|
3728
|
+
case 'integer':
|
|
3729
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
|
|
3730
|
+
case 'number':
|
|
3731
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
|
|
3732
|
+
case 'boolean':
|
|
3733
|
+
return rng.next() < 0.5;
|
|
3734
|
+
case 'null':
|
|
3735
|
+
return null;
|
|
3736
|
+
case 'array': {
|
|
3737
|
+
const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
|
|
3738
|
+
const items = schema.items || {};
|
|
3739
|
+
let arr = [];
|
|
3740
|
+
for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
|
|
3741
|
+
return arr;
|
|
3742
|
+
}
|
|
3743
|
+
case 'object': {
|
|
3744
|
+
const out = {};
|
|
3745
|
+
const props = schema.properties || {};
|
|
3746
|
+
for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
|
|
3747
|
+
return out;
|
|
3748
|
+
}
|
|
3749
|
+
default:
|
|
3750
|
+
return {};
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
`.trim();
|
|
3754
|
+
|
|
3755
|
+
// src/emit/emit-mocks.ts
|
|
3756
|
+
var REF_PREFIX = "#/components/schemas/";
|
|
3757
|
+
function toMswPath(path, baseUrl) {
|
|
3758
|
+
return `${baseUrl}${path}`;
|
|
3759
|
+
}
|
|
3760
|
+
function responseSchemaFor(route, defs) {
|
|
3761
|
+
const cs = route.contract.contractSource;
|
|
3762
|
+
if (cs.responseSchema) {
|
|
3763
|
+
const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
|
|
3764
|
+
for (const [name, node] of Object.entries(named)) {
|
|
3765
|
+
if (!(name in defs)) defs[name] = node;
|
|
3766
|
+
}
|
|
3767
|
+
return root;
|
|
3768
|
+
}
|
|
3769
|
+
return {};
|
|
3770
|
+
}
|
|
3771
|
+
function buildMocksFile(routes, opts = {}) {
|
|
3772
|
+
const seed = opts.seed ?? 1;
|
|
3773
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
3774
|
+
const contracted = routes.filter((r) => r.contract);
|
|
3775
|
+
const defs = {};
|
|
3776
|
+
const handlers = [];
|
|
3777
|
+
for (const r of contracted) {
|
|
3778
|
+
const schema = responseSchemaFor(r, defs);
|
|
3779
|
+
const method = r.method.toLowerCase();
|
|
3780
|
+
const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
|
|
3781
|
+
const path = toMswPath(r.path, baseUrl);
|
|
3782
|
+
const cs = r.contract.contractSource;
|
|
3783
|
+
const schemaLiteral = JSON.stringify(schema);
|
|
3784
|
+
const pathLit = JSON.stringify(path);
|
|
3785
|
+
if (cs.stream) {
|
|
3786
|
+
handlers.push(
|
|
3787
|
+
[
|
|
3788
|
+
` // ${r.name} (stream)`,
|
|
3789
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
3790
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
3791
|
+
" const body = `data: ${JSON.stringify(value)}\\n\\n`;",
|
|
3792
|
+
" return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
|
|
3793
|
+
" }),"
|
|
3794
|
+
].join("\n")
|
|
3795
|
+
);
|
|
3796
|
+
} else {
|
|
3797
|
+
handlers.push(
|
|
3798
|
+
[
|
|
3799
|
+
` // ${r.name}`,
|
|
3800
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
3801
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
3802
|
+
" return HttpResponse.json(value);",
|
|
3803
|
+
" }),"
|
|
3804
|
+
].join("\n")
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
const lines = [
|
|
3809
|
+
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
3810
|
+
"// MSW handlers returning deterministic, schema-shaped mock data.",
|
|
3811
|
+
"/* eslint-disable */",
|
|
3812
|
+
"// @ts-nocheck",
|
|
3813
|
+
"",
|
|
3814
|
+
"import { http, HttpResponse } from 'msw';",
|
|
3815
|
+
"",
|
|
3816
|
+
`const SEED = ${seed};`,
|
|
3817
|
+
"",
|
|
3818
|
+
"// ---------------------------------------------------------------------------",
|
|
3819
|
+
"// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
|
|
3820
|
+
"// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
|
|
3821
|
+
"// ---------------------------------------------------------------------------",
|
|
3822
|
+
MOCK_GEN_RUNTIME,
|
|
3823
|
+
"",
|
|
3824
|
+
"// Shared component schemas referenced by $ref.",
|
|
3825
|
+
`const DEFS = ${JSON.stringify(defs, null, 2)};`,
|
|
3826
|
+
"",
|
|
3827
|
+
"/** MSW request handlers, one per contracted route. */",
|
|
3828
|
+
"export const handlers = [",
|
|
3829
|
+
...handlers,
|
|
3830
|
+
"];",
|
|
3831
|
+
""
|
|
3832
|
+
];
|
|
3833
|
+
return lines.join("\n");
|
|
3834
|
+
}
|
|
3835
|
+
async function emitMocks(routes, outDir, opts = {}) {
|
|
3080
3836
|
await (0, import_promises7.mkdir)(outDir, { recursive: true });
|
|
3837
|
+
const content = buildMocksFile(routes, opts);
|
|
3838
|
+
const fileName = opts.fileName ?? "mocks.ts";
|
|
3839
|
+
await (0, import_promises7.writeFile)((0, import_node_path10.join)(outDir, fileName), content, "utf8");
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
// src/emit/emit-openapi.ts
|
|
3843
|
+
var import_promises8 = require("fs/promises");
|
|
3844
|
+
var import_node_path11 = require("path");
|
|
3845
|
+
var REF_PREFIX2 = "#/components/schemas/";
|
|
3846
|
+
function toOpenApiPath(path) {
|
|
3847
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
3848
|
+
}
|
|
3849
|
+
function positionSchema(schema, tsType, components) {
|
|
3850
|
+
if (schema) {
|
|
3851
|
+
const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
|
|
3852
|
+
for (const [name, node] of Object.entries(named)) {
|
|
3853
|
+
if (!(name in components)) components[name] = node;
|
|
3854
|
+
}
|
|
3855
|
+
return root;
|
|
3856
|
+
}
|
|
3857
|
+
return tsType ? { description: tsType } : {};
|
|
3858
|
+
}
|
|
3859
|
+
function buildParameters(route) {
|
|
3860
|
+
const params = [];
|
|
3861
|
+
for (const p of route.params) {
|
|
3862
|
+
if (p.source === "path") {
|
|
3863
|
+
params.push({
|
|
3864
|
+
name: p.name,
|
|
3865
|
+
in: "path",
|
|
3866
|
+
required: true,
|
|
3867
|
+
schema: { type: "string" }
|
|
3868
|
+
});
|
|
3869
|
+
} else if (p.source === "query") {
|
|
3870
|
+
params.push({
|
|
3871
|
+
name: p.name,
|
|
3872
|
+
in: "query",
|
|
3873
|
+
required: false,
|
|
3874
|
+
schema: { type: "string" }
|
|
3875
|
+
});
|
|
3876
|
+
} else if (p.source === "header") {
|
|
3877
|
+
params.push({
|
|
3878
|
+
name: p.name,
|
|
3879
|
+
in: "header",
|
|
3880
|
+
required: false,
|
|
3881
|
+
schema: { type: "string" }
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
return params;
|
|
3886
|
+
}
|
|
3887
|
+
function buildResponses(cs, components) {
|
|
3888
|
+
const responses = {};
|
|
3889
|
+
const successSchema = positionSchema(
|
|
3890
|
+
// Prefer rich response IR when present; otherwise fall back to the TS type.
|
|
3891
|
+
cs.responseSchema ?? null,
|
|
3892
|
+
cs.response,
|
|
3893
|
+
components
|
|
3894
|
+
);
|
|
3895
|
+
const successContentType = cs.stream ? "text/event-stream" : "application/json";
|
|
3896
|
+
responses["200"] = {
|
|
3897
|
+
description: cs.stream ? "Server-sent event stream" : "Successful response",
|
|
3898
|
+
content: { [successContentType]: { schema: successSchema } }
|
|
3899
|
+
};
|
|
3900
|
+
const errorSchema = positionSchema(null, cs.error ?? null, components);
|
|
3901
|
+
const errorBody = {
|
|
3902
|
+
description: "Error response",
|
|
3903
|
+
content: { "application/json": { schema: errorSchema } }
|
|
3904
|
+
};
|
|
3905
|
+
if (cs.error || cs.errorRef) {
|
|
3906
|
+
responses["400"] = errorBody;
|
|
3907
|
+
responses.default = errorBody;
|
|
3908
|
+
} else {
|
|
3909
|
+
responses.default = {
|
|
3910
|
+
description: "Error response",
|
|
3911
|
+
content: { "application/json": { schema: {} } }
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
return responses;
|
|
3915
|
+
}
|
|
3916
|
+
function buildOperation(route, components) {
|
|
3917
|
+
const cs = route.contract.contractSource;
|
|
3918
|
+
const op = {
|
|
3919
|
+
operationId: route.name,
|
|
3920
|
+
parameters: buildParameters(route),
|
|
3921
|
+
responses: buildResponses(cs, components)
|
|
3922
|
+
};
|
|
3923
|
+
const method = route.method.toUpperCase();
|
|
3924
|
+
const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
|
|
3925
|
+
if (hasBody && (cs.bodySchema || cs.body)) {
|
|
3926
|
+
const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
|
|
3927
|
+
op.requestBody = {
|
|
3928
|
+
required: true,
|
|
3929
|
+
content: { "application/json": { schema: bodySchema } }
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3932
|
+
return op;
|
|
3933
|
+
}
|
|
3934
|
+
function buildOpenApiSpec(routes, opts = {}) {
|
|
3935
|
+
const components = {};
|
|
3936
|
+
const paths = {};
|
|
3937
|
+
for (const route of routes) {
|
|
3938
|
+
if (!route.contract) continue;
|
|
3939
|
+
const oaPath = toOpenApiPath(route.path);
|
|
3940
|
+
const method = route.method.toLowerCase();
|
|
3941
|
+
let pathItem = paths[oaPath];
|
|
3942
|
+
if (!pathItem) {
|
|
3943
|
+
pathItem = {};
|
|
3944
|
+
paths[oaPath] = pathItem;
|
|
3945
|
+
}
|
|
3946
|
+
pathItem[method] = buildOperation(route, components);
|
|
3947
|
+
}
|
|
3948
|
+
const info = opts.info ?? {};
|
|
3949
|
+
const doc = {
|
|
3950
|
+
openapi: "3.1.0",
|
|
3951
|
+
info: {
|
|
3952
|
+
title: info.title ?? "NestJS API",
|
|
3953
|
+
version: info.version ?? "1.0.0",
|
|
3954
|
+
...info.description ? { description: info.description } : {}
|
|
3955
|
+
},
|
|
3956
|
+
paths,
|
|
3957
|
+
components: { schemas: components }
|
|
3958
|
+
};
|
|
3959
|
+
return doc;
|
|
3960
|
+
}
|
|
3961
|
+
async function emitOpenApi(routes, outDir, opts = {}) {
|
|
3962
|
+
await (0, import_promises8.mkdir)(outDir, { recursive: true });
|
|
3963
|
+
const doc = buildOpenApiSpec(routes, opts);
|
|
3964
|
+
const fileName = opts.fileName ?? "openapi.json";
|
|
3965
|
+
await (0, import_promises8.writeFile)((0, import_node_path11.join)(outDir, fileName), `${JSON.stringify(doc, null, 2)}
|
|
3966
|
+
`, "utf8");
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
// src/emit/emit-pages.ts
|
|
3970
|
+
var import_promises9 = require("fs/promises");
|
|
3971
|
+
var import_node_path12 = require("path");
|
|
3972
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
3973
|
+
await (0, import_promises9.mkdir)(outDir, { recursive: true });
|
|
3081
3974
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
3082
3975
|
const augBody = pages.map((p) => {
|
|
3083
3976
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -3096,7 +3989,7 @@ ${augBody}
|
|
|
3096
3989
|
}
|
|
3097
3990
|
${sharedPropsBlock}}
|
|
3098
3991
|
`;
|
|
3099
|
-
await (0,
|
|
3992
|
+
await (0, import_promises9.writeFile)((0, import_node_path12.join)(outDir, "pages.d.ts"), content, "utf8");
|
|
3100
3993
|
}
|
|
3101
3994
|
function buildSharedPropsBlock(sharedProps) {
|
|
3102
3995
|
if (!sharedProps) return "";
|
|
@@ -3115,7 +4008,7 @@ ${propsBody}
|
|
|
3115
4008
|
`;
|
|
3116
4009
|
}
|
|
3117
4010
|
function buildAugmentationType(page, outDir) {
|
|
3118
|
-
let importPath = (0,
|
|
4011
|
+
let importPath = (0, import_node_path12.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
|
|
3119
4012
|
if (!importPath.startsWith(".")) {
|
|
3120
4013
|
importPath = `./${importPath}`;
|
|
3121
4014
|
}
|
|
@@ -3126,12 +4019,12 @@ function needsQuotes(name) {
|
|
|
3126
4019
|
}
|
|
3127
4020
|
|
|
3128
4021
|
// src/emit/emit-routes.ts
|
|
3129
|
-
var
|
|
3130
|
-
var
|
|
4022
|
+
var import_promises10 = require("fs/promises");
|
|
4023
|
+
var import_node_path13 = require("path");
|
|
3131
4024
|
async function emitRoutes(routes, outDir) {
|
|
3132
|
-
await (0,
|
|
4025
|
+
await (0, import_promises10.mkdir)(outDir, { recursive: true });
|
|
3133
4026
|
const content = buildRoutesFile(routes);
|
|
3134
|
-
await (0,
|
|
4027
|
+
await (0, import_promises10.writeFile)((0, import_node_path13.join)(outDir, "routes.ts"), content, "utf8");
|
|
3135
4028
|
}
|
|
3136
4029
|
function buildRoutesFile(routes) {
|
|
3137
4030
|
if (routes.length === 0) {
|
|
@@ -3279,21 +4172,38 @@ async function generate(config, inputRoutes = []) {
|
|
|
3279
4172
|
});
|
|
3280
4173
|
}
|
|
3281
4174
|
const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
|
|
4175
|
+
if (hasContracts && config.openapi.enabled) {
|
|
4176
|
+
await emitOpenApi(routes, config.codegen.outDir, {
|
|
4177
|
+
fileName: config.openapi.fileName,
|
|
4178
|
+
info: {
|
|
4179
|
+
title: config.openapi.title,
|
|
4180
|
+
version: config.openapi.version,
|
|
4181
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
4182
|
+
}
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
if (hasContracts && config.mocks.enabled) {
|
|
4186
|
+
await emitMocks(routes, config.codegen.outDir, {
|
|
4187
|
+
fileName: config.mocks.fileName,
|
|
4188
|
+
seed: config.mocks.seed,
|
|
4189
|
+
baseUrl: config.mocks.baseUrl
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
3282
4192
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
3283
4193
|
if (extensions.length > 0) {
|
|
3284
4194
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
3285
4195
|
for (const file of extraFiles) {
|
|
3286
|
-
const dest = (0,
|
|
3287
|
-
await (0,
|
|
3288
|
-
await (0,
|
|
4196
|
+
const dest = (0, import_node_path14.join)(config.codegen.outDir, file.path);
|
|
4197
|
+
await (0, import_promises11.mkdir)((0, import_node_path14.dirname)(dest), { recursive: true });
|
|
4198
|
+
await (0, import_promises11.writeFile)(dest, file.contents, "utf8");
|
|
3289
4199
|
}
|
|
3290
4200
|
}
|
|
3291
4201
|
}
|
|
3292
4202
|
|
|
3293
4203
|
// src/watch/lock-file.ts
|
|
3294
|
-
var
|
|
3295
|
-
var
|
|
3296
|
-
var
|
|
4204
|
+
var import_promises12 = require("fs/promises");
|
|
4205
|
+
var import_promises13 = require("fs/promises");
|
|
4206
|
+
var import_node_path15 = require("path");
|
|
3297
4207
|
var LOCK_FILE = ".watcher.lock";
|
|
3298
4208
|
function isProcessAlive(pid) {
|
|
3299
4209
|
try {
|
|
@@ -3304,21 +4214,21 @@ function isProcessAlive(pid) {
|
|
|
3304
4214
|
}
|
|
3305
4215
|
}
|
|
3306
4216
|
async function acquireLock(outDir) {
|
|
3307
|
-
await (0,
|
|
3308
|
-
const lockPath = (0,
|
|
4217
|
+
await (0, import_promises13.mkdir)(outDir, { recursive: true });
|
|
4218
|
+
const lockPath = (0, import_node_path15.join)(outDir, LOCK_FILE);
|
|
3309
4219
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3310
4220
|
try {
|
|
3311
|
-
const fd = await (0,
|
|
4221
|
+
const fd = await (0, import_promises12.open)(lockPath, "wx");
|
|
3312
4222
|
await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
|
|
3313
4223
|
`, "utf8");
|
|
3314
4224
|
await fd.close();
|
|
3315
4225
|
} catch (err) {
|
|
3316
4226
|
if (err.code === "EEXIST") {
|
|
3317
4227
|
try {
|
|
3318
|
-
const raw = await (0,
|
|
4228
|
+
const raw = await (0, import_promises13.readFile)(lockPath, "utf8");
|
|
3319
4229
|
const existing = JSON.parse(raw);
|
|
3320
4230
|
if (isProcessAlive(existing.pid)) return null;
|
|
3321
|
-
await (0,
|
|
4231
|
+
await (0, import_promises13.unlink)(lockPath);
|
|
3322
4232
|
return acquireLock(outDir);
|
|
3323
4233
|
} catch {
|
|
3324
4234
|
return null;
|
|
@@ -3329,7 +4239,7 @@ async function acquireLock(outDir) {
|
|
|
3329
4239
|
return {
|
|
3330
4240
|
release: async () => {
|
|
3331
4241
|
try {
|
|
3332
|
-
await (0,
|
|
4242
|
+
await (0, import_promises13.unlink)(lockPath);
|
|
3333
4243
|
} catch {
|
|
3334
4244
|
}
|
|
3335
4245
|
}
|
|
@@ -3345,7 +4255,7 @@ async function watch(config, onChange) {
|
|
|
3345
4255
|
if (lock === null) {
|
|
3346
4256
|
let holderPid = "unknown";
|
|
3347
4257
|
try {
|
|
3348
|
-
const raw = await (0,
|
|
4258
|
+
const raw = await (0, import_promises14.readFile)((0, import_node_path16.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3349
4259
|
const data = JSON.parse(raw);
|
|
3350
4260
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3351
4261
|
} catch {
|
|
@@ -3355,12 +4265,20 @@ async function watch(config, onChange) {
|
|
|
3355
4265
|
);
|
|
3356
4266
|
return NO_OP_WATCHER;
|
|
3357
4267
|
}
|
|
4268
|
+
let discovery = null;
|
|
4269
|
+
async function getDiscovery() {
|
|
4270
|
+
if (discovery === null) {
|
|
4271
|
+
discovery = await PersistentDiscovery.create({
|
|
4272
|
+
cwd: config.codegen.cwd,
|
|
4273
|
+
glob: config.contracts.glob,
|
|
4274
|
+
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
4275
|
+
});
|
|
4276
|
+
return discovery;
|
|
4277
|
+
}
|
|
4278
|
+
return discovery;
|
|
4279
|
+
}
|
|
3358
4280
|
try {
|
|
3359
|
-
const initialRoutes = await
|
|
3360
|
-
cwd: config.codegen.cwd,
|
|
3361
|
-
glob: config.contracts.glob,
|
|
3362
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3363
|
-
});
|
|
4281
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3364
4282
|
await generate(config, initialRoutes);
|
|
3365
4283
|
} catch (err) {
|
|
3366
4284
|
console.warn(
|
|
@@ -3373,7 +4291,7 @@ async function watch(config, onChange) {
|
|
|
3373
4291
|
}
|
|
3374
4292
|
let pagesDebounceTimer;
|
|
3375
4293
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3376
|
-
const pagesWatcher = import_chokidar.default.watch((0,
|
|
4294
|
+
const pagesWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, pagesGlob), {
|
|
3377
4295
|
ignoreInitial: true,
|
|
3378
4296
|
persistent: true,
|
|
3379
4297
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3399,23 +4317,23 @@ async function watch(config, onChange) {
|
|
|
3399
4317
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3400
4318
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3401
4319
|
let contractsDebounceTimer;
|
|
3402
|
-
const
|
|
4320
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4321
|
+
const contractsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.contracts.glob), {
|
|
3403
4322
|
ignoreInitial: true,
|
|
3404
4323
|
persistent: true,
|
|
3405
4324
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3406
4325
|
});
|
|
3407
|
-
function scheduleContractsRegenerate() {
|
|
4326
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4327
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3408
4328
|
if (contractsDebounceTimer !== void 0) {
|
|
3409
4329
|
clearTimeout(contractsDebounceTimer);
|
|
3410
4330
|
}
|
|
3411
4331
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3412
4332
|
contractsDebounceTimer = void 0;
|
|
4333
|
+
const changed = [...pendingChangedPaths];
|
|
4334
|
+
pendingChangedPaths.clear();
|
|
3413
4335
|
try {
|
|
3414
|
-
const routes = await
|
|
3415
|
-
cwd: config.codegen.cwd,
|
|
3416
|
-
glob: config.contracts.glob,
|
|
3417
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3418
|
-
});
|
|
4336
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3419
4337
|
await generate(config, routes);
|
|
3420
4338
|
} catch (err) {
|
|
3421
4339
|
console.error(
|
|
@@ -3426,17 +4344,17 @@ async function watch(config, onChange) {
|
|
|
3426
4344
|
onChange?.();
|
|
3427
4345
|
}, config.contracts.debounceMs);
|
|
3428
4346
|
}
|
|
3429
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3430
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3431
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3432
|
-
const formsWatcher = import_chokidar.default.watch((0,
|
|
4347
|
+
contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4348
|
+
contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4349
|
+
contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
4350
|
+
const formsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.forms.watch), {
|
|
3433
4351
|
ignoreInitial: true,
|
|
3434
4352
|
persistent: true,
|
|
3435
4353
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3436
4354
|
});
|
|
3437
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3438
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3439
|
-
formsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
4355
|
+
formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4356
|
+
formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4357
|
+
formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
3440
4358
|
return {
|
|
3441
4359
|
close: async () => {
|
|
3442
4360
|
if (pagesDebounceTimer !== void 0) {
|