@dudousxd/nestjs-codegen 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/cli/main.cjs +1080 -160
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1063 -143
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/{index-DA4uySjo.d.cts → index-B0mS84Jj.d.cts} +83 -1
- package/dist/{index-DA4uySjo.d.ts → index-B0mS84Jj.d.ts} +83 -1
- package/dist/index.cjs +1053 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -4
- package/dist/index.d.ts +104 -4
- package/dist/index.js +1034 -105
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +1015 -113
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +1 -1
- package/dist/nest/index.d.ts +1 -1
- package/dist/nest/index.js +1009 -107
- package/dist/nest/index.js.map +1 -1
- package/package.json +30 -11
package/dist/index.js
CHANGED
|
@@ -131,6 +131,19 @@ function applyDefaults(userConfig, cwd) {
|
|
|
131
131
|
enabled: userConfig.forms?.enabled ?? true,
|
|
132
132
|
watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
|
|
133
133
|
zodImport: userConfig.forms?.zodImport ?? "zod"
|
|
134
|
+
},
|
|
135
|
+
openapi: {
|
|
136
|
+
enabled: userConfig.openapi?.enabled ?? false,
|
|
137
|
+
fileName: userConfig.openapi?.fileName ?? "openapi.json",
|
|
138
|
+
title: userConfig.openapi?.title ?? "NestJS API",
|
|
139
|
+
version: userConfig.openapi?.version ?? "1.0.0",
|
|
140
|
+
description: userConfig.openapi?.description ?? null
|
|
141
|
+
},
|
|
142
|
+
mocks: {
|
|
143
|
+
enabled: userConfig.mocks?.enabled ?? false,
|
|
144
|
+
fileName: userConfig.mocks?.fileName ?? "mocks.ts",
|
|
145
|
+
seed: userConfig.mocks?.seed ?? 1,
|
|
146
|
+
baseUrl: userConfig.mocks?.baseUrl ?? ""
|
|
134
147
|
}
|
|
135
148
|
};
|
|
136
149
|
}
|
|
@@ -168,8 +181,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
168
181
|
}
|
|
169
182
|
|
|
170
183
|
// src/generate.ts
|
|
171
|
-
import { mkdir as
|
|
172
|
-
import { dirname as dirname2, join as
|
|
184
|
+
import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
|
|
185
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
173
186
|
|
|
174
187
|
// src/discovery/pages.ts
|
|
175
188
|
import { readFile } from "fs/promises";
|
|
@@ -698,17 +711,28 @@ function emitFilterQueryType(c) {
|
|
|
698
711
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
699
712
|
}
|
|
700
713
|
function buildResponseType(c, outDir) {
|
|
714
|
+
const respRef = c.contractSource.responseRef;
|
|
715
|
+
if (c.contractSource.stream) {
|
|
716
|
+
if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
717
|
+
return c.contractSource.response;
|
|
718
|
+
}
|
|
701
719
|
if (c.controllerRef) {
|
|
702
720
|
let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
703
721
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
704
722
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
705
723
|
}
|
|
706
|
-
const respRef = c.contractSource.responseRef;
|
|
707
724
|
if (respRef) {
|
|
708
725
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
709
726
|
}
|
|
710
727
|
return c.contractSource.response;
|
|
711
728
|
}
|
|
729
|
+
function buildErrorType(c) {
|
|
730
|
+
const errRef = c.contractSource.errorRef;
|
|
731
|
+
if (errRef) {
|
|
732
|
+
return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
|
|
733
|
+
}
|
|
734
|
+
return c.contractSource.error ?? "unknown";
|
|
735
|
+
}
|
|
712
736
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
713
737
|
const pad = " ".repeat(indent);
|
|
714
738
|
const lines = [];
|
|
@@ -723,12 +747,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
723
747
|
const bodyRef = c.contractSource.bodyRef;
|
|
724
748
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
725
749
|
const response = buildResponseType(c, outDir);
|
|
750
|
+
const error = buildErrorType(c);
|
|
726
751
|
const params = buildParamsType(c.params);
|
|
727
752
|
const safeMethod = JSON.stringify(method);
|
|
728
753
|
const safeUrl = JSON.stringify(c.path);
|
|
729
754
|
const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
|
|
755
|
+
const stream = c.contractSource.stream ? "true" : "false";
|
|
730
756
|
lines.push(
|
|
731
|
-
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
|
|
757
|
+
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
|
|
732
758
|
);
|
|
733
759
|
} else {
|
|
734
760
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -800,15 +826,21 @@ function emitReqHelper() {
|
|
|
800
826
|
""
|
|
801
827
|
];
|
|
802
828
|
}
|
|
803
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
829
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
804
830
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
805
831
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
832
|
+
if (streamExpr) {
|
|
833
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
834
|
+
}
|
|
806
835
|
for (const [name, value] of Object.entries(members)) {
|
|
807
836
|
lines.push(`${pad} ${name}: ${value},`);
|
|
808
837
|
}
|
|
809
838
|
lines.push(`${pad}}),`);
|
|
810
839
|
return lines;
|
|
811
840
|
}
|
|
841
|
+
function renderStreamExpr(req) {
|
|
842
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
843
|
+
}
|
|
812
844
|
function emitApiObjectBlock(tree, indent, p) {
|
|
813
845
|
const pad = " ".repeat(indent);
|
|
814
846
|
const lines = [];
|
|
@@ -843,7 +875,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
843
875
|
}
|
|
844
876
|
const members = {};
|
|
845
877
|
for (const [name, { value }] of owned) members[name] = value;
|
|
846
|
-
|
|
878
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
879
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
847
880
|
}
|
|
848
881
|
return lines;
|
|
849
882
|
}
|
|
@@ -881,6 +914,8 @@ var ROUTE_NAMESPACE = [
|
|
|
881
914
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
882
915
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
883
916
|
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
917
|
+
" /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
|
|
918
|
+
' export type Stream<K extends string> = ResolveByName<K, "response">;',
|
|
884
919
|
" export type Request<K extends string> = {",
|
|
885
920
|
" body: Body<K>;",
|
|
886
921
|
" query: Query<K>;",
|
|
@@ -897,6 +932,7 @@ var PATH_NAMESPACE = [
|
|
|
897
932
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
898
933
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
899
934
|
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
935
|
+
' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
900
936
|
"}",
|
|
901
937
|
""
|
|
902
938
|
];
|
|
@@ -908,6 +944,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
908
944
|
" export type Params<K extends string> = never;",
|
|
909
945
|
" export type Error<K extends string> = never;",
|
|
910
946
|
" export type FilterFields<K extends string> = never;",
|
|
947
|
+
" export type Stream<K extends string> = never;",
|
|
911
948
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
912
949
|
"}",
|
|
913
950
|
""
|
|
@@ -920,6 +957,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
920
957
|
" export type Params<M extends string, U extends string> = never;",
|
|
921
958
|
" export type Error<M extends string, U extends string> = never;",
|
|
922
959
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
960
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
923
961
|
"}",
|
|
924
962
|
""
|
|
925
963
|
];
|
|
@@ -943,7 +981,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
943
981
|
for (const r of contracted) {
|
|
944
982
|
const cs = r.contract?.contractSource;
|
|
945
983
|
if (!cs) continue;
|
|
946
|
-
const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
|
|
984
|
+
const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
|
|
947
985
|
for (const ref of refs) {
|
|
948
986
|
if (!ref) continue;
|
|
949
987
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -1121,18 +1159,27 @@ function refRootIdentifier(refName) {
|
|
|
1121
1159
|
function hasSource(src) {
|
|
1122
1160
|
return !!(src.schema || src.zodText || src.zodRef);
|
|
1123
1161
|
}
|
|
1162
|
+
function escapeRegExp(s) {
|
|
1163
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1164
|
+
}
|
|
1165
|
+
var wordBoundaryRegexCache = /* @__PURE__ */ new Map();
|
|
1166
|
+
function wordBoundaryRegex(token) {
|
|
1167
|
+
let re = wordBoundaryRegexCache.get(token);
|
|
1168
|
+
if (re === void 0) {
|
|
1169
|
+
re = new RegExp(`\\b${escapeRegExp(token)}\\b`, "g");
|
|
1170
|
+
wordBoundaryRegexCache.set(token, re);
|
|
1171
|
+
}
|
|
1172
|
+
return re;
|
|
1173
|
+
}
|
|
1124
1174
|
function applyRenames(text, renames) {
|
|
1125
1175
|
if (!renames || renames.size === 0) return text;
|
|
1126
1176
|
let out = text;
|
|
1127
1177
|
for (const [from, to] of renames) {
|
|
1128
1178
|
if (from === to) continue;
|
|
1129
|
-
out = out.replace(
|
|
1179
|
+
out = out.replace(wordBoundaryRegex(from), to);
|
|
1130
1180
|
}
|
|
1131
1181
|
return out;
|
|
1132
1182
|
}
|
|
1133
|
-
function escapeRegExp(s) {
|
|
1134
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1135
|
-
}
|
|
1136
1183
|
function isSelfReferential(name, text) {
|
|
1137
1184
|
return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
|
|
1138
1185
|
}
|
|
@@ -1144,7 +1191,23 @@ function planNestedSchemas(entries) {
|
|
|
1144
1191
|
const local = Object.entries(entry.nestedSchemas);
|
|
1145
1192
|
if (local.length === 0) continue;
|
|
1146
1193
|
const rename = /* @__PURE__ */ new Map();
|
|
1147
|
-
|
|
1194
|
+
const renameValues = /* @__PURE__ */ new Set();
|
|
1195
|
+
const setRename = (key, value) => {
|
|
1196
|
+
const prev = rename.get(key);
|
|
1197
|
+
rename.set(key, value);
|
|
1198
|
+
if (prev !== void 0 && prev !== value) {
|
|
1199
|
+
let stillUsed = false;
|
|
1200
|
+
for (const v of rename.values()) {
|
|
1201
|
+
if (v === prev) {
|
|
1202
|
+
stillUsed = true;
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (!stillUsed) renameValues.delete(prev);
|
|
1207
|
+
}
|
|
1208
|
+
renameValues.add(value);
|
|
1209
|
+
};
|
|
1210
|
+
for (const [name] of local) setRename(name, name);
|
|
1148
1211
|
const textFor = (name) => {
|
|
1149
1212
|
const raw = entry.nestedSchemas?.[name] ?? "";
|
|
1150
1213
|
return applyRenames(raw, rename);
|
|
@@ -1162,11 +1225,11 @@ function planNestedSchemas(entries) {
|
|
|
1162
1225
|
if (existing === text) continue;
|
|
1163
1226
|
let i = 2;
|
|
1164
1227
|
let candidate = `${name}_${i}`;
|
|
1165
|
-
while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) ||
|
|
1228
|
+
while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || renameValues.has(candidate)) {
|
|
1166
1229
|
i += 1;
|
|
1167
1230
|
candidate = `${name}_${i}`;
|
|
1168
1231
|
}
|
|
1169
|
-
|
|
1232
|
+
setRename(name, candidate);
|
|
1170
1233
|
changed = true;
|
|
1171
1234
|
}
|
|
1172
1235
|
}
|
|
@@ -1376,11 +1439,470 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
1376
1439
|
await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
|
|
1377
1440
|
}
|
|
1378
1441
|
|
|
1379
|
-
// src/emit/emit-
|
|
1442
|
+
// src/emit/emit-mocks.ts
|
|
1380
1443
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1381
|
-
import { join as join8
|
|
1382
|
-
|
|
1444
|
+
import { join as join8 } from "path";
|
|
1445
|
+
|
|
1446
|
+
// src/ir/schema-node-to-json-schema.ts
|
|
1447
|
+
var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
|
|
1448
|
+
function parseLiteral(raw) {
|
|
1449
|
+
const t = raw.trim();
|
|
1450
|
+
if (t === "true") return true;
|
|
1451
|
+
if (t === "false") return false;
|
|
1452
|
+
if (t === "null") return null;
|
|
1453
|
+
const q = t[0];
|
|
1454
|
+
if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
|
|
1455
|
+
return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
|
|
1456
|
+
}
|
|
1457
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
|
|
1458
|
+
return Number(t);
|
|
1459
|
+
}
|
|
1460
|
+
return t;
|
|
1461
|
+
}
|
|
1462
|
+
function literalsType(values) {
|
|
1463
|
+
const types = new Set(values.map((v) => v === null ? "null" : typeof v));
|
|
1464
|
+
if (types.size === 1) {
|
|
1465
|
+
const only = [...types][0];
|
|
1466
|
+
if (only === "string") return "string";
|
|
1467
|
+
if (only === "number") return "number";
|
|
1468
|
+
if (only === "boolean") return "boolean";
|
|
1469
|
+
}
|
|
1470
|
+
return void 0;
|
|
1471
|
+
}
|
|
1472
|
+
function convert(node, ctx) {
|
|
1473
|
+
switch (node.kind) {
|
|
1474
|
+
case "string": {
|
|
1475
|
+
const out = { type: "string" };
|
|
1476
|
+
for (const c of node.checks) {
|
|
1477
|
+
if (c.check === "email") out.format = "email";
|
|
1478
|
+
else if (c.check === "url") out.format = "uri";
|
|
1479
|
+
else if (c.check === "uuid") out.format = "uuid";
|
|
1480
|
+
else if (c.check === "min") out.minLength = Number(c.value);
|
|
1481
|
+
else if (c.check === "max") out.maxLength = Number(c.value);
|
|
1482
|
+
else if (c.check === "regex") {
|
|
1483
|
+
const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
|
|
1484
|
+
out.pattern = m ? m[1] : c.pattern;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return out;
|
|
1488
|
+
}
|
|
1489
|
+
case "number": {
|
|
1490
|
+
const out = { type: "number" };
|
|
1491
|
+
for (const c of node.checks) {
|
|
1492
|
+
if (c.check === "int") out.type = "integer";
|
|
1493
|
+
else if (c.check === "min") out.minimum = Number(c.value);
|
|
1494
|
+
else if (c.check === "max") out.maximum = Number(c.value);
|
|
1495
|
+
else if (c.check === "positive") out.exclusiveMinimum = 0;
|
|
1496
|
+
else if (c.check === "negative") out.exclusiveMaximum = 0;
|
|
1497
|
+
}
|
|
1498
|
+
return out;
|
|
1499
|
+
}
|
|
1500
|
+
case "boolean":
|
|
1501
|
+
return { type: "boolean" };
|
|
1502
|
+
case "date":
|
|
1503
|
+
return { type: "string", format: "date-time" };
|
|
1504
|
+
case "unknown":
|
|
1505
|
+
return node.note ? { description: node.note } : {};
|
|
1506
|
+
case "instanceof":
|
|
1507
|
+
return { type: "object", description: `instanceof ${node.ctor}` };
|
|
1508
|
+
case "enum": {
|
|
1509
|
+
const values = node.literals.map(parseLiteral);
|
|
1510
|
+
const t = literalsType(values);
|
|
1511
|
+
const out = { enum: values };
|
|
1512
|
+
if (t) out.type = t;
|
|
1513
|
+
return out;
|
|
1514
|
+
}
|
|
1515
|
+
case "literal": {
|
|
1516
|
+
const value = parseLiteral(node.raw);
|
|
1517
|
+
const out = { const: value };
|
|
1518
|
+
const t = literalsType([value]);
|
|
1519
|
+
if (t) out.type = t;
|
|
1520
|
+
return out;
|
|
1521
|
+
}
|
|
1522
|
+
case "union": {
|
|
1523
|
+
const options = node.options.map((o) => convert(o, ctx));
|
|
1524
|
+
const out = { oneOf: options };
|
|
1525
|
+
if (node.discriminator) {
|
|
1526
|
+
out.discriminator = { propertyName: node.discriminator };
|
|
1527
|
+
}
|
|
1528
|
+
return out;
|
|
1529
|
+
}
|
|
1530
|
+
case "object": {
|
|
1531
|
+
const properties = {};
|
|
1532
|
+
const required = [];
|
|
1533
|
+
for (const f of node.fields) {
|
|
1534
|
+
if (f.value.kind === "optional") {
|
|
1535
|
+
properties[f.key] = convert(f.value.inner, ctx);
|
|
1536
|
+
} else {
|
|
1537
|
+
properties[f.key] = convert(f.value, ctx);
|
|
1538
|
+
required.push(f.key);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const out = { type: "object", properties };
|
|
1542
|
+
if (required.length > 0) out.required = required;
|
|
1543
|
+
out.additionalProperties = node.passthrough;
|
|
1544
|
+
return out;
|
|
1545
|
+
}
|
|
1546
|
+
case "array":
|
|
1547
|
+
return { type: "array", items: convert(node.element, ctx) };
|
|
1548
|
+
case "optional":
|
|
1549
|
+
return widenNullable(convert(node.inner, ctx));
|
|
1550
|
+
case "ref":
|
|
1551
|
+
case "lazyRef":
|
|
1552
|
+
return { $ref: `${ctx.refPrefix}${node.name}` };
|
|
1553
|
+
case "annotated":
|
|
1554
|
+
return convert(node.inner, ctx);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function widenNullable(schema) {
|
|
1558
|
+
if (schema.$ref) {
|
|
1559
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1560
|
+
}
|
|
1561
|
+
if (typeof schema.type === "string") {
|
|
1562
|
+
return { ...schema, type: [schema.type, "null"] };
|
|
1563
|
+
}
|
|
1564
|
+
if (Array.isArray(schema.type)) {
|
|
1565
|
+
return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
|
|
1566
|
+
}
|
|
1567
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1568
|
+
}
|
|
1569
|
+
function schemaNodeToJsonSchema(node, ctx = DEFAULT_CTX) {
|
|
1570
|
+
return convert(node, ctx);
|
|
1571
|
+
}
|
|
1572
|
+
function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
|
|
1573
|
+
const named = {};
|
|
1574
|
+
for (const [name, node] of mod.named) {
|
|
1575
|
+
named[name] = convert(node, ctx);
|
|
1576
|
+
}
|
|
1577
|
+
return { root: convert(mod.root, ctx), named };
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/emit/mock-gen-runtime.ts
|
|
1581
|
+
var MOCK_GEN_RUNTIME = `
|
|
1582
|
+
/** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
|
|
1583
|
+
function makeRng(seed) {
|
|
1584
|
+
let a = seed >>> 0;
|
|
1585
|
+
return {
|
|
1586
|
+
next() {
|
|
1587
|
+
a |= 0;
|
|
1588
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
1589
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
1590
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
1591
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1592
|
+
},
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function __pick(rng, items) {
|
|
1597
|
+
return items[Math.floor(rng.next() * items.length)];
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function __intBetween(rng, min, max) {
|
|
1601
|
+
return Math.floor(rng.next() * (max - min + 1)) + min;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
|
|
1605
|
+
const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
|
|
1606
|
+
const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
|
|
1607
|
+
|
|
1608
|
+
function __fakeWords(rng, count) {
|
|
1609
|
+
let out = [];
|
|
1610
|
+
for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
|
|
1611
|
+
return out.join(' ');
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function __hex(rng, len) {
|
|
1615
|
+
let s = '';
|
|
1616
|
+
for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
|
|
1617
|
+
return s;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function __fakeUuid(rng) {
|
|
1621
|
+
return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function __fakeString(rng, schema) {
|
|
1625
|
+
switch (schema.format) {
|
|
1626
|
+
case 'email':
|
|
1627
|
+
return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
|
|
1628
|
+
case 'uri':
|
|
1629
|
+
case 'url':
|
|
1630
|
+
return 'https://example.com/' + __pick(rng, __WORDS);
|
|
1631
|
+
case 'uuid':
|
|
1632
|
+
return __fakeUuid(rng);
|
|
1633
|
+
case 'date-time':
|
|
1634
|
+
return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
|
|
1635
|
+
default:
|
|
1636
|
+
return __fakeWords(rng, __intBetween(rng, 1, 3));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
|
|
1641
|
+
function generateMock(schema, rng, defs, depth) {
|
|
1642
|
+
defs = defs || {};
|
|
1643
|
+
depth = depth || 0;
|
|
1644
|
+
if (schema.$ref) {
|
|
1645
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
1646
|
+
const target = defs[name];
|
|
1647
|
+
if (!target || depth > 4) return null;
|
|
1648
|
+
return generateMock(target, rng, defs, depth + 1);
|
|
1649
|
+
}
|
|
1650
|
+
if ('const' in schema) return schema.const;
|
|
1651
|
+
if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
|
|
1652
|
+
if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
|
|
1653
|
+
if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
|
|
1654
|
+
let type = Array.isArray(schema.type)
|
|
1655
|
+
? (schema.type.filter((t) => t !== 'null')[0] || 'null')
|
|
1656
|
+
: schema.type;
|
|
1657
|
+
switch (type) {
|
|
1658
|
+
case 'string':
|
|
1659
|
+
return __fakeString(rng, schema);
|
|
1660
|
+
case 'integer':
|
|
1661
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
|
|
1662
|
+
case 'number':
|
|
1663
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
|
|
1664
|
+
case 'boolean':
|
|
1665
|
+
return rng.next() < 0.5;
|
|
1666
|
+
case 'null':
|
|
1667
|
+
return null;
|
|
1668
|
+
case 'array': {
|
|
1669
|
+
const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
|
|
1670
|
+
const items = schema.items || {};
|
|
1671
|
+
let arr = [];
|
|
1672
|
+
for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
|
|
1673
|
+
return arr;
|
|
1674
|
+
}
|
|
1675
|
+
case 'object': {
|
|
1676
|
+
const out = {};
|
|
1677
|
+
const props = schema.properties || {};
|
|
1678
|
+
for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
|
|
1679
|
+
return out;
|
|
1680
|
+
}
|
|
1681
|
+
default:
|
|
1682
|
+
return {};
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
`.trim();
|
|
1686
|
+
|
|
1687
|
+
// src/emit/emit-mocks.ts
|
|
1688
|
+
var REF_PREFIX = "#/components/schemas/";
|
|
1689
|
+
function toMswPath(path, baseUrl) {
|
|
1690
|
+
return `${baseUrl}${path}`;
|
|
1691
|
+
}
|
|
1692
|
+
function responseSchemaFor(route, defs) {
|
|
1693
|
+
const cs = route.contract.contractSource;
|
|
1694
|
+
if (cs.responseSchema) {
|
|
1695
|
+
const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
|
|
1696
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1697
|
+
if (!(name in defs)) defs[name] = node;
|
|
1698
|
+
}
|
|
1699
|
+
return root;
|
|
1700
|
+
}
|
|
1701
|
+
return {};
|
|
1702
|
+
}
|
|
1703
|
+
function buildMocksFile(routes, opts = {}) {
|
|
1704
|
+
const seed = opts.seed ?? 1;
|
|
1705
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
1706
|
+
const contracted = routes.filter((r) => r.contract);
|
|
1707
|
+
const defs = {};
|
|
1708
|
+
const handlers = [];
|
|
1709
|
+
for (const r of contracted) {
|
|
1710
|
+
const schema = responseSchemaFor(r, defs);
|
|
1711
|
+
const method = r.method.toLowerCase();
|
|
1712
|
+
const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
|
|
1713
|
+
const path = toMswPath(r.path, baseUrl);
|
|
1714
|
+
const cs = r.contract.contractSource;
|
|
1715
|
+
const schemaLiteral = JSON.stringify(schema);
|
|
1716
|
+
const pathLit = JSON.stringify(path);
|
|
1717
|
+
if (cs.stream) {
|
|
1718
|
+
handlers.push(
|
|
1719
|
+
[
|
|
1720
|
+
` // ${r.name} (stream)`,
|
|
1721
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1722
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1723
|
+
" const body = `data: ${JSON.stringify(value)}\\n\\n`;",
|
|
1724
|
+
" return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
|
|
1725
|
+
" }),"
|
|
1726
|
+
].join("\n")
|
|
1727
|
+
);
|
|
1728
|
+
} else {
|
|
1729
|
+
handlers.push(
|
|
1730
|
+
[
|
|
1731
|
+
` // ${r.name}`,
|
|
1732
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1733
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1734
|
+
" return HttpResponse.json(value);",
|
|
1735
|
+
" }),"
|
|
1736
|
+
].join("\n")
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
const lines = [
|
|
1741
|
+
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1742
|
+
"// MSW handlers returning deterministic, schema-shaped mock data.",
|
|
1743
|
+
"/* eslint-disable */",
|
|
1744
|
+
"// @ts-nocheck",
|
|
1745
|
+
"",
|
|
1746
|
+
"import { http, HttpResponse } from 'msw';",
|
|
1747
|
+
"",
|
|
1748
|
+
`const SEED = ${seed};`,
|
|
1749
|
+
"",
|
|
1750
|
+
"// ---------------------------------------------------------------------------",
|
|
1751
|
+
"// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
|
|
1752
|
+
"// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
|
|
1753
|
+
"// ---------------------------------------------------------------------------",
|
|
1754
|
+
MOCK_GEN_RUNTIME,
|
|
1755
|
+
"",
|
|
1756
|
+
"// Shared component schemas referenced by $ref.",
|
|
1757
|
+
`const DEFS = ${JSON.stringify(defs, null, 2)};`,
|
|
1758
|
+
"",
|
|
1759
|
+
"/** MSW request handlers, one per contracted route. */",
|
|
1760
|
+
"export const handlers = [",
|
|
1761
|
+
...handlers,
|
|
1762
|
+
"];",
|
|
1763
|
+
""
|
|
1764
|
+
];
|
|
1765
|
+
return lines.join("\n");
|
|
1766
|
+
}
|
|
1767
|
+
async function emitMocks(routes, outDir, opts = {}) {
|
|
1383
1768
|
await mkdir5(outDir, { recursive: true });
|
|
1769
|
+
const content = buildMocksFile(routes, opts);
|
|
1770
|
+
const fileName = opts.fileName ?? "mocks.ts";
|
|
1771
|
+
await writeFile5(join8(outDir, fileName), content, "utf8");
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/emit/emit-openapi.ts
|
|
1775
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1776
|
+
import { join as join9 } from "path";
|
|
1777
|
+
var REF_PREFIX2 = "#/components/schemas/";
|
|
1778
|
+
function toOpenApiPath(path) {
|
|
1779
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
1780
|
+
}
|
|
1781
|
+
function positionSchema(schema, tsType, components) {
|
|
1782
|
+
if (schema) {
|
|
1783
|
+
const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
|
|
1784
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1785
|
+
if (!(name in components)) components[name] = node;
|
|
1786
|
+
}
|
|
1787
|
+
return root;
|
|
1788
|
+
}
|
|
1789
|
+
return tsType ? { description: tsType } : {};
|
|
1790
|
+
}
|
|
1791
|
+
function buildParameters(route) {
|
|
1792
|
+
const params = [];
|
|
1793
|
+
for (const p of route.params) {
|
|
1794
|
+
if (p.source === "path") {
|
|
1795
|
+
params.push({
|
|
1796
|
+
name: p.name,
|
|
1797
|
+
in: "path",
|
|
1798
|
+
required: true,
|
|
1799
|
+
schema: { type: "string" }
|
|
1800
|
+
});
|
|
1801
|
+
} else if (p.source === "query") {
|
|
1802
|
+
params.push({
|
|
1803
|
+
name: p.name,
|
|
1804
|
+
in: "query",
|
|
1805
|
+
required: false,
|
|
1806
|
+
schema: { type: "string" }
|
|
1807
|
+
});
|
|
1808
|
+
} else if (p.source === "header") {
|
|
1809
|
+
params.push({
|
|
1810
|
+
name: p.name,
|
|
1811
|
+
in: "header",
|
|
1812
|
+
required: false,
|
|
1813
|
+
schema: { type: "string" }
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return params;
|
|
1818
|
+
}
|
|
1819
|
+
function buildResponses(cs, components) {
|
|
1820
|
+
const responses = {};
|
|
1821
|
+
const successSchema = positionSchema(
|
|
1822
|
+
// Prefer rich response IR when present; otherwise fall back to the TS type.
|
|
1823
|
+
cs.responseSchema ?? null,
|
|
1824
|
+
cs.response,
|
|
1825
|
+
components
|
|
1826
|
+
);
|
|
1827
|
+
const successContentType = cs.stream ? "text/event-stream" : "application/json";
|
|
1828
|
+
responses["200"] = {
|
|
1829
|
+
description: cs.stream ? "Server-sent event stream" : "Successful response",
|
|
1830
|
+
content: { [successContentType]: { schema: successSchema } }
|
|
1831
|
+
};
|
|
1832
|
+
const errorSchema = positionSchema(null, cs.error ?? null, components);
|
|
1833
|
+
const errorBody = {
|
|
1834
|
+
description: "Error response",
|
|
1835
|
+
content: { "application/json": { schema: errorSchema } }
|
|
1836
|
+
};
|
|
1837
|
+
if (cs.error || cs.errorRef) {
|
|
1838
|
+
responses["400"] = errorBody;
|
|
1839
|
+
responses.default = errorBody;
|
|
1840
|
+
} else {
|
|
1841
|
+
responses.default = {
|
|
1842
|
+
description: "Error response",
|
|
1843
|
+
content: { "application/json": { schema: {} } }
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
return responses;
|
|
1847
|
+
}
|
|
1848
|
+
function buildOperation(route, components) {
|
|
1849
|
+
const cs = route.contract.contractSource;
|
|
1850
|
+
const op = {
|
|
1851
|
+
operationId: route.name,
|
|
1852
|
+
parameters: buildParameters(route),
|
|
1853
|
+
responses: buildResponses(cs, components)
|
|
1854
|
+
};
|
|
1855
|
+
const method = route.method.toUpperCase();
|
|
1856
|
+
const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
|
|
1857
|
+
if (hasBody && (cs.bodySchema || cs.body)) {
|
|
1858
|
+
const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
|
|
1859
|
+
op.requestBody = {
|
|
1860
|
+
required: true,
|
|
1861
|
+
content: { "application/json": { schema: bodySchema } }
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
return op;
|
|
1865
|
+
}
|
|
1866
|
+
function buildOpenApiSpec(routes, opts = {}) {
|
|
1867
|
+
const components = {};
|
|
1868
|
+
const paths = {};
|
|
1869
|
+
for (const route of routes) {
|
|
1870
|
+
if (!route.contract) continue;
|
|
1871
|
+
const oaPath = toOpenApiPath(route.path);
|
|
1872
|
+
const method = route.method.toLowerCase();
|
|
1873
|
+
let pathItem = paths[oaPath];
|
|
1874
|
+
if (!pathItem) {
|
|
1875
|
+
pathItem = {};
|
|
1876
|
+
paths[oaPath] = pathItem;
|
|
1877
|
+
}
|
|
1878
|
+
pathItem[method] = buildOperation(route, components);
|
|
1879
|
+
}
|
|
1880
|
+
const info = opts.info ?? {};
|
|
1881
|
+
const doc = {
|
|
1882
|
+
openapi: "3.1.0",
|
|
1883
|
+
info: {
|
|
1884
|
+
title: info.title ?? "NestJS API",
|
|
1885
|
+
version: info.version ?? "1.0.0",
|
|
1886
|
+
...info.description ? { description: info.description } : {}
|
|
1887
|
+
},
|
|
1888
|
+
paths,
|
|
1889
|
+
components: { schemas: components }
|
|
1890
|
+
};
|
|
1891
|
+
return doc;
|
|
1892
|
+
}
|
|
1893
|
+
async function emitOpenApi(routes, outDir, opts = {}) {
|
|
1894
|
+
await mkdir6(outDir, { recursive: true });
|
|
1895
|
+
const doc = buildOpenApiSpec(routes, opts);
|
|
1896
|
+
const fileName = opts.fileName ?? "openapi.json";
|
|
1897
|
+
await writeFile6(join9(outDir, fileName), `${JSON.stringify(doc, null, 2)}
|
|
1898
|
+
`, "utf8");
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// src/emit/emit-pages.ts
|
|
1902
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
1903
|
+
import { join as join10, relative as relative5 } from "path";
|
|
1904
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
1905
|
+
await mkdir7(outDir, { recursive: true });
|
|
1384
1906
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
1385
1907
|
const augBody = pages.map((p) => {
|
|
1386
1908
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -1399,7 +1921,7 @@ ${augBody}
|
|
|
1399
1921
|
}
|
|
1400
1922
|
${sharedPropsBlock}}
|
|
1401
1923
|
`;
|
|
1402
|
-
await
|
|
1924
|
+
await writeFile7(join10(outDir, "pages.d.ts"), content, "utf8");
|
|
1403
1925
|
}
|
|
1404
1926
|
function buildSharedPropsBlock(sharedProps) {
|
|
1405
1927
|
if (!sharedProps) return "";
|
|
@@ -1429,12 +1951,12 @@ function needsQuotes(name) {
|
|
|
1429
1951
|
}
|
|
1430
1952
|
|
|
1431
1953
|
// src/emit/emit-routes.ts
|
|
1432
|
-
import { mkdir as
|
|
1433
|
-
import { join as
|
|
1954
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
1955
|
+
import { join as join11 } from "path";
|
|
1434
1956
|
async function emitRoutes(routes, outDir) {
|
|
1435
|
-
await
|
|
1957
|
+
await mkdir8(outDir, { recursive: true });
|
|
1436
1958
|
const content = buildRoutesFile(routes);
|
|
1437
|
-
await
|
|
1959
|
+
await writeFile8(join11(outDir, "routes.ts"), content, "utf8");
|
|
1438
1960
|
}
|
|
1439
1961
|
function buildRoutesFile(routes) {
|
|
1440
1962
|
if (routes.length === 0) {
|
|
@@ -1582,24 +2104,41 @@ async function generate(config, inputRoutes = []) {
|
|
|
1582
2104
|
});
|
|
1583
2105
|
}
|
|
1584
2106
|
const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
|
|
2107
|
+
if (hasContracts && config.openapi.enabled) {
|
|
2108
|
+
await emitOpenApi(routes, config.codegen.outDir, {
|
|
2109
|
+
fileName: config.openapi.fileName,
|
|
2110
|
+
info: {
|
|
2111
|
+
title: config.openapi.title,
|
|
2112
|
+
version: config.openapi.version,
|
|
2113
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
if (hasContracts && config.mocks.enabled) {
|
|
2118
|
+
await emitMocks(routes, config.codegen.outDir, {
|
|
2119
|
+
fileName: config.mocks.fileName,
|
|
2120
|
+
seed: config.mocks.seed,
|
|
2121
|
+
baseUrl: config.mocks.baseUrl
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
1585
2124
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
1586
2125
|
if (extensions.length > 0) {
|
|
1587
2126
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1588
2127
|
for (const file of extraFiles) {
|
|
1589
|
-
const dest =
|
|
1590
|
-
await
|
|
1591
|
-
await
|
|
2128
|
+
const dest = join12(config.codegen.outDir, file.path);
|
|
2129
|
+
await mkdir9(dirname2(dest), { recursive: true });
|
|
2130
|
+
await writeFile9(dest, file.contents, "utf8");
|
|
1592
2131
|
}
|
|
1593
2132
|
}
|
|
1594
2133
|
}
|
|
1595
2134
|
|
|
1596
2135
|
// src/watch/watcher.ts
|
|
1597
2136
|
import { readFile as readFile3 } from "fs/promises";
|
|
1598
|
-
import { join as
|
|
2137
|
+
import { join as join15 } from "path";
|
|
1599
2138
|
import chokidar from "chokidar";
|
|
1600
2139
|
|
|
1601
2140
|
// src/discovery/contracts-fast.ts
|
|
1602
|
-
import { join as
|
|
2141
|
+
import { join as join13, resolve as resolve3 } from "path";
|
|
1603
2142
|
import fg2 from "fast-glob";
|
|
1604
2143
|
import {
|
|
1605
2144
|
Node as Node8,
|
|
@@ -1781,7 +2320,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
|
|
|
1781
2320
|
}
|
|
1782
2321
|
return null;
|
|
1783
2322
|
}
|
|
2323
|
+
function resolveImportedVariable(name, sourceFile, project) {
|
|
2324
|
+
const local = sourceFile.getVariableDeclaration(name);
|
|
2325
|
+
if (local) return { decl: local, file: sourceFile };
|
|
2326
|
+
return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
|
|
2327
|
+
}
|
|
2328
|
+
function resolveVariableViaImports(name, sourceFile, project, seen) {
|
|
2329
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
2330
|
+
const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
|
|
2331
|
+
if (!namedImport) continue;
|
|
2332
|
+
const sourceName = namedImport.getName();
|
|
2333
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
2334
|
+
const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
|
|
2335
|
+
if (found) return found;
|
|
2336
|
+
}
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
|
|
2340
|
+
const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
|
|
2341
|
+
for (const candidate of candidates) {
|
|
2342
|
+
let importedFile = project.getSourceFile(candidate);
|
|
2343
|
+
if (!importedFile) {
|
|
2344
|
+
try {
|
|
2345
|
+
importedFile = project.addSourceFileAtPath(candidate);
|
|
2346
|
+
} catch {
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const found = resolveVariableInFile(name, importedFile, project, seen);
|
|
2351
|
+
if (found) return found;
|
|
2352
|
+
}
|
|
2353
|
+
return null;
|
|
2354
|
+
}
|
|
2355
|
+
function resolveVariableInFile(name, file, project, seen) {
|
|
2356
|
+
const filePath = file.getFilePath();
|
|
2357
|
+
if (seen.has(filePath)) return null;
|
|
2358
|
+
seen.add(filePath);
|
|
2359
|
+
const local = file.getVariableDeclaration(name);
|
|
2360
|
+
if (local) return { decl: local, file };
|
|
2361
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
2362
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
2363
|
+
const namedExports = exportDecl.getNamedExports();
|
|
2364
|
+
if (moduleSpecifier) {
|
|
2365
|
+
const hasStar = namedExports.length === 0;
|
|
2366
|
+
const reExport2 = namedExports.find(
|
|
2367
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2368
|
+
);
|
|
2369
|
+
if (!hasStar && !reExport2) continue;
|
|
2370
|
+
const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
|
|
2371
|
+
const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
|
|
2372
|
+
if (found) return found;
|
|
2373
|
+
continue;
|
|
2374
|
+
}
|
|
2375
|
+
const reExport = namedExports.find(
|
|
2376
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2377
|
+
);
|
|
2378
|
+
if (!reExport) continue;
|
|
2379
|
+
const sourceName = reExport.getName();
|
|
2380
|
+
const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
|
|
2381
|
+
if (viaImports) return viaImports;
|
|
2382
|
+
}
|
|
2383
|
+
return null;
|
|
2384
|
+
}
|
|
1784
2385
|
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
2386
|
+
function clearTypeResolutionCaches(project) {
|
|
2387
|
+
_findTypeCache.delete(project);
|
|
2388
|
+
_resolveNamedRefCache.delete(project);
|
|
2389
|
+
}
|
|
1785
2390
|
function findType(name, sourceFile, project) {
|
|
1786
2391
|
let byKey = _findTypeCache.get(project);
|
|
1787
2392
|
if (byKey === void 0) {
|
|
@@ -1922,9 +2527,11 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
1922
2527
|
warnings: [],
|
|
1923
2528
|
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
1924
2529
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
2530
|
+
usedSchemaNames: /* @__PURE__ */ new Set(),
|
|
1925
2531
|
visiting: /* @__PURE__ */ new Set(),
|
|
1926
2532
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
1927
|
-
depth: 0
|
|
2533
|
+
depth: 0,
|
|
2534
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
1928
2535
|
};
|
|
1929
2536
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
1930
2537
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -1948,11 +2555,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
1948
2555
|
const typeNode = prop.getTypeNode();
|
|
1949
2556
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
1950
2557
|
const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
|
|
2558
|
+
const discriminator = resolveDiscriminator(dec("Type"));
|
|
2559
|
+
if (discriminator) {
|
|
2560
|
+
const options = discriminator.subTypes.map(
|
|
2561
|
+
(name) => buildNestedReference(name, classFile, ctx)
|
|
2562
|
+
);
|
|
2563
|
+
const unionNode = {
|
|
2564
|
+
kind: "union",
|
|
2565
|
+
options,
|
|
2566
|
+
discriminator: discriminator.property
|
|
2567
|
+
};
|
|
2568
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2569
|
+
const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
|
|
2570
|
+
return applyPresence(node2, decorators);
|
|
2571
|
+
}
|
|
2572
|
+
const propTypeParam = singularClassName(typeText);
|
|
2573
|
+
if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
|
|
2574
|
+
const bound = ctx.typeBindings.get(propTypeParam);
|
|
2575
|
+
const childNode = buildNestedReference(bound, classFile, ctx);
|
|
2576
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2577
|
+
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
2578
|
+
return applyPresence(node2, decorators);
|
|
2579
|
+
}
|
|
1951
2580
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
1952
2581
|
if (has("ValidateNested") || typeRefName) {
|
|
2582
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
1953
2583
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
1954
2584
|
if (childName) {
|
|
1955
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
2585
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
1956
2586
|
const wrapArray = has("IsArray") || isArrayType;
|
|
1957
2587
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
1958
2588
|
return applyPresence(node2, decorators);
|
|
@@ -2077,10 +2707,13 @@ function baseFromType(typeText, isArrayType) {
|
|
|
2077
2707
|
return { kind: "unknown" };
|
|
2078
2708
|
}
|
|
2079
2709
|
}
|
|
2080
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2710
|
+
function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
|
|
2711
|
+
const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
|
|
2712
|
+
const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
|
|
2713
|
+
if (ctx.visiting.has(cacheKey)) {
|
|
2714
|
+
const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
|
|
2715
|
+
ctx.emittedClasses.set(cacheKey, reserved);
|
|
2716
|
+
ctx.usedSchemaNames.add(reserved);
|
|
2084
2717
|
ctx.recursiveSchemas.add(reserved);
|
|
2085
2718
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2086
2719
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -2099,29 +2732,37 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
2099
2732
|
}
|
|
2100
2733
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
2101
2734
|
}
|
|
2102
|
-
const existing = ctx.emittedClasses.get(
|
|
2735
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
2103
2736
|
if (existing) return { kind: "ref", name: existing };
|
|
2104
|
-
const schemaName = aliasFor(
|
|
2737
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
2105
2738
|
const resolved = findType(className, fromFile, ctx.project);
|
|
2106
2739
|
if (!resolved || resolved.kind !== "class") {
|
|
2107
2740
|
return { kind: "object", fields: [], passthrough: true };
|
|
2108
2741
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2742
|
+
const params = resolved.decl.getTypeParameters().map((p) => p.getName());
|
|
2743
|
+
const newBindings = [];
|
|
2744
|
+
params.forEach((param, i) => {
|
|
2745
|
+
const arg = typeArgs[i];
|
|
2746
|
+
if (arg) newBindings.push([param, arg]);
|
|
2747
|
+
});
|
|
2748
|
+
for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
|
|
2749
|
+
ctx.emittedClasses.set(cacheKey, schemaName);
|
|
2750
|
+
ctx.usedSchemaNames.add(schemaName);
|
|
2751
|
+
ctx.visiting.add(cacheKey);
|
|
2111
2752
|
ctx.depth += 1;
|
|
2112
2753
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
2113
2754
|
ctx.depth -= 1;
|
|
2114
|
-
ctx.visiting.delete(
|
|
2755
|
+
ctx.visiting.delete(cacheKey);
|
|
2756
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
2115
2757
|
ctx.named.set(schemaName, childNode);
|
|
2758
|
+
ctx.usedSchemaNames.add(schemaName);
|
|
2116
2759
|
return { kind: "ref", name: schemaName };
|
|
2117
2760
|
}
|
|
2118
2761
|
function aliasFor(className, ctx) {
|
|
2119
2762
|
const baseName = `${className}Schema`;
|
|
2120
2763
|
let candidate = baseName;
|
|
2121
2764
|
let i = 1;
|
|
2122
|
-
|
|
2123
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
2124
|
-
while (used.has(candidate)) {
|
|
2765
|
+
while (ctx.usedSchemaNames.has(candidate)) {
|
|
2125
2766
|
candidate = `${baseName}_${i}`;
|
|
2126
2767
|
i += 1;
|
|
2127
2768
|
}
|
|
@@ -2158,6 +2799,39 @@ function messageRaw(decorator) {
|
|
|
2158
2799
|
}
|
|
2159
2800
|
return void 0;
|
|
2160
2801
|
}
|
|
2802
|
+
function resolveDiscriminator(decorator) {
|
|
2803
|
+
const optsArg = decorator?.getArguments()[1];
|
|
2804
|
+
if (!optsArg || !Node3.isObjectLiteralExpression(optsArg)) return null;
|
|
2805
|
+
let discProp;
|
|
2806
|
+
for (const prop of optsArg.getProperties()) {
|
|
2807
|
+
if (Node3.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
|
|
2808
|
+
discProp = prop.getInitializer();
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
if (!discProp || !Node3.isObjectLiteralExpression(discProp)) return null;
|
|
2812
|
+
let property = null;
|
|
2813
|
+
const subTypes = [];
|
|
2814
|
+
for (const prop of discProp.getProperties()) {
|
|
2815
|
+
if (!Node3.isPropertyAssignment(prop)) continue;
|
|
2816
|
+
const name = prop.getName();
|
|
2817
|
+
const init = prop.getInitializer();
|
|
2818
|
+
if (!init) continue;
|
|
2819
|
+
if (name === "property" && Node3.isStringLiteral(init)) {
|
|
2820
|
+
property = init.getLiteralValue();
|
|
2821
|
+
} else if (name === "subTypes" && Node3.isArrayLiteralExpression(init)) {
|
|
2822
|
+
for (const el of init.getElements()) {
|
|
2823
|
+
if (!Node3.isObjectLiteralExpression(el)) continue;
|
|
2824
|
+
for (const p of el.getProperties()) {
|
|
2825
|
+
if (!Node3.isPropertyAssignment(p) || p.getName() !== "name") continue;
|
|
2826
|
+
const nameInit = p.getInitializer();
|
|
2827
|
+
if (nameInit && Node3.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
if (!property || subTypes.length === 0) return null;
|
|
2833
|
+
return { property, subTypes };
|
|
2834
|
+
}
|
|
2161
2835
|
function resolveTypeFactoryName(decorator) {
|
|
2162
2836
|
const arg = firstArg(decorator);
|
|
2163
2837
|
if (!arg) return null;
|
|
@@ -2171,6 +2845,17 @@ function singularClassName(typeText) {
|
|
|
2171
2845
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2172
2846
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2173
2847
|
}
|
|
2848
|
+
function genericTypeArgNames(typeNode) {
|
|
2849
|
+
if (!typeNode || !Node3.isTypeReference(typeNode)) return [];
|
|
2850
|
+
const names = [];
|
|
2851
|
+
for (const arg of typeNode.getTypeArguments()) {
|
|
2852
|
+
if (!Node3.isTypeReference(arg)) return [];
|
|
2853
|
+
const tn = arg.getTypeName();
|
|
2854
|
+
if (!Node3.isIdentifier(tn)) return [];
|
|
2855
|
+
names.push(tn.getText());
|
|
2856
|
+
}
|
|
2857
|
+
return names;
|
|
2858
|
+
}
|
|
2174
2859
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
2175
2860
|
const arg = firstArg(decorator);
|
|
2176
2861
|
if (!arg) return null;
|
|
@@ -2230,6 +2915,9 @@ import {
|
|
|
2230
2915
|
|
|
2231
2916
|
// src/discovery/enum-resolution.ts
|
|
2232
2917
|
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
2918
|
+
function clearEnumCache(project) {
|
|
2919
|
+
_enumCache.delete(project);
|
|
2920
|
+
}
|
|
2233
2921
|
function resolveEnumValues(name, sourceFile, project) {
|
|
2234
2922
|
let byKey = _enumCache.get(project);
|
|
2235
2923
|
if (byKey === void 0) {
|
|
@@ -2698,24 +3386,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
2698
3386
|
"Map",
|
|
2699
3387
|
"Set"
|
|
2700
3388
|
]);
|
|
2701
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3389
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2702
3390
|
if (depth <= 0) return "unknown";
|
|
2703
3391
|
if (Node6.isArrayTypeNode(typeNode)) {
|
|
2704
3392
|
const elementType = typeNode.getElementTypeNode();
|
|
2705
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3393
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
2706
3394
|
}
|
|
2707
3395
|
if (Node6.isUnionTypeNode(typeNode)) {
|
|
2708
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3396
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
2709
3397
|
}
|
|
2710
3398
|
if (Node6.isIntersectionTypeNode(typeNode)) {
|
|
2711
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3399
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
2712
3400
|
}
|
|
2713
3401
|
if (Node6.isParenthesizedTypeNode(typeNode)) {
|
|
2714
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3402
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
2715
3403
|
}
|
|
2716
3404
|
if (Node6.isTypeReference(typeNode)) {
|
|
2717
3405
|
const typeName = typeNode.getTypeName();
|
|
2718
3406
|
const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3407
|
+
const bound = subst.get(name);
|
|
3408
|
+
if (bound !== void 0) return bound;
|
|
2719
3409
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
2720
3410
|
if (name === "Date") return "string";
|
|
2721
3411
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -2723,14 +3413,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2723
3413
|
return "unknown";
|
|
2724
3414
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
2725
3415
|
if (wrapperMode) {
|
|
2726
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3416
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
2727
3417
|
}
|
|
2728
3418
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
2729
3419
|
return typeNode.getText();
|
|
2730
3420
|
}
|
|
2731
3421
|
const resolved = findType(name, sourceFile, project);
|
|
2732
3422
|
if (resolved) {
|
|
2733
|
-
|
|
3423
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
3424
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
2734
3425
|
}
|
|
2735
3426
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
2736
3427
|
return "unknown";
|
|
@@ -2743,32 +3434,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2743
3434
|
if (kind === SyntaxKind3.AnyKeyword) return "unknown";
|
|
2744
3435
|
return typeNode.getText();
|
|
2745
3436
|
}
|
|
2746
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
3437
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
2747
3438
|
const typeArgs = typeNode.getTypeArguments();
|
|
2748
3439
|
const firstTypeArg = typeArgs[0];
|
|
2749
3440
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2750
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3441
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
2751
3442
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2752
3443
|
}
|
|
2753
3444
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2754
3445
|
}
|
|
2755
|
-
function
|
|
3446
|
+
function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
|
|
3447
|
+
if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
|
|
3448
|
+
const params = result.decl.getTypeParameters().map((p) => p.getName());
|
|
3449
|
+
if (params.length === 0) return /* @__PURE__ */ new Map();
|
|
3450
|
+
const args = typeNode.getTypeArguments();
|
|
3451
|
+
const subst = /* @__PURE__ */ new Map();
|
|
3452
|
+
params.forEach((param, i) => {
|
|
3453
|
+
const arg = args[i];
|
|
3454
|
+
if (arg)
|
|
3455
|
+
subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
|
|
3456
|
+
});
|
|
3457
|
+
return subst;
|
|
3458
|
+
}
|
|
3459
|
+
function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2756
3460
|
if (depth < 0) return "unknown";
|
|
2757
3461
|
switch (result.kind) {
|
|
2758
3462
|
case "class":
|
|
2759
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3463
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2760
3464
|
case "interface":
|
|
2761
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3465
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2762
3466
|
case "typeAlias":
|
|
2763
3467
|
if (result.typeNode) {
|
|
2764
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
3468
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
2765
3469
|
}
|
|
2766
3470
|
return result.text;
|
|
2767
3471
|
case "enum":
|
|
2768
3472
|
return result.members.join(" | ");
|
|
2769
3473
|
}
|
|
2770
3474
|
}
|
|
2771
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
3475
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2772
3476
|
if (depth < 0) return "unknown";
|
|
2773
3477
|
const lines = [];
|
|
2774
3478
|
for (const prop of decl.getProperties()) {
|
|
@@ -2777,7 +3481,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
2777
3481
|
const propTypeNode = prop.getTypeNode();
|
|
2778
3482
|
let propType = "unknown";
|
|
2779
3483
|
if (propTypeNode) {
|
|
2780
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
3484
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
2781
3485
|
}
|
|
2782
3486
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
2783
3487
|
}
|
|
@@ -2826,7 +3530,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
2826
3530
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
2827
3531
|
}
|
|
2828
3532
|
function extractResponseType(method, sourceFile, project) {
|
|
2829
|
-
const apiResponseDecorator = method.
|
|
3533
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2830
3534
|
if (apiResponseDecorator) {
|
|
2831
3535
|
const args = apiResponseDecorator.getArguments();
|
|
2832
3536
|
const optsArg = args[0];
|
|
@@ -2855,6 +3559,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
2855
3559
|
}
|
|
2856
3560
|
return "unknown";
|
|
2857
3561
|
}
|
|
3562
|
+
function apiResponseStatus(decorator) {
|
|
3563
|
+
const optsArg = decorator.getArguments()[0];
|
|
3564
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
3565
|
+
for (const prop of optsArg.getProperties()) {
|
|
3566
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3567
|
+
if (prop.getName() !== "status") continue;
|
|
3568
|
+
const val = prop.getInitializer();
|
|
3569
|
+
if (val && Node6.isNumericLiteral(val)) return Number(val.getLiteralValue());
|
|
3570
|
+
}
|
|
3571
|
+
return null;
|
|
3572
|
+
}
|
|
3573
|
+
function apiResponseTypeNode(decorator) {
|
|
3574
|
+
const optsArg = decorator.getArguments()[0];
|
|
3575
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
3576
|
+
for (const prop of optsArg.getProperties()) {
|
|
3577
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3578
|
+
if (prop.getName() !== "type") continue;
|
|
3579
|
+
const val = prop.getInitializer();
|
|
3580
|
+
if (!val) return null;
|
|
3581
|
+
if (Node6.isArrayLiteralExpression(val)) {
|
|
3582
|
+
const first = val.getElements()[0];
|
|
3583
|
+
return first ? { node: first, isArray: true } : null;
|
|
3584
|
+
}
|
|
3585
|
+
return { node: val, isArray: false };
|
|
3586
|
+
}
|
|
3587
|
+
return null;
|
|
3588
|
+
}
|
|
3589
|
+
function extractErrorType(method, sourceFile, project) {
|
|
3590
|
+
for (const decorator of method.getDecorators()) {
|
|
3591
|
+
if (decorator.getName() !== "ApiResponse") continue;
|
|
3592
|
+
const status = apiResponseStatus(decorator);
|
|
3593
|
+
if (status === null || status < 400) continue;
|
|
3594
|
+
const typeInfo = apiResponseTypeNode(decorator);
|
|
3595
|
+
if (!typeInfo) continue;
|
|
3596
|
+
const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
|
|
3597
|
+
const type = typeInfo.isArray ? `Array<${inner}>` : inner;
|
|
3598
|
+
let ref = null;
|
|
3599
|
+
if (Node6.isIdentifier(typeInfo.node)) {
|
|
3600
|
+
const name = typeInfo.node.getText();
|
|
3601
|
+
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3602
|
+
if (localDecl?.isExported()) {
|
|
3603
|
+
ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
|
|
3604
|
+
} else {
|
|
3605
|
+
const resolved = resolveImportedType(name, sourceFile, project);
|
|
3606
|
+
if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
|
|
3607
|
+
ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
return { type, ref };
|
|
3612
|
+
}
|
|
3613
|
+
return null;
|
|
3614
|
+
}
|
|
2858
3615
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
2859
3616
|
if (!Node6.isIdentifier(node)) return "unknown";
|
|
2860
3617
|
const name = node.getText();
|
|
@@ -2870,17 +3627,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
2870
3627
|
unwrapContainers: true
|
|
2871
3628
|
});
|
|
2872
3629
|
}
|
|
3630
|
+
var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
|
|
3631
|
+
var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
|
|
3632
|
+
var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
|
|
3633
|
+
function detectStreamElement(method) {
|
|
3634
|
+
const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
|
|
3635
|
+
let node = method.getReturnTypeNode();
|
|
3636
|
+
node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
|
|
3637
|
+
const containerEl = streamContainerElement(node);
|
|
3638
|
+
if (containerEl) {
|
|
3639
|
+
return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
|
|
3640
|
+
}
|
|
3641
|
+
if (hasSse) return node ?? null;
|
|
3642
|
+
return null;
|
|
3643
|
+
}
|
|
3644
|
+
function streamContainerElement(node) {
|
|
3645
|
+
if (!node || !Node6.isTypeReference(node)) return null;
|
|
3646
|
+
const typeName = node.getTypeName();
|
|
3647
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3648
|
+
if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
|
|
3649
|
+
return node.getTypeArguments()[0] ?? null;
|
|
3650
|
+
}
|
|
3651
|
+
return null;
|
|
3652
|
+
}
|
|
3653
|
+
function unwrapNamedContainer(node, names) {
|
|
3654
|
+
if (!node || !Node6.isTypeReference(node)) return node;
|
|
3655
|
+
const typeName = node.getTypeName();
|
|
3656
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3657
|
+
if (names.has(name)) {
|
|
3658
|
+
return node.getTypeArguments()[0] ?? node;
|
|
3659
|
+
}
|
|
3660
|
+
return node;
|
|
3661
|
+
}
|
|
2873
3662
|
function extractDtoContract(method, sourceFile, project) {
|
|
2874
3663
|
let body = extractBodyType(method, sourceFile, project);
|
|
2875
3664
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
2876
3665
|
const query = extractQueryType(method, sourceFile, project);
|
|
3666
|
+
const streamElement = detectStreamElement(method);
|
|
3667
|
+
const isStream = streamElement !== null;
|
|
2877
3668
|
if (filterInfo && filterInfo.source === "body") {
|
|
2878
3669
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
2879
3670
|
body = body ?? bodyType;
|
|
2880
3671
|
}
|
|
2881
3672
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
2882
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
2883
|
-
|
|
3673
|
+
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
3674
|
+
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
3675
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
2884
3676
|
return null;
|
|
2885
3677
|
}
|
|
2886
3678
|
let bodyRef = null;
|
|
@@ -2894,12 +3686,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2894
3686
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
2895
3687
|
}
|
|
2896
3688
|
}
|
|
2897
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
3689
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
2898
3690
|
if (returnTypeNode) {
|
|
2899
3691
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
2900
3692
|
}
|
|
2901
|
-
if (!responseRef) {
|
|
2902
|
-
const apiResp = method.
|
|
3693
|
+
if (!responseRef && !isStream) {
|
|
3694
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2903
3695
|
if (apiResp) {
|
|
2904
3696
|
const args = apiResp.getArguments();
|
|
2905
3697
|
const optsArg = args[0];
|
|
@@ -2941,16 +3733,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2941
3733
|
query,
|
|
2942
3734
|
body,
|
|
2943
3735
|
response,
|
|
3736
|
+
error: errorInfo?.type ?? null,
|
|
2944
3737
|
params: paramsType,
|
|
2945
3738
|
queryRef,
|
|
2946
3739
|
bodyRef,
|
|
2947
3740
|
responseRef,
|
|
3741
|
+
errorRef: errorInfo?.ref ?? null,
|
|
2948
3742
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2949
3743
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2950
3744
|
filterSource: filterInfo?.source ?? null,
|
|
2951
3745
|
formWarnings,
|
|
2952
3746
|
bodySchema,
|
|
2953
|
-
querySchema
|
|
3747
|
+
querySchema,
|
|
3748
|
+
stream: isStream
|
|
2954
3749
|
};
|
|
2955
3750
|
}
|
|
2956
3751
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -3068,6 +3863,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
3068
3863
|
let query = null;
|
|
3069
3864
|
let body = null;
|
|
3070
3865
|
let response = "unknown";
|
|
3866
|
+
let error = null;
|
|
3071
3867
|
let bodyZodText = null;
|
|
3072
3868
|
let queryZodText = null;
|
|
3073
3869
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -3083,25 +3879,38 @@ function parseDefineContractCall(callExpr) {
|
|
|
3083
3879
|
bodyZodText = val.getText();
|
|
3084
3880
|
} else if (propName === "response") {
|
|
3085
3881
|
response = zodAstToTs(val);
|
|
3882
|
+
} else if (propName === "error") {
|
|
3883
|
+
error = zodAstToTs(val);
|
|
3086
3884
|
}
|
|
3087
3885
|
}
|
|
3088
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3886
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
3089
3887
|
}
|
|
3090
3888
|
|
|
3091
3889
|
// src/discovery/contracts-fast.ts
|
|
3092
3890
|
async function discoverContractsFast(opts) {
|
|
3093
3891
|
const { cwd, glob, tsconfig } = opts;
|
|
3094
|
-
const tsconfigPath =
|
|
3095
|
-
|
|
3892
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3893
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3894
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3895
|
+
for (const f of files) {
|
|
3896
|
+
project.addSourceFileAtPath(f);
|
|
3897
|
+
}
|
|
3898
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3899
|
+
return extractAllRoutes(project);
|
|
3900
|
+
}
|
|
3901
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
3902
|
+
return tsconfig ? resolve3(tsconfig) : join13(cwd, "tsconfig.json");
|
|
3903
|
+
}
|
|
3904
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
3096
3905
|
try {
|
|
3097
|
-
|
|
3906
|
+
return new Project3({
|
|
3098
3907
|
tsConfigFilePath: tsconfigPath,
|
|
3099
3908
|
skipAddingFilesFromTsConfig: true,
|
|
3100
3909
|
skipLoadingLibFiles: true,
|
|
3101
3910
|
skipFileDependencyResolution: true
|
|
3102
3911
|
});
|
|
3103
3912
|
} catch {
|
|
3104
|
-
|
|
3913
|
+
return new Project3({
|
|
3105
3914
|
skipAddingFilesFromTsConfig: true,
|
|
3106
3915
|
skipLoadingLibFiles: true,
|
|
3107
3916
|
skipFileDependencyResolution: true,
|
|
@@ -3112,20 +3921,105 @@ async function discoverContractsFast(opts) {
|
|
|
3112
3921
|
}
|
|
3113
3922
|
});
|
|
3114
3923
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
project.addSourceFileAtPath(f);
|
|
3118
|
-
}
|
|
3119
|
-
const routes = [];
|
|
3924
|
+
}
|
|
3925
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
3120
3926
|
setDiscoveryContext(project, {
|
|
3121
3927
|
projectRoot: cwd,
|
|
3122
3928
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3123
3929
|
});
|
|
3930
|
+
}
|
|
3931
|
+
function extractRoutesFrom(project, controllerPaths) {
|
|
3932
|
+
const routes = [];
|
|
3933
|
+
for (const path of controllerPaths) {
|
|
3934
|
+
const sourceFile = project.getSourceFile(path);
|
|
3935
|
+
if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3936
|
+
}
|
|
3937
|
+
return routes;
|
|
3938
|
+
}
|
|
3939
|
+
function extractAllRoutes(project) {
|
|
3940
|
+
const routes = [];
|
|
3124
3941
|
for (const sourceFile of project.getSourceFiles()) {
|
|
3125
3942
|
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3126
3943
|
}
|
|
3127
3944
|
return routes;
|
|
3128
3945
|
}
|
|
3946
|
+
var PersistentDiscovery = class _PersistentDiscovery {
|
|
3947
|
+
project;
|
|
3948
|
+
cwd;
|
|
3949
|
+
glob;
|
|
3950
|
+
/** Absolute paths of the controllers currently loaded as extraction roots. */
|
|
3951
|
+
controllerPaths = /* @__PURE__ */ new Set();
|
|
3952
|
+
constructor(project, cwd, glob) {
|
|
3953
|
+
this.project = project;
|
|
3954
|
+
this.cwd = cwd;
|
|
3955
|
+
this.glob = glob;
|
|
3956
|
+
}
|
|
3957
|
+
/**
|
|
3958
|
+
* Build the initial persistent Project: create it, glob + add all controllers,
|
|
3959
|
+
* bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
|
|
3960
|
+
*/
|
|
3961
|
+
static async create(opts) {
|
|
3962
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3963
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3964
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3965
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3966
|
+
const instance = new _PersistentDiscovery(project, cwd, glob);
|
|
3967
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3968
|
+
for (const f of files) {
|
|
3969
|
+
project.addSourceFileAtPath(f);
|
|
3970
|
+
instance.controllerPaths.add(f);
|
|
3971
|
+
}
|
|
3972
|
+
return instance;
|
|
3973
|
+
}
|
|
3974
|
+
/** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
|
|
3975
|
+
discover() {
|
|
3976
|
+
return this.runExtraction();
|
|
3977
|
+
}
|
|
3978
|
+
/**
|
|
3979
|
+
* Re-discover after one or more files changed. Refreshes the changed file(s)
|
|
3980
|
+
* from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
|
|
3981
|
+
* to pick up added/removed controllers, clears the per-Project caches, then
|
|
3982
|
+
* re-extracts. `changedPaths` is a hint; correctness does not depend on it
|
|
3983
|
+
* being exhaustive because re-globbing + refresh-on-presence covers the set.
|
|
3984
|
+
*/
|
|
3985
|
+
async rediscover(changedPaths) {
|
|
3986
|
+
if (changedPaths) {
|
|
3987
|
+
for (const p of changedPaths) {
|
|
3988
|
+
const abs = resolve3(p);
|
|
3989
|
+
const sf = this.project.getSourceFile(abs);
|
|
3990
|
+
if (sf) {
|
|
3991
|
+
await sf.refreshFromFileSystem();
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
const globbed = new Set(
|
|
3996
|
+
await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
|
|
3997
|
+
);
|
|
3998
|
+
for (const f of globbed) {
|
|
3999
|
+
if (!this.controllerPaths.has(f)) {
|
|
4000
|
+
try {
|
|
4001
|
+
this.project.addSourceFileAtPath(f);
|
|
4002
|
+
this.controllerPaths.add(f);
|
|
4003
|
+
} catch {
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
for (const f of this.controllerPaths) {
|
|
4008
|
+
if (!globbed.has(f)) {
|
|
4009
|
+
const sf = this.project.getSourceFile(f);
|
|
4010
|
+
if (sf) this.project.removeSourceFile(sf);
|
|
4011
|
+
this.controllerPaths.delete(f);
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
return this.runExtraction();
|
|
4015
|
+
}
|
|
4016
|
+
/** Clear stale per-Project caches, then extract over the controller set. */
|
|
4017
|
+
runExtraction() {
|
|
4018
|
+
clearTypeResolutionCaches(this.project);
|
|
4019
|
+
clearEnumCache(this.project);
|
|
4020
|
+
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
4021
|
+
}
|
|
4022
|
+
};
|
|
3129
4023
|
function decoratorStringArg(decoratorExpr) {
|
|
3130
4024
|
if (!decoratorExpr) return void 0;
|
|
3131
4025
|
if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -3181,6 +4075,11 @@ function resolveVerb(method) {
|
|
|
3181
4075
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3182
4076
|
}
|
|
3183
4077
|
}
|
|
4078
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
4079
|
+
if (sseDecorator) {
|
|
4080
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
4081
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
4082
|
+
}
|
|
3184
4083
|
return null;
|
|
3185
4084
|
}
|
|
3186
4085
|
function readAsDecorator(node, label) {
|
|
@@ -3223,7 +4122,17 @@ function buildRoute(args) {
|
|
|
3223
4122
|
};
|
|
3224
4123
|
}
|
|
3225
4124
|
function extractContractRoute(args) {
|
|
3226
|
-
const {
|
|
4125
|
+
const {
|
|
4126
|
+
cls,
|
|
4127
|
+
method,
|
|
4128
|
+
applyContractDecorator,
|
|
4129
|
+
verb,
|
|
4130
|
+
prefix,
|
|
4131
|
+
className,
|
|
4132
|
+
sourceFile,
|
|
4133
|
+
project,
|
|
4134
|
+
seenNames
|
|
4135
|
+
} = args;
|
|
3227
4136
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3228
4137
|
if (!firstDecoratorArg) return null;
|
|
3229
4138
|
let contractDef = null;
|
|
@@ -3233,18 +4142,19 @@ function extractContractRoute(args) {
|
|
|
3233
4142
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3234
4143
|
} else if (Node8.isIdentifier(firstDecoratorArg)) {
|
|
3235
4144
|
const identName = firstDecoratorArg.getText();
|
|
3236
|
-
const
|
|
3237
|
-
if (!
|
|
4145
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
4146
|
+
if (!resolvedVar) {
|
|
3238
4147
|
console.warn(
|
|
3239
|
-
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()}
|
|
4148
|
+
`[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
|
|
3240
4149
|
);
|
|
3241
4150
|
return null;
|
|
3242
4151
|
}
|
|
4152
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
3243
4153
|
const initializer = varDecl.getInitializer();
|
|
3244
4154
|
if (!initializer) return null;
|
|
3245
4155
|
contractDef = parseDefineContractCall(initializer);
|
|
3246
4156
|
if (contractDef && varDecl.isExported()) {
|
|
3247
|
-
const filePath =
|
|
4157
|
+
const filePath = declFile.getFilePath();
|
|
3248
4158
|
if (contractDef.body !== null) {
|
|
3249
4159
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3250
4160
|
}
|
|
@@ -3277,6 +4187,7 @@ function extractContractRoute(args) {
|
|
|
3277
4187
|
query: contractDef.query,
|
|
3278
4188
|
body: contractDef.body,
|
|
3279
4189
|
response: contractDef.response,
|
|
4190
|
+
error: contractDef.error,
|
|
3280
4191
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3281
4192
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3282
4193
|
// would drag server-only deps into the client bundle).
|
|
@@ -3308,15 +4219,18 @@ function extractDtoRoute(args) {
|
|
|
3308
4219
|
query: dtoContract?.query ?? null,
|
|
3309
4220
|
body: dtoContract?.body ?? null,
|
|
3310
4221
|
response: dtoContract?.response ?? "unknown",
|
|
4222
|
+
error: dtoContract?.error ?? null,
|
|
3311
4223
|
queryRef: dtoContract?.queryRef ?? null,
|
|
3312
4224
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3313
4225
|
responseRef: dtoContract?.responseRef ?? null,
|
|
4226
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
3314
4227
|
filterFields: dtoContract?.filterFields ?? null,
|
|
3315
4228
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3316
4229
|
filterSource: dtoContract?.filterSource ?? null,
|
|
3317
4230
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3318
4231
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3319
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
4232
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
4233
|
+
stream: dtoContract?.stream ?? false
|
|
3320
4234
|
}
|
|
3321
4235
|
});
|
|
3322
4236
|
}
|
|
@@ -3340,6 +4254,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3340
4254
|
prefix,
|
|
3341
4255
|
className,
|
|
3342
4256
|
sourceFile,
|
|
4257
|
+
project,
|
|
3343
4258
|
seenNames
|
|
3344
4259
|
}) : extractDtoRoute({
|
|
3345
4260
|
cls,
|
|
@@ -3359,8 +4274,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3359
4274
|
|
|
3360
4275
|
// src/watch/lock-file.ts
|
|
3361
4276
|
import { open } from "fs/promises";
|
|
3362
|
-
import { mkdir as
|
|
3363
|
-
import { join as
|
|
4277
|
+
import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
|
|
4278
|
+
import { join as join14 } from "path";
|
|
3364
4279
|
var LOCK_FILE = ".watcher.lock";
|
|
3365
4280
|
function isProcessAlive(pid) {
|
|
3366
4281
|
try {
|
|
@@ -3371,8 +4286,8 @@ function isProcessAlive(pid) {
|
|
|
3371
4286
|
}
|
|
3372
4287
|
}
|
|
3373
4288
|
async function acquireLock(outDir) {
|
|
3374
|
-
await
|
|
3375
|
-
const lockPath =
|
|
4289
|
+
await mkdir10(outDir, { recursive: true });
|
|
4290
|
+
const lockPath = join14(outDir, LOCK_FILE);
|
|
3376
4291
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3377
4292
|
try {
|
|
3378
4293
|
const fd = await open(lockPath, "wx");
|
|
@@ -3412,7 +4327,7 @@ async function watch(config, onChange) {
|
|
|
3412
4327
|
if (lock === null) {
|
|
3413
4328
|
let holderPid = "unknown";
|
|
3414
4329
|
try {
|
|
3415
|
-
const raw = await readFile3(
|
|
4330
|
+
const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3416
4331
|
const data = JSON.parse(raw);
|
|
3417
4332
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3418
4333
|
} catch {
|
|
@@ -3422,12 +4337,20 @@ async function watch(config, onChange) {
|
|
|
3422
4337
|
);
|
|
3423
4338
|
return NO_OP_WATCHER;
|
|
3424
4339
|
}
|
|
4340
|
+
let discovery = null;
|
|
4341
|
+
async function getDiscovery() {
|
|
4342
|
+
if (discovery === null) {
|
|
4343
|
+
discovery = await PersistentDiscovery.create({
|
|
4344
|
+
cwd: config.codegen.cwd,
|
|
4345
|
+
glob: config.contracts.glob,
|
|
4346
|
+
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
4347
|
+
});
|
|
4348
|
+
return discovery;
|
|
4349
|
+
}
|
|
4350
|
+
return discovery;
|
|
4351
|
+
}
|
|
3425
4352
|
try {
|
|
3426
|
-
const initialRoutes = await
|
|
3427
|
-
cwd: config.codegen.cwd,
|
|
3428
|
-
glob: config.contracts.glob,
|
|
3429
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3430
|
-
});
|
|
4353
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3431
4354
|
await generate(config, initialRoutes);
|
|
3432
4355
|
} catch (err) {
|
|
3433
4356
|
console.warn(
|
|
@@ -3440,7 +4363,7 @@ async function watch(config, onChange) {
|
|
|
3440
4363
|
}
|
|
3441
4364
|
let pagesDebounceTimer;
|
|
3442
4365
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3443
|
-
const pagesWatcher = chokidar.watch(
|
|
4366
|
+
const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
|
|
3444
4367
|
ignoreInitial: true,
|
|
3445
4368
|
persistent: true,
|
|
3446
4369
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3466,23 +4389,23 @@ async function watch(config, onChange) {
|
|
|
3466
4389
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3467
4390
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3468
4391
|
let contractsDebounceTimer;
|
|
3469
|
-
const
|
|
4392
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4393
|
+
const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
|
|
3470
4394
|
ignoreInitial: true,
|
|
3471
4395
|
persistent: true,
|
|
3472
4396
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3473
4397
|
});
|
|
3474
|
-
function scheduleContractsRegenerate() {
|
|
4398
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4399
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3475
4400
|
if (contractsDebounceTimer !== void 0) {
|
|
3476
4401
|
clearTimeout(contractsDebounceTimer);
|
|
3477
4402
|
}
|
|
3478
4403
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3479
4404
|
contractsDebounceTimer = void 0;
|
|
4405
|
+
const changed = [...pendingChangedPaths];
|
|
4406
|
+
pendingChangedPaths.clear();
|
|
3480
4407
|
try {
|
|
3481
|
-
const routes = await
|
|
3482
|
-
cwd: config.codegen.cwd,
|
|
3483
|
-
glob: config.contracts.glob,
|
|
3484
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3485
|
-
});
|
|
4408
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3486
4409
|
await generate(config, routes);
|
|
3487
4410
|
} catch (err) {
|
|
3488
4411
|
console.error(
|
|
@@ -3493,17 +4416,17 @@ async function watch(config, onChange) {
|
|
|
3493
4416
|
onChange?.();
|
|
3494
4417
|
}, config.contracts.debounceMs);
|
|
3495
4418
|
}
|
|
3496
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3497
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3498
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3499
|
-
const formsWatcher = chokidar.watch(
|
|
4419
|
+
contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4420
|
+
contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4421
|
+
contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
4422
|
+
const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
|
|
3500
4423
|
ignoreInitial: true,
|
|
3501
4424
|
persistent: true,
|
|
3502
4425
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3503
4426
|
});
|
|
3504
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3505
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3506
|
-
formsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
4427
|
+
formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4428
|
+
formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4429
|
+
formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
3507
4430
|
return {
|
|
3508
4431
|
close: async () => {
|
|
3509
4432
|
if (pagesDebounceTimer !== void 0) {
|
|
@@ -3572,16 +4495,20 @@ function renderTsType(node, ctx) {
|
|
|
3572
4495
|
}
|
|
3573
4496
|
|
|
3574
4497
|
// src/index.ts
|
|
3575
|
-
var VERSION = "0.
|
|
4498
|
+
var VERSION = "0.5.1";
|
|
3576
4499
|
export {
|
|
3577
4500
|
CodegenError,
|
|
3578
4501
|
ConfigError,
|
|
3579
4502
|
VERSION,
|
|
3580
4503
|
acquireLock,
|
|
4504
|
+
buildMocksFile,
|
|
4505
|
+
buildOpenApiSpec,
|
|
3581
4506
|
defineConfig,
|
|
3582
4507
|
discoverContractsFast,
|
|
3583
4508
|
emitApi,
|
|
3584
4509
|
emitForms,
|
|
4510
|
+
emitMocks,
|
|
4511
|
+
emitOpenApi,
|
|
3585
4512
|
emitRoutes,
|
|
3586
4513
|
extractSchemaFromDto,
|
|
3587
4514
|
generate,
|
|
@@ -3589,6 +4516,8 @@ export {
|
|
|
3589
4516
|
renderTsType,
|
|
3590
4517
|
resolveAdapter,
|
|
3591
4518
|
resolveConfig,
|
|
4519
|
+
schemaModuleToJsonSchema,
|
|
4520
|
+
schemaNodeToJsonSchema,
|
|
3592
4521
|
watch
|
|
3593
4522
|
};
|
|
3594
4523
|
//# sourceMappingURL=index.js.map
|