@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.cjs
CHANGED
|
@@ -34,10 +34,14 @@ __export(src_exports, {
|
|
|
34
34
|
ConfigError: () => ConfigError,
|
|
35
35
|
VERSION: () => VERSION,
|
|
36
36
|
acquireLock: () => acquireLock,
|
|
37
|
+
buildMocksFile: () => buildMocksFile,
|
|
38
|
+
buildOpenApiSpec: () => buildOpenApiSpec,
|
|
37
39
|
defineConfig: () => defineConfig,
|
|
38
40
|
discoverContractsFast: () => discoverContractsFast,
|
|
39
41
|
emitApi: () => emitApi,
|
|
40
42
|
emitForms: () => emitForms,
|
|
43
|
+
emitMocks: () => emitMocks,
|
|
44
|
+
emitOpenApi: () => emitOpenApi,
|
|
41
45
|
emitRoutes: () => emitRoutes,
|
|
42
46
|
extractSchemaFromDto: () => extractSchemaFromDto,
|
|
43
47
|
generate: () => generate,
|
|
@@ -45,6 +49,8 @@ __export(src_exports, {
|
|
|
45
49
|
renderTsType: () => renderTsType,
|
|
46
50
|
resolveAdapter: () => resolveAdapter,
|
|
47
51
|
resolveConfig: () => resolveConfig,
|
|
52
|
+
schemaModuleToJsonSchema: () => schemaModuleToJsonSchema,
|
|
53
|
+
schemaNodeToJsonSchema: () => schemaNodeToJsonSchema,
|
|
48
54
|
watch: () => watch
|
|
49
55
|
});
|
|
50
56
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -182,6 +188,19 @@ function applyDefaults(userConfig, cwd) {
|
|
|
182
188
|
enabled: userConfig.forms?.enabled ?? true,
|
|
183
189
|
watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
|
|
184
190
|
zodImport: userConfig.forms?.zodImport ?? "zod"
|
|
191
|
+
},
|
|
192
|
+
openapi: {
|
|
193
|
+
enabled: userConfig.openapi?.enabled ?? false,
|
|
194
|
+
fileName: userConfig.openapi?.fileName ?? "openapi.json",
|
|
195
|
+
title: userConfig.openapi?.title ?? "NestJS API",
|
|
196
|
+
version: userConfig.openapi?.version ?? "1.0.0",
|
|
197
|
+
description: userConfig.openapi?.description ?? null
|
|
198
|
+
},
|
|
199
|
+
mocks: {
|
|
200
|
+
enabled: userConfig.mocks?.enabled ?? false,
|
|
201
|
+
fileName: userConfig.mocks?.fileName ?? "mocks.ts",
|
|
202
|
+
seed: userConfig.mocks?.seed ?? 1,
|
|
203
|
+
baseUrl: userConfig.mocks?.baseUrl ?? ""
|
|
185
204
|
}
|
|
186
205
|
};
|
|
187
206
|
}
|
|
@@ -219,8 +238,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
219
238
|
}
|
|
220
239
|
|
|
221
240
|
// src/generate.ts
|
|
222
|
-
var
|
|
223
|
-
var
|
|
241
|
+
var import_promises11 = require("fs/promises");
|
|
242
|
+
var import_node_path12 = require("path");
|
|
224
243
|
|
|
225
244
|
// src/discovery/pages.ts
|
|
226
245
|
var import_promises2 = require("fs/promises");
|
|
@@ -749,17 +768,28 @@ function emitFilterQueryType(c) {
|
|
|
749
768
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
750
769
|
}
|
|
751
770
|
function buildResponseType(c, outDir) {
|
|
771
|
+
const respRef = c.contractSource.responseRef;
|
|
772
|
+
if (c.contractSource.stream) {
|
|
773
|
+
if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
774
|
+
return c.contractSource.response;
|
|
775
|
+
}
|
|
752
776
|
if (c.controllerRef) {
|
|
753
777
|
let relPath = (0, import_node_path4.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
754
778
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
755
779
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
756
780
|
}
|
|
757
|
-
const respRef = c.contractSource.responseRef;
|
|
758
781
|
if (respRef) {
|
|
759
782
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
760
783
|
}
|
|
761
784
|
return c.contractSource.response;
|
|
762
785
|
}
|
|
786
|
+
function buildErrorType(c) {
|
|
787
|
+
const errRef = c.contractSource.errorRef;
|
|
788
|
+
if (errRef) {
|
|
789
|
+
return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
|
|
790
|
+
}
|
|
791
|
+
return c.contractSource.error ?? "unknown";
|
|
792
|
+
}
|
|
763
793
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
764
794
|
const pad = " ".repeat(indent);
|
|
765
795
|
const lines = [];
|
|
@@ -774,12 +804,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
774
804
|
const bodyRef = c.contractSource.bodyRef;
|
|
775
805
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
776
806
|
const response = buildResponseType(c, outDir);
|
|
807
|
+
const error = buildErrorType(c);
|
|
777
808
|
const params = buildParamsType(c.params);
|
|
778
809
|
const safeMethod = JSON.stringify(method);
|
|
779
810
|
const safeUrl = JSON.stringify(c.path);
|
|
780
811
|
const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
|
|
812
|
+
const stream = c.contractSource.stream ? "true" : "false";
|
|
781
813
|
lines.push(
|
|
782
|
-
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
|
|
814
|
+
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
|
|
783
815
|
);
|
|
784
816
|
} else {
|
|
785
817
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -851,15 +883,21 @@ function emitReqHelper() {
|
|
|
851
883
|
""
|
|
852
884
|
];
|
|
853
885
|
}
|
|
854
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
886
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
855
887
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
856
888
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
889
|
+
if (streamExpr) {
|
|
890
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
891
|
+
}
|
|
857
892
|
for (const [name, value] of Object.entries(members)) {
|
|
858
893
|
lines.push(`${pad} ${name}: ${value},`);
|
|
859
894
|
}
|
|
860
895
|
lines.push(`${pad}}),`);
|
|
861
896
|
return lines;
|
|
862
897
|
}
|
|
898
|
+
function renderStreamExpr(req) {
|
|
899
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
900
|
+
}
|
|
863
901
|
function emitApiObjectBlock(tree, indent, p) {
|
|
864
902
|
const pad = " ".repeat(indent);
|
|
865
903
|
const lines = [];
|
|
@@ -894,7 +932,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
894
932
|
}
|
|
895
933
|
const members = {};
|
|
896
934
|
for (const [name, { value }] of owned) members[name] = value;
|
|
897
|
-
|
|
935
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
936
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
898
937
|
}
|
|
899
938
|
return lines;
|
|
900
939
|
}
|
|
@@ -932,6 +971,8 @@ var ROUTE_NAMESPACE = [
|
|
|
932
971
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
933
972
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
934
973
|
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
974
|
+
" /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
|
|
975
|
+
' export type Stream<K extends string> = ResolveByName<K, "response">;',
|
|
935
976
|
" export type Request<K extends string> = {",
|
|
936
977
|
" body: Body<K>;",
|
|
937
978
|
" query: Query<K>;",
|
|
@@ -948,6 +989,7 @@ var PATH_NAMESPACE = [
|
|
|
948
989
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
949
990
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
950
991
|
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
992
|
+
' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
951
993
|
"}",
|
|
952
994
|
""
|
|
953
995
|
];
|
|
@@ -959,6 +1001,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
959
1001
|
" export type Params<K extends string> = never;",
|
|
960
1002
|
" export type Error<K extends string> = never;",
|
|
961
1003
|
" export type FilterFields<K extends string> = never;",
|
|
1004
|
+
" export type Stream<K extends string> = never;",
|
|
962
1005
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
963
1006
|
"}",
|
|
964
1007
|
""
|
|
@@ -971,6 +1014,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
971
1014
|
" export type Params<M extends string, U extends string> = never;",
|
|
972
1015
|
" export type Error<M extends string, U extends string> = never;",
|
|
973
1016
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
1017
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
974
1018
|
"}",
|
|
975
1019
|
""
|
|
976
1020
|
];
|
|
@@ -994,7 +1038,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
994
1038
|
for (const r of contracted) {
|
|
995
1039
|
const cs = r.contract?.contractSource;
|
|
996
1040
|
if (!cs) continue;
|
|
997
|
-
const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
|
|
1041
|
+
const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
|
|
998
1042
|
for (const ref of refs) {
|
|
999
1043
|
if (!ref) continue;
|
|
1000
1044
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -1172,18 +1216,27 @@ function refRootIdentifier(refName) {
|
|
|
1172
1216
|
function hasSource(src) {
|
|
1173
1217
|
return !!(src.schema || src.zodText || src.zodRef);
|
|
1174
1218
|
}
|
|
1219
|
+
function escapeRegExp(s) {
|
|
1220
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1221
|
+
}
|
|
1222
|
+
var wordBoundaryRegexCache = /* @__PURE__ */ new Map();
|
|
1223
|
+
function wordBoundaryRegex(token) {
|
|
1224
|
+
let re = wordBoundaryRegexCache.get(token);
|
|
1225
|
+
if (re === void 0) {
|
|
1226
|
+
re = new RegExp(`\\b${escapeRegExp(token)}\\b`, "g");
|
|
1227
|
+
wordBoundaryRegexCache.set(token, re);
|
|
1228
|
+
}
|
|
1229
|
+
return re;
|
|
1230
|
+
}
|
|
1175
1231
|
function applyRenames(text, renames) {
|
|
1176
1232
|
if (!renames || renames.size === 0) return text;
|
|
1177
1233
|
let out = text;
|
|
1178
1234
|
for (const [from, to] of renames) {
|
|
1179
1235
|
if (from === to) continue;
|
|
1180
|
-
out = out.replace(
|
|
1236
|
+
out = out.replace(wordBoundaryRegex(from), to);
|
|
1181
1237
|
}
|
|
1182
1238
|
return out;
|
|
1183
1239
|
}
|
|
1184
|
-
function escapeRegExp(s) {
|
|
1185
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1186
|
-
}
|
|
1187
1240
|
function isSelfReferential(name, text) {
|
|
1188
1241
|
return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
|
|
1189
1242
|
}
|
|
@@ -1195,7 +1248,23 @@ function planNestedSchemas(entries) {
|
|
|
1195
1248
|
const local = Object.entries(entry.nestedSchemas);
|
|
1196
1249
|
if (local.length === 0) continue;
|
|
1197
1250
|
const rename = /* @__PURE__ */ new Map();
|
|
1198
|
-
|
|
1251
|
+
const renameValues = /* @__PURE__ */ new Set();
|
|
1252
|
+
const setRename = (key, value) => {
|
|
1253
|
+
const prev = rename.get(key);
|
|
1254
|
+
rename.set(key, value);
|
|
1255
|
+
if (prev !== void 0 && prev !== value) {
|
|
1256
|
+
let stillUsed = false;
|
|
1257
|
+
for (const v of rename.values()) {
|
|
1258
|
+
if (v === prev) {
|
|
1259
|
+
stillUsed = true;
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (!stillUsed) renameValues.delete(prev);
|
|
1264
|
+
}
|
|
1265
|
+
renameValues.add(value);
|
|
1266
|
+
};
|
|
1267
|
+
for (const [name] of local) setRename(name, name);
|
|
1199
1268
|
const textFor = (name) => {
|
|
1200
1269
|
const raw = entry.nestedSchemas?.[name] ?? "";
|
|
1201
1270
|
return applyRenames(raw, rename);
|
|
@@ -1213,11 +1282,11 @@ function planNestedSchemas(entries) {
|
|
|
1213
1282
|
if (existing === text) continue;
|
|
1214
1283
|
let i = 2;
|
|
1215
1284
|
let candidate = `${name}_${i}`;
|
|
1216
|
-
while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) ||
|
|
1285
|
+
while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || renameValues.has(candidate)) {
|
|
1217
1286
|
i += 1;
|
|
1218
1287
|
candidate = `${name}_${i}`;
|
|
1219
1288
|
}
|
|
1220
|
-
|
|
1289
|
+
setRename(name, candidate);
|
|
1221
1290
|
changed = true;
|
|
1222
1291
|
}
|
|
1223
1292
|
}
|
|
@@ -1427,11 +1496,470 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
1427
1496
|
await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
|
|
1428
1497
|
}
|
|
1429
1498
|
|
|
1430
|
-
// src/emit/emit-
|
|
1499
|
+
// src/emit/emit-mocks.ts
|
|
1431
1500
|
var import_promises7 = require("fs/promises");
|
|
1432
1501
|
var import_node_path8 = require("path");
|
|
1433
|
-
|
|
1502
|
+
|
|
1503
|
+
// src/ir/schema-node-to-json-schema.ts
|
|
1504
|
+
var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
|
|
1505
|
+
function parseLiteral(raw) {
|
|
1506
|
+
const t = raw.trim();
|
|
1507
|
+
if (t === "true") return true;
|
|
1508
|
+
if (t === "false") return false;
|
|
1509
|
+
if (t === "null") return null;
|
|
1510
|
+
const q = t[0];
|
|
1511
|
+
if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
|
|
1512
|
+
return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
|
|
1513
|
+
}
|
|
1514
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
|
|
1515
|
+
return Number(t);
|
|
1516
|
+
}
|
|
1517
|
+
return t;
|
|
1518
|
+
}
|
|
1519
|
+
function literalsType(values) {
|
|
1520
|
+
const types = new Set(values.map((v) => v === null ? "null" : typeof v));
|
|
1521
|
+
if (types.size === 1) {
|
|
1522
|
+
const only = [...types][0];
|
|
1523
|
+
if (only === "string") return "string";
|
|
1524
|
+
if (only === "number") return "number";
|
|
1525
|
+
if (only === "boolean") return "boolean";
|
|
1526
|
+
}
|
|
1527
|
+
return void 0;
|
|
1528
|
+
}
|
|
1529
|
+
function convert(node, ctx) {
|
|
1530
|
+
switch (node.kind) {
|
|
1531
|
+
case "string": {
|
|
1532
|
+
const out = { type: "string" };
|
|
1533
|
+
for (const c of node.checks) {
|
|
1534
|
+
if (c.check === "email") out.format = "email";
|
|
1535
|
+
else if (c.check === "url") out.format = "uri";
|
|
1536
|
+
else if (c.check === "uuid") out.format = "uuid";
|
|
1537
|
+
else if (c.check === "min") out.minLength = Number(c.value);
|
|
1538
|
+
else if (c.check === "max") out.maxLength = Number(c.value);
|
|
1539
|
+
else if (c.check === "regex") {
|
|
1540
|
+
const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
|
|
1541
|
+
out.pattern = m ? m[1] : c.pattern;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return out;
|
|
1545
|
+
}
|
|
1546
|
+
case "number": {
|
|
1547
|
+
const out = { type: "number" };
|
|
1548
|
+
for (const c of node.checks) {
|
|
1549
|
+
if (c.check === "int") out.type = "integer";
|
|
1550
|
+
else if (c.check === "min") out.minimum = Number(c.value);
|
|
1551
|
+
else if (c.check === "max") out.maximum = Number(c.value);
|
|
1552
|
+
else if (c.check === "positive") out.exclusiveMinimum = 0;
|
|
1553
|
+
else if (c.check === "negative") out.exclusiveMaximum = 0;
|
|
1554
|
+
}
|
|
1555
|
+
return out;
|
|
1556
|
+
}
|
|
1557
|
+
case "boolean":
|
|
1558
|
+
return { type: "boolean" };
|
|
1559
|
+
case "date":
|
|
1560
|
+
return { type: "string", format: "date-time" };
|
|
1561
|
+
case "unknown":
|
|
1562
|
+
return node.note ? { description: node.note } : {};
|
|
1563
|
+
case "instanceof":
|
|
1564
|
+
return { type: "object", description: `instanceof ${node.ctor}` };
|
|
1565
|
+
case "enum": {
|
|
1566
|
+
const values = node.literals.map(parseLiteral);
|
|
1567
|
+
const t = literalsType(values);
|
|
1568
|
+
const out = { enum: values };
|
|
1569
|
+
if (t) out.type = t;
|
|
1570
|
+
return out;
|
|
1571
|
+
}
|
|
1572
|
+
case "literal": {
|
|
1573
|
+
const value = parseLiteral(node.raw);
|
|
1574
|
+
const out = { const: value };
|
|
1575
|
+
const t = literalsType([value]);
|
|
1576
|
+
if (t) out.type = t;
|
|
1577
|
+
return out;
|
|
1578
|
+
}
|
|
1579
|
+
case "union": {
|
|
1580
|
+
const options = node.options.map((o) => convert(o, ctx));
|
|
1581
|
+
const out = { oneOf: options };
|
|
1582
|
+
if (node.discriminator) {
|
|
1583
|
+
out.discriminator = { propertyName: node.discriminator };
|
|
1584
|
+
}
|
|
1585
|
+
return out;
|
|
1586
|
+
}
|
|
1587
|
+
case "object": {
|
|
1588
|
+
const properties = {};
|
|
1589
|
+
const required = [];
|
|
1590
|
+
for (const f of node.fields) {
|
|
1591
|
+
if (f.value.kind === "optional") {
|
|
1592
|
+
properties[f.key] = convert(f.value.inner, ctx);
|
|
1593
|
+
} else {
|
|
1594
|
+
properties[f.key] = convert(f.value, ctx);
|
|
1595
|
+
required.push(f.key);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const out = { type: "object", properties };
|
|
1599
|
+
if (required.length > 0) out.required = required;
|
|
1600
|
+
out.additionalProperties = node.passthrough;
|
|
1601
|
+
return out;
|
|
1602
|
+
}
|
|
1603
|
+
case "array":
|
|
1604
|
+
return { type: "array", items: convert(node.element, ctx) };
|
|
1605
|
+
case "optional":
|
|
1606
|
+
return widenNullable(convert(node.inner, ctx));
|
|
1607
|
+
case "ref":
|
|
1608
|
+
case "lazyRef":
|
|
1609
|
+
return { $ref: `${ctx.refPrefix}${node.name}` };
|
|
1610
|
+
case "annotated":
|
|
1611
|
+
return convert(node.inner, ctx);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function widenNullable(schema) {
|
|
1615
|
+
if (schema.$ref) {
|
|
1616
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1617
|
+
}
|
|
1618
|
+
if (typeof schema.type === "string") {
|
|
1619
|
+
return { ...schema, type: [schema.type, "null"] };
|
|
1620
|
+
}
|
|
1621
|
+
if (Array.isArray(schema.type)) {
|
|
1622
|
+
return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
|
|
1623
|
+
}
|
|
1624
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1625
|
+
}
|
|
1626
|
+
function schemaNodeToJsonSchema(node, ctx = DEFAULT_CTX) {
|
|
1627
|
+
return convert(node, ctx);
|
|
1628
|
+
}
|
|
1629
|
+
function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
|
|
1630
|
+
const named = {};
|
|
1631
|
+
for (const [name, node] of mod.named) {
|
|
1632
|
+
named[name] = convert(node, ctx);
|
|
1633
|
+
}
|
|
1634
|
+
return { root: convert(mod.root, ctx), named };
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// src/emit/mock-gen-runtime.ts
|
|
1638
|
+
var MOCK_GEN_RUNTIME = `
|
|
1639
|
+
/** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
|
|
1640
|
+
function makeRng(seed) {
|
|
1641
|
+
let a = seed >>> 0;
|
|
1642
|
+
return {
|
|
1643
|
+
next() {
|
|
1644
|
+
a |= 0;
|
|
1645
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
1646
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
1647
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
1648
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1649
|
+
},
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function __pick(rng, items) {
|
|
1654
|
+
return items[Math.floor(rng.next() * items.length)];
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function __intBetween(rng, min, max) {
|
|
1658
|
+
return Math.floor(rng.next() * (max - min + 1)) + min;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
|
|
1662
|
+
const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
|
|
1663
|
+
const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
|
|
1664
|
+
|
|
1665
|
+
function __fakeWords(rng, count) {
|
|
1666
|
+
let out = [];
|
|
1667
|
+
for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
|
|
1668
|
+
return out.join(' ');
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function __hex(rng, len) {
|
|
1672
|
+
let s = '';
|
|
1673
|
+
for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
|
|
1674
|
+
return s;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function __fakeUuid(rng) {
|
|
1678
|
+
return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function __fakeString(rng, schema) {
|
|
1682
|
+
switch (schema.format) {
|
|
1683
|
+
case 'email':
|
|
1684
|
+
return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
|
|
1685
|
+
case 'uri':
|
|
1686
|
+
case 'url':
|
|
1687
|
+
return 'https://example.com/' + __pick(rng, __WORDS);
|
|
1688
|
+
case 'uuid':
|
|
1689
|
+
return __fakeUuid(rng);
|
|
1690
|
+
case 'date-time':
|
|
1691
|
+
return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
|
|
1692
|
+
default:
|
|
1693
|
+
return __fakeWords(rng, __intBetween(rng, 1, 3));
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
|
|
1698
|
+
function generateMock(schema, rng, defs, depth) {
|
|
1699
|
+
defs = defs || {};
|
|
1700
|
+
depth = depth || 0;
|
|
1701
|
+
if (schema.$ref) {
|
|
1702
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
1703
|
+
const target = defs[name];
|
|
1704
|
+
if (!target || depth > 4) return null;
|
|
1705
|
+
return generateMock(target, rng, defs, depth + 1);
|
|
1706
|
+
}
|
|
1707
|
+
if ('const' in schema) return schema.const;
|
|
1708
|
+
if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
|
|
1709
|
+
if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
|
|
1710
|
+
if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
|
|
1711
|
+
let type = Array.isArray(schema.type)
|
|
1712
|
+
? (schema.type.filter((t) => t !== 'null')[0] || 'null')
|
|
1713
|
+
: schema.type;
|
|
1714
|
+
switch (type) {
|
|
1715
|
+
case 'string':
|
|
1716
|
+
return __fakeString(rng, schema);
|
|
1717
|
+
case 'integer':
|
|
1718
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
|
|
1719
|
+
case 'number':
|
|
1720
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
|
|
1721
|
+
case 'boolean':
|
|
1722
|
+
return rng.next() < 0.5;
|
|
1723
|
+
case 'null':
|
|
1724
|
+
return null;
|
|
1725
|
+
case 'array': {
|
|
1726
|
+
const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
|
|
1727
|
+
const items = schema.items || {};
|
|
1728
|
+
let arr = [];
|
|
1729
|
+
for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
|
|
1730
|
+
return arr;
|
|
1731
|
+
}
|
|
1732
|
+
case 'object': {
|
|
1733
|
+
const out = {};
|
|
1734
|
+
const props = schema.properties || {};
|
|
1735
|
+
for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
|
|
1736
|
+
return out;
|
|
1737
|
+
}
|
|
1738
|
+
default:
|
|
1739
|
+
return {};
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
`.trim();
|
|
1743
|
+
|
|
1744
|
+
// src/emit/emit-mocks.ts
|
|
1745
|
+
var REF_PREFIX = "#/components/schemas/";
|
|
1746
|
+
function toMswPath(path, baseUrl) {
|
|
1747
|
+
return `${baseUrl}${path}`;
|
|
1748
|
+
}
|
|
1749
|
+
function responseSchemaFor(route, defs) {
|
|
1750
|
+
const cs = route.contract.contractSource;
|
|
1751
|
+
if (cs.responseSchema) {
|
|
1752
|
+
const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
|
|
1753
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1754
|
+
if (!(name in defs)) defs[name] = node;
|
|
1755
|
+
}
|
|
1756
|
+
return root;
|
|
1757
|
+
}
|
|
1758
|
+
return {};
|
|
1759
|
+
}
|
|
1760
|
+
function buildMocksFile(routes, opts = {}) {
|
|
1761
|
+
const seed = opts.seed ?? 1;
|
|
1762
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
1763
|
+
const contracted = routes.filter((r) => r.contract);
|
|
1764
|
+
const defs = {};
|
|
1765
|
+
const handlers = [];
|
|
1766
|
+
for (const r of contracted) {
|
|
1767
|
+
const schema = responseSchemaFor(r, defs);
|
|
1768
|
+
const method = r.method.toLowerCase();
|
|
1769
|
+
const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
|
|
1770
|
+
const path = toMswPath(r.path, baseUrl);
|
|
1771
|
+
const cs = r.contract.contractSource;
|
|
1772
|
+
const schemaLiteral = JSON.stringify(schema);
|
|
1773
|
+
const pathLit = JSON.stringify(path);
|
|
1774
|
+
if (cs.stream) {
|
|
1775
|
+
handlers.push(
|
|
1776
|
+
[
|
|
1777
|
+
` // ${r.name} (stream)`,
|
|
1778
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1779
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1780
|
+
" const body = `data: ${JSON.stringify(value)}\\n\\n`;",
|
|
1781
|
+
" return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
|
|
1782
|
+
" }),"
|
|
1783
|
+
].join("\n")
|
|
1784
|
+
);
|
|
1785
|
+
} else {
|
|
1786
|
+
handlers.push(
|
|
1787
|
+
[
|
|
1788
|
+
` // ${r.name}`,
|
|
1789
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1790
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1791
|
+
" return HttpResponse.json(value);",
|
|
1792
|
+
" }),"
|
|
1793
|
+
].join("\n")
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
const lines = [
|
|
1798
|
+
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1799
|
+
"// MSW handlers returning deterministic, schema-shaped mock data.",
|
|
1800
|
+
"/* eslint-disable */",
|
|
1801
|
+
"// @ts-nocheck",
|
|
1802
|
+
"",
|
|
1803
|
+
"import { http, HttpResponse } from 'msw';",
|
|
1804
|
+
"",
|
|
1805
|
+
`const SEED = ${seed};`,
|
|
1806
|
+
"",
|
|
1807
|
+
"// ---------------------------------------------------------------------------",
|
|
1808
|
+
"// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
|
|
1809
|
+
"// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
|
|
1810
|
+
"// ---------------------------------------------------------------------------",
|
|
1811
|
+
MOCK_GEN_RUNTIME,
|
|
1812
|
+
"",
|
|
1813
|
+
"// Shared component schemas referenced by $ref.",
|
|
1814
|
+
`const DEFS = ${JSON.stringify(defs, null, 2)};`,
|
|
1815
|
+
"",
|
|
1816
|
+
"/** MSW request handlers, one per contracted route. */",
|
|
1817
|
+
"export const handlers = [",
|
|
1818
|
+
...handlers,
|
|
1819
|
+
"];",
|
|
1820
|
+
""
|
|
1821
|
+
];
|
|
1822
|
+
return lines.join("\n");
|
|
1823
|
+
}
|
|
1824
|
+
async function emitMocks(routes, outDir, opts = {}) {
|
|
1434
1825
|
await (0, import_promises7.mkdir)(outDir, { recursive: true });
|
|
1826
|
+
const content = buildMocksFile(routes, opts);
|
|
1827
|
+
const fileName = opts.fileName ?? "mocks.ts";
|
|
1828
|
+
await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, fileName), content, "utf8");
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// src/emit/emit-openapi.ts
|
|
1832
|
+
var import_promises8 = require("fs/promises");
|
|
1833
|
+
var import_node_path9 = require("path");
|
|
1834
|
+
var REF_PREFIX2 = "#/components/schemas/";
|
|
1835
|
+
function toOpenApiPath(path) {
|
|
1836
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
1837
|
+
}
|
|
1838
|
+
function positionSchema(schema, tsType, components) {
|
|
1839
|
+
if (schema) {
|
|
1840
|
+
const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
|
|
1841
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1842
|
+
if (!(name in components)) components[name] = node;
|
|
1843
|
+
}
|
|
1844
|
+
return root;
|
|
1845
|
+
}
|
|
1846
|
+
return tsType ? { description: tsType } : {};
|
|
1847
|
+
}
|
|
1848
|
+
function buildParameters(route) {
|
|
1849
|
+
const params = [];
|
|
1850
|
+
for (const p of route.params) {
|
|
1851
|
+
if (p.source === "path") {
|
|
1852
|
+
params.push({
|
|
1853
|
+
name: p.name,
|
|
1854
|
+
in: "path",
|
|
1855
|
+
required: true,
|
|
1856
|
+
schema: { type: "string" }
|
|
1857
|
+
});
|
|
1858
|
+
} else if (p.source === "query") {
|
|
1859
|
+
params.push({
|
|
1860
|
+
name: p.name,
|
|
1861
|
+
in: "query",
|
|
1862
|
+
required: false,
|
|
1863
|
+
schema: { type: "string" }
|
|
1864
|
+
});
|
|
1865
|
+
} else if (p.source === "header") {
|
|
1866
|
+
params.push({
|
|
1867
|
+
name: p.name,
|
|
1868
|
+
in: "header",
|
|
1869
|
+
required: false,
|
|
1870
|
+
schema: { type: "string" }
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return params;
|
|
1875
|
+
}
|
|
1876
|
+
function buildResponses(cs, components) {
|
|
1877
|
+
const responses = {};
|
|
1878
|
+
const successSchema = positionSchema(
|
|
1879
|
+
// Prefer rich response IR when present; otherwise fall back to the TS type.
|
|
1880
|
+
cs.responseSchema ?? null,
|
|
1881
|
+
cs.response,
|
|
1882
|
+
components
|
|
1883
|
+
);
|
|
1884
|
+
const successContentType = cs.stream ? "text/event-stream" : "application/json";
|
|
1885
|
+
responses["200"] = {
|
|
1886
|
+
description: cs.stream ? "Server-sent event stream" : "Successful response",
|
|
1887
|
+
content: { [successContentType]: { schema: successSchema } }
|
|
1888
|
+
};
|
|
1889
|
+
const errorSchema = positionSchema(null, cs.error ?? null, components);
|
|
1890
|
+
const errorBody = {
|
|
1891
|
+
description: "Error response",
|
|
1892
|
+
content: { "application/json": { schema: errorSchema } }
|
|
1893
|
+
};
|
|
1894
|
+
if (cs.error || cs.errorRef) {
|
|
1895
|
+
responses["400"] = errorBody;
|
|
1896
|
+
responses.default = errorBody;
|
|
1897
|
+
} else {
|
|
1898
|
+
responses.default = {
|
|
1899
|
+
description: "Error response",
|
|
1900
|
+
content: { "application/json": { schema: {} } }
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
return responses;
|
|
1904
|
+
}
|
|
1905
|
+
function buildOperation(route, components) {
|
|
1906
|
+
const cs = route.contract.contractSource;
|
|
1907
|
+
const op = {
|
|
1908
|
+
operationId: route.name,
|
|
1909
|
+
parameters: buildParameters(route),
|
|
1910
|
+
responses: buildResponses(cs, components)
|
|
1911
|
+
};
|
|
1912
|
+
const method = route.method.toUpperCase();
|
|
1913
|
+
const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
|
|
1914
|
+
if (hasBody && (cs.bodySchema || cs.body)) {
|
|
1915
|
+
const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
|
|
1916
|
+
op.requestBody = {
|
|
1917
|
+
required: true,
|
|
1918
|
+
content: { "application/json": { schema: bodySchema } }
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
return op;
|
|
1922
|
+
}
|
|
1923
|
+
function buildOpenApiSpec(routes, opts = {}) {
|
|
1924
|
+
const components = {};
|
|
1925
|
+
const paths = {};
|
|
1926
|
+
for (const route of routes) {
|
|
1927
|
+
if (!route.contract) continue;
|
|
1928
|
+
const oaPath = toOpenApiPath(route.path);
|
|
1929
|
+
const method = route.method.toLowerCase();
|
|
1930
|
+
let pathItem = paths[oaPath];
|
|
1931
|
+
if (!pathItem) {
|
|
1932
|
+
pathItem = {};
|
|
1933
|
+
paths[oaPath] = pathItem;
|
|
1934
|
+
}
|
|
1935
|
+
pathItem[method] = buildOperation(route, components);
|
|
1936
|
+
}
|
|
1937
|
+
const info = opts.info ?? {};
|
|
1938
|
+
const doc = {
|
|
1939
|
+
openapi: "3.1.0",
|
|
1940
|
+
info: {
|
|
1941
|
+
title: info.title ?? "NestJS API",
|
|
1942
|
+
version: info.version ?? "1.0.0",
|
|
1943
|
+
...info.description ? { description: info.description } : {}
|
|
1944
|
+
},
|
|
1945
|
+
paths,
|
|
1946
|
+
components: { schemas: components }
|
|
1947
|
+
};
|
|
1948
|
+
return doc;
|
|
1949
|
+
}
|
|
1950
|
+
async function emitOpenApi(routes, outDir, opts = {}) {
|
|
1951
|
+
await (0, import_promises8.mkdir)(outDir, { recursive: true });
|
|
1952
|
+
const doc = buildOpenApiSpec(routes, opts);
|
|
1953
|
+
const fileName = opts.fileName ?? "openapi.json";
|
|
1954
|
+
await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, fileName), `${JSON.stringify(doc, null, 2)}
|
|
1955
|
+
`, "utf8");
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// src/emit/emit-pages.ts
|
|
1959
|
+
var import_promises9 = require("fs/promises");
|
|
1960
|
+
var import_node_path10 = require("path");
|
|
1961
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
1962
|
+
await (0, import_promises9.mkdir)(outDir, { recursive: true });
|
|
1435
1963
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
1436
1964
|
const augBody = pages.map((p) => {
|
|
1437
1965
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -1450,7 +1978,7 @@ ${augBody}
|
|
|
1450
1978
|
}
|
|
1451
1979
|
${sharedPropsBlock}}
|
|
1452
1980
|
`;
|
|
1453
|
-
await (0,
|
|
1981
|
+
await (0, import_promises9.writeFile)((0, import_node_path10.join)(outDir, "pages.d.ts"), content, "utf8");
|
|
1454
1982
|
}
|
|
1455
1983
|
function buildSharedPropsBlock(sharedProps) {
|
|
1456
1984
|
if (!sharedProps) return "";
|
|
@@ -1469,7 +1997,7 @@ ${propsBody}
|
|
|
1469
1997
|
`;
|
|
1470
1998
|
}
|
|
1471
1999
|
function buildAugmentationType(page, outDir) {
|
|
1472
|
-
let importPath = (0,
|
|
2000
|
+
let importPath = (0, import_node_path10.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
|
|
1473
2001
|
if (!importPath.startsWith(".")) {
|
|
1474
2002
|
importPath = `./${importPath}`;
|
|
1475
2003
|
}
|
|
@@ -1480,12 +2008,12 @@ function needsQuotes(name) {
|
|
|
1480
2008
|
}
|
|
1481
2009
|
|
|
1482
2010
|
// src/emit/emit-routes.ts
|
|
1483
|
-
var
|
|
1484
|
-
var
|
|
2011
|
+
var import_promises10 = require("fs/promises");
|
|
2012
|
+
var import_node_path11 = require("path");
|
|
1485
2013
|
async function emitRoutes(routes, outDir) {
|
|
1486
|
-
await (0,
|
|
2014
|
+
await (0, import_promises10.mkdir)(outDir, { recursive: true });
|
|
1487
2015
|
const content = buildRoutesFile(routes);
|
|
1488
|
-
await (0,
|
|
2016
|
+
await (0, import_promises10.writeFile)((0, import_node_path11.join)(outDir, "routes.ts"), content, "utf8");
|
|
1489
2017
|
}
|
|
1490
2018
|
function buildRoutesFile(routes) {
|
|
1491
2019
|
if (routes.length === 0) {
|
|
@@ -1633,24 +2161,41 @@ async function generate(config, inputRoutes = []) {
|
|
|
1633
2161
|
});
|
|
1634
2162
|
}
|
|
1635
2163
|
const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
|
|
2164
|
+
if (hasContracts && config.openapi.enabled) {
|
|
2165
|
+
await emitOpenApi(routes, config.codegen.outDir, {
|
|
2166
|
+
fileName: config.openapi.fileName,
|
|
2167
|
+
info: {
|
|
2168
|
+
title: config.openapi.title,
|
|
2169
|
+
version: config.openapi.version,
|
|
2170
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
if (hasContracts && config.mocks.enabled) {
|
|
2175
|
+
await emitMocks(routes, config.codegen.outDir, {
|
|
2176
|
+
fileName: config.mocks.fileName,
|
|
2177
|
+
seed: config.mocks.seed,
|
|
2178
|
+
baseUrl: config.mocks.baseUrl
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
1636
2181
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
1637
2182
|
if (extensions.length > 0) {
|
|
1638
2183
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1639
2184
|
for (const file of extraFiles) {
|
|
1640
|
-
const dest = (0,
|
|
1641
|
-
await (0,
|
|
1642
|
-
await (0,
|
|
2185
|
+
const dest = (0, import_node_path12.join)(config.codegen.outDir, file.path);
|
|
2186
|
+
await (0, import_promises11.mkdir)((0, import_node_path12.dirname)(dest), { recursive: true });
|
|
2187
|
+
await (0, import_promises11.writeFile)(dest, file.contents, "utf8");
|
|
1643
2188
|
}
|
|
1644
2189
|
}
|
|
1645
2190
|
}
|
|
1646
2191
|
|
|
1647
2192
|
// src/watch/watcher.ts
|
|
1648
|
-
var
|
|
1649
|
-
var
|
|
2193
|
+
var import_promises14 = require("fs/promises");
|
|
2194
|
+
var import_node_path16 = require("path");
|
|
1650
2195
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
1651
2196
|
|
|
1652
2197
|
// src/discovery/contracts-fast.ts
|
|
1653
|
-
var
|
|
2198
|
+
var import_node_path14 = require("path");
|
|
1654
2199
|
var import_fast_glob2 = __toESM(require("fast-glob"), 1);
|
|
1655
2200
|
var import_ts_morph9 = require("ts-morph");
|
|
1656
2201
|
|
|
@@ -1662,7 +2207,7 @@ var import_ts_morph4 = require("ts-morph");
|
|
|
1662
2207
|
|
|
1663
2208
|
// src/discovery/type-ref-resolution.ts
|
|
1664
2209
|
var import_node_fs = require("fs");
|
|
1665
|
-
var
|
|
2210
|
+
var import_node_path13 = require("path");
|
|
1666
2211
|
var import_ts_morph3 = require("ts-morph");
|
|
1667
2212
|
var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
|
|
1668
2213
|
var _ctxByProject = /* @__PURE__ */ new WeakMap();
|
|
@@ -1714,12 +2259,12 @@ function findTypeInFile(name, file) {
|
|
|
1714
2259
|
}
|
|
1715
2260
|
function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
1716
2261
|
if (moduleSpecifier.startsWith(".")) {
|
|
1717
|
-
const dir = (0,
|
|
2262
|
+
const dir = (0, import_node_path13.dirname)(sourceFile.getFilePath());
|
|
1718
2263
|
const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
|
|
1719
2264
|
return [
|
|
1720
|
-
(0,
|
|
1721
|
-
(0,
|
|
1722
|
-
(0,
|
|
2265
|
+
(0, import_node_path13.resolve)(dir, `${noExt}.ts`),
|
|
2266
|
+
(0, import_node_path13.resolve)(dir, `${moduleSpecifier}.ts`),
|
|
2267
|
+
(0, import_node_path13.resolve)(dir, moduleSpecifier, "index.ts")
|
|
1723
2268
|
];
|
|
1724
2269
|
}
|
|
1725
2270
|
const ctx = _ctxFor(project);
|
|
@@ -1740,8 +2285,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
|
1740
2285
|
const rest = moduleSpecifier.slice(prefix.length);
|
|
1741
2286
|
const candidates = [];
|
|
1742
2287
|
for (const mapping of mappings) {
|
|
1743
|
-
const resolved = (0,
|
|
1744
|
-
candidates.push(`${resolved}.ts`, (0,
|
|
2288
|
+
const resolved = (0, import_node_path13.resolve)(baseUrl, mapping.replace("*", rest));
|
|
2289
|
+
candidates.push(`${resolved}.ts`, (0, import_node_path13.resolve)(resolved, "index.ts"));
|
|
1745
2290
|
}
|
|
1746
2291
|
dbg(" resolved candidates:", candidates);
|
|
1747
2292
|
return candidates;
|
|
@@ -1822,7 +2367,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
|
|
|
1822
2367
|
}
|
|
1823
2368
|
return null;
|
|
1824
2369
|
}
|
|
2370
|
+
function resolveImportedVariable(name, sourceFile, project) {
|
|
2371
|
+
const local = sourceFile.getVariableDeclaration(name);
|
|
2372
|
+
if (local) return { decl: local, file: sourceFile };
|
|
2373
|
+
return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
|
|
2374
|
+
}
|
|
2375
|
+
function resolveVariableViaImports(name, sourceFile, project, seen) {
|
|
2376
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
2377
|
+
const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
|
|
2378
|
+
if (!namedImport) continue;
|
|
2379
|
+
const sourceName = namedImport.getName();
|
|
2380
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
2381
|
+
const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
|
|
2382
|
+
if (found) return found;
|
|
2383
|
+
}
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
|
|
2387
|
+
const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
|
|
2388
|
+
for (const candidate of candidates) {
|
|
2389
|
+
let importedFile = project.getSourceFile(candidate);
|
|
2390
|
+
if (!importedFile) {
|
|
2391
|
+
try {
|
|
2392
|
+
importedFile = project.addSourceFileAtPath(candidate);
|
|
2393
|
+
} catch {
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
const found = resolveVariableInFile(name, importedFile, project, seen);
|
|
2398
|
+
if (found) return found;
|
|
2399
|
+
}
|
|
2400
|
+
return null;
|
|
2401
|
+
}
|
|
2402
|
+
function resolveVariableInFile(name, file, project, seen) {
|
|
2403
|
+
const filePath = file.getFilePath();
|
|
2404
|
+
if (seen.has(filePath)) return null;
|
|
2405
|
+
seen.add(filePath);
|
|
2406
|
+
const local = file.getVariableDeclaration(name);
|
|
2407
|
+
if (local) return { decl: local, file };
|
|
2408
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
2409
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
2410
|
+
const namedExports = exportDecl.getNamedExports();
|
|
2411
|
+
if (moduleSpecifier) {
|
|
2412
|
+
const hasStar = namedExports.length === 0;
|
|
2413
|
+
const reExport2 = namedExports.find(
|
|
2414
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2415
|
+
);
|
|
2416
|
+
if (!hasStar && !reExport2) continue;
|
|
2417
|
+
const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
|
|
2418
|
+
const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
|
|
2419
|
+
if (found) return found;
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
const reExport = namedExports.find(
|
|
2423
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2424
|
+
);
|
|
2425
|
+
if (!reExport) continue;
|
|
2426
|
+
const sourceName = reExport.getName();
|
|
2427
|
+
const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
|
|
2428
|
+
if (viaImports) return viaImports;
|
|
2429
|
+
}
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
1825
2432
|
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
2433
|
+
function clearTypeResolutionCaches(project) {
|
|
2434
|
+
_findTypeCache.delete(project);
|
|
2435
|
+
_resolveNamedRefCache.delete(project);
|
|
2436
|
+
}
|
|
1826
2437
|
function findType(name, sourceFile, project) {
|
|
1827
2438
|
let byKey = _findTypeCache.get(project);
|
|
1828
2439
|
if (byKey === void 0) {
|
|
@@ -1963,9 +2574,11 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
1963
2574
|
warnings: [],
|
|
1964
2575
|
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
1965
2576
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
2577
|
+
usedSchemaNames: /* @__PURE__ */ new Set(),
|
|
1966
2578
|
visiting: /* @__PURE__ */ new Set(),
|
|
1967
2579
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
1968
|
-
depth: 0
|
|
2580
|
+
depth: 0,
|
|
2581
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
1969
2582
|
};
|
|
1970
2583
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
1971
2584
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -1989,11 +2602,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
1989
2602
|
const typeNode = prop.getTypeNode();
|
|
1990
2603
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
1991
2604
|
const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
|
|
2605
|
+
const discriminator = resolveDiscriminator(dec("Type"));
|
|
2606
|
+
if (discriminator) {
|
|
2607
|
+
const options = discriminator.subTypes.map(
|
|
2608
|
+
(name) => buildNestedReference(name, classFile, ctx)
|
|
2609
|
+
);
|
|
2610
|
+
const unionNode = {
|
|
2611
|
+
kind: "union",
|
|
2612
|
+
options,
|
|
2613
|
+
discriminator: discriminator.property
|
|
2614
|
+
};
|
|
2615
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2616
|
+
const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
|
|
2617
|
+
return applyPresence(node2, decorators);
|
|
2618
|
+
}
|
|
2619
|
+
const propTypeParam = singularClassName(typeText);
|
|
2620
|
+
if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
|
|
2621
|
+
const bound = ctx.typeBindings.get(propTypeParam);
|
|
2622
|
+
const childNode = buildNestedReference(bound, classFile, ctx);
|
|
2623
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2624
|
+
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
2625
|
+
return applyPresence(node2, decorators);
|
|
2626
|
+
}
|
|
1992
2627
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
1993
2628
|
if (has("ValidateNested") || typeRefName) {
|
|
2629
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
1994
2630
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
1995
2631
|
if (childName) {
|
|
1996
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
2632
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
1997
2633
|
const wrapArray = has("IsArray") || isArrayType;
|
|
1998
2634
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
1999
2635
|
return applyPresence(node2, decorators);
|
|
@@ -2118,10 +2754,13 @@ function baseFromType(typeText, isArrayType) {
|
|
|
2118
2754
|
return { kind: "unknown" };
|
|
2119
2755
|
}
|
|
2120
2756
|
}
|
|
2121
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2757
|
+
function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
|
|
2758
|
+
const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
|
|
2759
|
+
const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
|
|
2760
|
+
if (ctx.visiting.has(cacheKey)) {
|
|
2761
|
+
const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
|
|
2762
|
+
ctx.emittedClasses.set(cacheKey, reserved);
|
|
2763
|
+
ctx.usedSchemaNames.add(reserved);
|
|
2125
2764
|
ctx.recursiveSchemas.add(reserved);
|
|
2126
2765
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2127
2766
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -2140,29 +2779,37 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
2140
2779
|
}
|
|
2141
2780
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
2142
2781
|
}
|
|
2143
|
-
const existing = ctx.emittedClasses.get(
|
|
2782
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
2144
2783
|
if (existing) return { kind: "ref", name: existing };
|
|
2145
|
-
const schemaName = aliasFor(
|
|
2784
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
2146
2785
|
const resolved = findType(className, fromFile, ctx.project);
|
|
2147
2786
|
if (!resolved || resolved.kind !== "class") {
|
|
2148
2787
|
return { kind: "object", fields: [], passthrough: true };
|
|
2149
2788
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2789
|
+
const params = resolved.decl.getTypeParameters().map((p) => p.getName());
|
|
2790
|
+
const newBindings = [];
|
|
2791
|
+
params.forEach((param, i) => {
|
|
2792
|
+
const arg = typeArgs[i];
|
|
2793
|
+
if (arg) newBindings.push([param, arg]);
|
|
2794
|
+
});
|
|
2795
|
+
for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
|
|
2796
|
+
ctx.emittedClasses.set(cacheKey, schemaName);
|
|
2797
|
+
ctx.usedSchemaNames.add(schemaName);
|
|
2798
|
+
ctx.visiting.add(cacheKey);
|
|
2152
2799
|
ctx.depth += 1;
|
|
2153
2800
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
2154
2801
|
ctx.depth -= 1;
|
|
2155
|
-
ctx.visiting.delete(
|
|
2802
|
+
ctx.visiting.delete(cacheKey);
|
|
2803
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
2156
2804
|
ctx.named.set(schemaName, childNode);
|
|
2805
|
+
ctx.usedSchemaNames.add(schemaName);
|
|
2157
2806
|
return { kind: "ref", name: schemaName };
|
|
2158
2807
|
}
|
|
2159
2808
|
function aliasFor(className, ctx) {
|
|
2160
2809
|
const baseName = `${className}Schema`;
|
|
2161
2810
|
let candidate = baseName;
|
|
2162
2811
|
let i = 1;
|
|
2163
|
-
|
|
2164
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
2165
|
-
while (used.has(candidate)) {
|
|
2812
|
+
while (ctx.usedSchemaNames.has(candidate)) {
|
|
2166
2813
|
candidate = `${baseName}_${i}`;
|
|
2167
2814
|
i += 1;
|
|
2168
2815
|
}
|
|
@@ -2199,6 +2846,39 @@ function messageRaw(decorator) {
|
|
|
2199
2846
|
}
|
|
2200
2847
|
return void 0;
|
|
2201
2848
|
}
|
|
2849
|
+
function resolveDiscriminator(decorator) {
|
|
2850
|
+
const optsArg = decorator?.getArguments()[1];
|
|
2851
|
+
if (!optsArg || !import_ts_morph4.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
2852
|
+
let discProp;
|
|
2853
|
+
for (const prop of optsArg.getProperties()) {
|
|
2854
|
+
if (import_ts_morph4.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
|
|
2855
|
+
discProp = prop.getInitializer();
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
if (!discProp || !import_ts_morph4.Node.isObjectLiteralExpression(discProp)) return null;
|
|
2859
|
+
let property = null;
|
|
2860
|
+
const subTypes = [];
|
|
2861
|
+
for (const prop of discProp.getProperties()) {
|
|
2862
|
+
if (!import_ts_morph4.Node.isPropertyAssignment(prop)) continue;
|
|
2863
|
+
const name = prop.getName();
|
|
2864
|
+
const init = prop.getInitializer();
|
|
2865
|
+
if (!init) continue;
|
|
2866
|
+
if (name === "property" && import_ts_morph4.Node.isStringLiteral(init)) {
|
|
2867
|
+
property = init.getLiteralValue();
|
|
2868
|
+
} else if (name === "subTypes" && import_ts_morph4.Node.isArrayLiteralExpression(init)) {
|
|
2869
|
+
for (const el of init.getElements()) {
|
|
2870
|
+
if (!import_ts_morph4.Node.isObjectLiteralExpression(el)) continue;
|
|
2871
|
+
for (const p of el.getProperties()) {
|
|
2872
|
+
if (!import_ts_morph4.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
|
|
2873
|
+
const nameInit = p.getInitializer();
|
|
2874
|
+
if (nameInit && import_ts_morph4.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
if (!property || subTypes.length === 0) return null;
|
|
2880
|
+
return { property, subTypes };
|
|
2881
|
+
}
|
|
2202
2882
|
function resolveTypeFactoryName(decorator) {
|
|
2203
2883
|
const arg = firstArg(decorator);
|
|
2204
2884
|
if (!arg) return null;
|
|
@@ -2212,6 +2892,17 @@ function singularClassName(typeText) {
|
|
|
2212
2892
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2213
2893
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2214
2894
|
}
|
|
2895
|
+
function genericTypeArgNames(typeNode) {
|
|
2896
|
+
if (!typeNode || !import_ts_morph4.Node.isTypeReference(typeNode)) return [];
|
|
2897
|
+
const names = [];
|
|
2898
|
+
for (const arg of typeNode.getTypeArguments()) {
|
|
2899
|
+
if (!import_ts_morph4.Node.isTypeReference(arg)) return [];
|
|
2900
|
+
const tn = arg.getTypeName();
|
|
2901
|
+
if (!import_ts_morph4.Node.isIdentifier(tn)) return [];
|
|
2902
|
+
names.push(tn.getText());
|
|
2903
|
+
}
|
|
2904
|
+
return names;
|
|
2905
|
+
}
|
|
2215
2906
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
2216
2907
|
const arg = firstArg(decorator);
|
|
2217
2908
|
if (!arg) return null;
|
|
@@ -2266,6 +2957,9 @@ var import_ts_morph5 = require("ts-morph");
|
|
|
2266
2957
|
|
|
2267
2958
|
// src/discovery/enum-resolution.ts
|
|
2268
2959
|
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
2960
|
+
function clearEnumCache(project) {
|
|
2961
|
+
_enumCache.delete(project);
|
|
2962
|
+
}
|
|
2269
2963
|
function resolveEnumValues(name, sourceFile, project) {
|
|
2270
2964
|
let byKey = _enumCache.get(project);
|
|
2271
2965
|
if (byKey === void 0) {
|
|
@@ -2734,24 +3428,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
2734
3428
|
"Map",
|
|
2735
3429
|
"Set"
|
|
2736
3430
|
]);
|
|
2737
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3431
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2738
3432
|
if (depth <= 0) return "unknown";
|
|
2739
3433
|
if (import_ts_morph7.Node.isArrayTypeNode(typeNode)) {
|
|
2740
3434
|
const elementType = typeNode.getElementTypeNode();
|
|
2741
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3435
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
2742
3436
|
}
|
|
2743
3437
|
if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
|
|
2744
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3438
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
2745
3439
|
}
|
|
2746
3440
|
if (import_ts_morph7.Node.isIntersectionTypeNode(typeNode)) {
|
|
2747
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3441
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
2748
3442
|
}
|
|
2749
3443
|
if (import_ts_morph7.Node.isParenthesizedTypeNode(typeNode)) {
|
|
2750
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3444
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
2751
3445
|
}
|
|
2752
3446
|
if (import_ts_morph7.Node.isTypeReference(typeNode)) {
|
|
2753
3447
|
const typeName = typeNode.getTypeName();
|
|
2754
3448
|
const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3449
|
+
const bound = subst.get(name);
|
|
3450
|
+
if (bound !== void 0) return bound;
|
|
2755
3451
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
2756
3452
|
if (name === "Date") return "string";
|
|
2757
3453
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -2759,14 +3455,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2759
3455
|
return "unknown";
|
|
2760
3456
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
2761
3457
|
if (wrapperMode) {
|
|
2762
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3458
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
2763
3459
|
}
|
|
2764
3460
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
2765
3461
|
return typeNode.getText();
|
|
2766
3462
|
}
|
|
2767
3463
|
const resolved = findType(name, sourceFile, project);
|
|
2768
3464
|
if (resolved) {
|
|
2769
|
-
|
|
3465
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
3466
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
2770
3467
|
}
|
|
2771
3468
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
2772
3469
|
return "unknown";
|
|
@@ -2779,32 +3476,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2779
3476
|
if (kind === import_ts_morph7.SyntaxKind.AnyKeyword) return "unknown";
|
|
2780
3477
|
return typeNode.getText();
|
|
2781
3478
|
}
|
|
2782
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
3479
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
2783
3480
|
const typeArgs = typeNode.getTypeArguments();
|
|
2784
3481
|
const firstTypeArg = typeArgs[0];
|
|
2785
3482
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2786
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3483
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
2787
3484
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2788
3485
|
}
|
|
2789
3486
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2790
3487
|
}
|
|
2791
|
-
function
|
|
3488
|
+
function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
|
|
3489
|
+
if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
|
|
3490
|
+
const params = result.decl.getTypeParameters().map((p) => p.getName());
|
|
3491
|
+
if (params.length === 0) return /* @__PURE__ */ new Map();
|
|
3492
|
+
const args = typeNode.getTypeArguments();
|
|
3493
|
+
const subst = /* @__PURE__ */ new Map();
|
|
3494
|
+
params.forEach((param, i) => {
|
|
3495
|
+
const arg = args[i];
|
|
3496
|
+
if (arg)
|
|
3497
|
+
subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
|
|
3498
|
+
});
|
|
3499
|
+
return subst;
|
|
3500
|
+
}
|
|
3501
|
+
function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2792
3502
|
if (depth < 0) return "unknown";
|
|
2793
3503
|
switch (result.kind) {
|
|
2794
3504
|
case "class":
|
|
2795
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3505
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2796
3506
|
case "interface":
|
|
2797
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3507
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2798
3508
|
case "typeAlias":
|
|
2799
3509
|
if (result.typeNode) {
|
|
2800
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
3510
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
2801
3511
|
}
|
|
2802
3512
|
return result.text;
|
|
2803
3513
|
case "enum":
|
|
2804
3514
|
return result.members.join(" | ");
|
|
2805
3515
|
}
|
|
2806
3516
|
}
|
|
2807
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
3517
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2808
3518
|
if (depth < 0) return "unknown";
|
|
2809
3519
|
const lines = [];
|
|
2810
3520
|
for (const prop of decl.getProperties()) {
|
|
@@ -2813,7 +3523,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
2813
3523
|
const propTypeNode = prop.getTypeNode();
|
|
2814
3524
|
let propType = "unknown";
|
|
2815
3525
|
if (propTypeNode) {
|
|
2816
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
3526
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
2817
3527
|
}
|
|
2818
3528
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
2819
3529
|
}
|
|
@@ -2862,7 +3572,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
2862
3572
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
2863
3573
|
}
|
|
2864
3574
|
function extractResponseType(method, sourceFile, project) {
|
|
2865
|
-
const apiResponseDecorator = method.
|
|
3575
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2866
3576
|
if (apiResponseDecorator) {
|
|
2867
3577
|
const args = apiResponseDecorator.getArguments();
|
|
2868
3578
|
const optsArg = args[0];
|
|
@@ -2891,6 +3601,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
2891
3601
|
}
|
|
2892
3602
|
return "unknown";
|
|
2893
3603
|
}
|
|
3604
|
+
function apiResponseStatus(decorator) {
|
|
3605
|
+
const optsArg = decorator.getArguments()[0];
|
|
3606
|
+
if (!optsArg || !import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
3607
|
+
for (const prop of optsArg.getProperties()) {
|
|
3608
|
+
if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
|
|
3609
|
+
if (prop.getName() !== "status") continue;
|
|
3610
|
+
const val = prop.getInitializer();
|
|
3611
|
+
if (val && import_ts_morph7.Node.isNumericLiteral(val)) return Number(val.getLiteralValue());
|
|
3612
|
+
}
|
|
3613
|
+
return null;
|
|
3614
|
+
}
|
|
3615
|
+
function apiResponseTypeNode(decorator) {
|
|
3616
|
+
const optsArg = decorator.getArguments()[0];
|
|
3617
|
+
if (!optsArg || !import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
3618
|
+
for (const prop of optsArg.getProperties()) {
|
|
3619
|
+
if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
|
|
3620
|
+
if (prop.getName() !== "type") continue;
|
|
3621
|
+
const val = prop.getInitializer();
|
|
3622
|
+
if (!val) return null;
|
|
3623
|
+
if (import_ts_morph7.Node.isArrayLiteralExpression(val)) {
|
|
3624
|
+
const first = val.getElements()[0];
|
|
3625
|
+
return first ? { node: first, isArray: true } : null;
|
|
3626
|
+
}
|
|
3627
|
+
return { node: val, isArray: false };
|
|
3628
|
+
}
|
|
3629
|
+
return null;
|
|
3630
|
+
}
|
|
3631
|
+
function extractErrorType(method, sourceFile, project) {
|
|
3632
|
+
for (const decorator of method.getDecorators()) {
|
|
3633
|
+
if (decorator.getName() !== "ApiResponse") continue;
|
|
3634
|
+
const status = apiResponseStatus(decorator);
|
|
3635
|
+
if (status === null || status < 400) continue;
|
|
3636
|
+
const typeInfo = apiResponseTypeNode(decorator);
|
|
3637
|
+
if (!typeInfo) continue;
|
|
3638
|
+
const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
|
|
3639
|
+
const type = typeInfo.isArray ? `Array<${inner}>` : inner;
|
|
3640
|
+
let ref = null;
|
|
3641
|
+
if (import_ts_morph7.Node.isIdentifier(typeInfo.node)) {
|
|
3642
|
+
const name = typeInfo.node.getText();
|
|
3643
|
+
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3644
|
+
if (localDecl?.isExported()) {
|
|
3645
|
+
ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
|
|
3646
|
+
} else {
|
|
3647
|
+
const resolved = resolveImportedType(name, sourceFile, project);
|
|
3648
|
+
if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
|
|
3649
|
+
ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return { type, ref };
|
|
3654
|
+
}
|
|
3655
|
+
return null;
|
|
3656
|
+
}
|
|
2894
3657
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
2895
3658
|
if (!import_ts_morph7.Node.isIdentifier(node)) return "unknown";
|
|
2896
3659
|
const name = node.getText();
|
|
@@ -2906,17 +3669,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
2906
3669
|
unwrapContainers: true
|
|
2907
3670
|
});
|
|
2908
3671
|
}
|
|
3672
|
+
var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
|
|
3673
|
+
var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
|
|
3674
|
+
var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
|
|
3675
|
+
function detectStreamElement(method) {
|
|
3676
|
+
const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
|
|
3677
|
+
let node = method.getReturnTypeNode();
|
|
3678
|
+
node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
|
|
3679
|
+
const containerEl = streamContainerElement(node);
|
|
3680
|
+
if (containerEl) {
|
|
3681
|
+
return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
|
|
3682
|
+
}
|
|
3683
|
+
if (hasSse) return node ?? null;
|
|
3684
|
+
return null;
|
|
3685
|
+
}
|
|
3686
|
+
function streamContainerElement(node) {
|
|
3687
|
+
if (!node || !import_ts_morph7.Node.isTypeReference(node)) return null;
|
|
3688
|
+
const typeName = node.getTypeName();
|
|
3689
|
+
const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3690
|
+
if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
|
|
3691
|
+
return node.getTypeArguments()[0] ?? null;
|
|
3692
|
+
}
|
|
3693
|
+
return null;
|
|
3694
|
+
}
|
|
3695
|
+
function unwrapNamedContainer(node, names) {
|
|
3696
|
+
if (!node || !import_ts_morph7.Node.isTypeReference(node)) return node;
|
|
3697
|
+
const typeName = node.getTypeName();
|
|
3698
|
+
const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3699
|
+
if (names.has(name)) {
|
|
3700
|
+
return node.getTypeArguments()[0] ?? node;
|
|
3701
|
+
}
|
|
3702
|
+
return node;
|
|
3703
|
+
}
|
|
2909
3704
|
function extractDtoContract(method, sourceFile, project) {
|
|
2910
3705
|
let body = extractBodyType(method, sourceFile, project);
|
|
2911
3706
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
2912
3707
|
const query = extractQueryType(method, sourceFile, project);
|
|
3708
|
+
const streamElement = detectStreamElement(method);
|
|
3709
|
+
const isStream = streamElement !== null;
|
|
2913
3710
|
if (filterInfo && filterInfo.source === "body") {
|
|
2914
3711
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
2915
3712
|
body = body ?? bodyType;
|
|
2916
3713
|
}
|
|
2917
3714
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
2918
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
2919
|
-
|
|
3715
|
+
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
3716
|
+
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
3717
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
2920
3718
|
return null;
|
|
2921
3719
|
}
|
|
2922
3720
|
let bodyRef = null;
|
|
@@ -2930,12 +3728,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2930
3728
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
2931
3729
|
}
|
|
2932
3730
|
}
|
|
2933
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
3731
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
2934
3732
|
if (returnTypeNode) {
|
|
2935
3733
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
2936
3734
|
}
|
|
2937
|
-
if (!responseRef) {
|
|
2938
|
-
const apiResp = method.
|
|
3735
|
+
if (!responseRef && !isStream) {
|
|
3736
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2939
3737
|
if (apiResp) {
|
|
2940
3738
|
const args = apiResp.getArguments();
|
|
2941
3739
|
const optsArg = args[0];
|
|
@@ -2977,16 +3775,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2977
3775
|
query,
|
|
2978
3776
|
body,
|
|
2979
3777
|
response,
|
|
3778
|
+
error: errorInfo?.type ?? null,
|
|
2980
3779
|
params: paramsType,
|
|
2981
3780
|
queryRef,
|
|
2982
3781
|
bodyRef,
|
|
2983
3782
|
responseRef,
|
|
3783
|
+
errorRef: errorInfo?.ref ?? null,
|
|
2984
3784
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2985
3785
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2986
3786
|
filterSource: filterInfo?.source ?? null,
|
|
2987
3787
|
formWarnings,
|
|
2988
3788
|
bodySchema,
|
|
2989
|
-
querySchema
|
|
3789
|
+
querySchema,
|
|
3790
|
+
stream: isStream
|
|
2990
3791
|
};
|
|
2991
3792
|
}
|
|
2992
3793
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -3104,6 +3905,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
3104
3905
|
let query = null;
|
|
3105
3906
|
let body = null;
|
|
3106
3907
|
let response = "unknown";
|
|
3908
|
+
let error = null;
|
|
3107
3909
|
let bodyZodText = null;
|
|
3108
3910
|
let queryZodText = null;
|
|
3109
3911
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -3119,25 +3921,38 @@ function parseDefineContractCall(callExpr) {
|
|
|
3119
3921
|
bodyZodText = val.getText();
|
|
3120
3922
|
} else if (propName === "response") {
|
|
3121
3923
|
response = zodAstToTs(val);
|
|
3924
|
+
} else if (propName === "error") {
|
|
3925
|
+
error = zodAstToTs(val);
|
|
3122
3926
|
}
|
|
3123
3927
|
}
|
|
3124
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3928
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
3125
3929
|
}
|
|
3126
3930
|
|
|
3127
3931
|
// src/discovery/contracts-fast.ts
|
|
3128
3932
|
async function discoverContractsFast(opts) {
|
|
3129
3933
|
const { cwd, glob, tsconfig } = opts;
|
|
3130
|
-
const tsconfigPath =
|
|
3131
|
-
|
|
3934
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3935
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3936
|
+
const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3937
|
+
for (const f of files) {
|
|
3938
|
+
project.addSourceFileAtPath(f);
|
|
3939
|
+
}
|
|
3940
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3941
|
+
return extractAllRoutes(project);
|
|
3942
|
+
}
|
|
3943
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
3944
|
+
return tsconfig ? (0, import_node_path14.resolve)(tsconfig) : (0, import_node_path14.join)(cwd, "tsconfig.json");
|
|
3945
|
+
}
|
|
3946
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
3132
3947
|
try {
|
|
3133
|
-
|
|
3948
|
+
return new import_ts_morph9.Project({
|
|
3134
3949
|
tsConfigFilePath: tsconfigPath,
|
|
3135
3950
|
skipAddingFilesFromTsConfig: true,
|
|
3136
3951
|
skipLoadingLibFiles: true,
|
|
3137
3952
|
skipFileDependencyResolution: true
|
|
3138
3953
|
});
|
|
3139
3954
|
} catch {
|
|
3140
|
-
|
|
3955
|
+
return new import_ts_morph9.Project({
|
|
3141
3956
|
skipAddingFilesFromTsConfig: true,
|
|
3142
3957
|
skipLoadingLibFiles: true,
|
|
3143
3958
|
skipFileDependencyResolution: true,
|
|
@@ -3148,20 +3963,105 @@ async function discoverContractsFast(opts) {
|
|
|
3148
3963
|
}
|
|
3149
3964
|
});
|
|
3150
3965
|
}
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
project.addSourceFileAtPath(f);
|
|
3154
|
-
}
|
|
3155
|
-
const routes = [];
|
|
3966
|
+
}
|
|
3967
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
3156
3968
|
setDiscoveryContext(project, {
|
|
3157
3969
|
projectRoot: cwd,
|
|
3158
3970
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3159
3971
|
});
|
|
3972
|
+
}
|
|
3973
|
+
function extractRoutesFrom(project, controllerPaths) {
|
|
3974
|
+
const routes = [];
|
|
3975
|
+
for (const path of controllerPaths) {
|
|
3976
|
+
const sourceFile = project.getSourceFile(path);
|
|
3977
|
+
if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3978
|
+
}
|
|
3979
|
+
return routes;
|
|
3980
|
+
}
|
|
3981
|
+
function extractAllRoutes(project) {
|
|
3982
|
+
const routes = [];
|
|
3160
3983
|
for (const sourceFile of project.getSourceFiles()) {
|
|
3161
3984
|
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3162
3985
|
}
|
|
3163
3986
|
return routes;
|
|
3164
3987
|
}
|
|
3988
|
+
var PersistentDiscovery = class _PersistentDiscovery {
|
|
3989
|
+
project;
|
|
3990
|
+
cwd;
|
|
3991
|
+
glob;
|
|
3992
|
+
/** Absolute paths of the controllers currently loaded as extraction roots. */
|
|
3993
|
+
controllerPaths = /* @__PURE__ */ new Set();
|
|
3994
|
+
constructor(project, cwd, glob) {
|
|
3995
|
+
this.project = project;
|
|
3996
|
+
this.cwd = cwd;
|
|
3997
|
+
this.glob = glob;
|
|
3998
|
+
}
|
|
3999
|
+
/**
|
|
4000
|
+
* Build the initial persistent Project: create it, glob + add all controllers,
|
|
4001
|
+
* bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
|
|
4002
|
+
*/
|
|
4003
|
+
static async create(opts) {
|
|
4004
|
+
const { cwd, glob, tsconfig } = opts;
|
|
4005
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
4006
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
4007
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
4008
|
+
const instance = new _PersistentDiscovery(project, cwd, glob);
|
|
4009
|
+
const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
|
|
4010
|
+
for (const f of files) {
|
|
4011
|
+
project.addSourceFileAtPath(f);
|
|
4012
|
+
instance.controllerPaths.add(f);
|
|
4013
|
+
}
|
|
4014
|
+
return instance;
|
|
4015
|
+
}
|
|
4016
|
+
/** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
|
|
4017
|
+
discover() {
|
|
4018
|
+
return this.runExtraction();
|
|
4019
|
+
}
|
|
4020
|
+
/**
|
|
4021
|
+
* Re-discover after one or more files changed. Refreshes the changed file(s)
|
|
4022
|
+
* from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
|
|
4023
|
+
* to pick up added/removed controllers, clears the per-Project caches, then
|
|
4024
|
+
* re-extracts. `changedPaths` is a hint; correctness does not depend on it
|
|
4025
|
+
* being exhaustive because re-globbing + refresh-on-presence covers the set.
|
|
4026
|
+
*/
|
|
4027
|
+
async rediscover(changedPaths) {
|
|
4028
|
+
if (changedPaths) {
|
|
4029
|
+
for (const p of changedPaths) {
|
|
4030
|
+
const abs = (0, import_node_path14.resolve)(p);
|
|
4031
|
+
const sf = this.project.getSourceFile(abs);
|
|
4032
|
+
if (sf) {
|
|
4033
|
+
await sf.refreshFromFileSystem();
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
const globbed = new Set(
|
|
4038
|
+
await (0, import_fast_glob2.default)(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
|
|
4039
|
+
);
|
|
4040
|
+
for (const f of globbed) {
|
|
4041
|
+
if (!this.controllerPaths.has(f)) {
|
|
4042
|
+
try {
|
|
4043
|
+
this.project.addSourceFileAtPath(f);
|
|
4044
|
+
this.controllerPaths.add(f);
|
|
4045
|
+
} catch {
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
for (const f of this.controllerPaths) {
|
|
4050
|
+
if (!globbed.has(f)) {
|
|
4051
|
+
const sf = this.project.getSourceFile(f);
|
|
4052
|
+
if (sf) this.project.removeSourceFile(sf);
|
|
4053
|
+
this.controllerPaths.delete(f);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return this.runExtraction();
|
|
4057
|
+
}
|
|
4058
|
+
/** Clear stale per-Project caches, then extract over the controller set. */
|
|
4059
|
+
runExtraction() {
|
|
4060
|
+
clearTypeResolutionCaches(this.project);
|
|
4061
|
+
clearEnumCache(this.project);
|
|
4062
|
+
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
4063
|
+
}
|
|
4064
|
+
};
|
|
3165
4065
|
function decoratorStringArg(decoratorExpr) {
|
|
3166
4066
|
if (!decoratorExpr) return void 0;
|
|
3167
4067
|
if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -3217,6 +4117,11 @@ function resolveVerb(method) {
|
|
|
3217
4117
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3218
4118
|
}
|
|
3219
4119
|
}
|
|
4120
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
4121
|
+
if (sseDecorator) {
|
|
4122
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
4123
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
4124
|
+
}
|
|
3220
4125
|
return null;
|
|
3221
4126
|
}
|
|
3222
4127
|
function readAsDecorator(node, label) {
|
|
@@ -3259,7 +4164,17 @@ function buildRoute(args) {
|
|
|
3259
4164
|
};
|
|
3260
4165
|
}
|
|
3261
4166
|
function extractContractRoute(args) {
|
|
3262
|
-
const {
|
|
4167
|
+
const {
|
|
4168
|
+
cls,
|
|
4169
|
+
method,
|
|
4170
|
+
applyContractDecorator,
|
|
4171
|
+
verb,
|
|
4172
|
+
prefix,
|
|
4173
|
+
className,
|
|
4174
|
+
sourceFile,
|
|
4175
|
+
project,
|
|
4176
|
+
seenNames
|
|
4177
|
+
} = args;
|
|
3263
4178
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3264
4179
|
if (!firstDecoratorArg) return null;
|
|
3265
4180
|
let contractDef = null;
|
|
@@ -3269,18 +4184,19 @@ function extractContractRoute(args) {
|
|
|
3269
4184
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3270
4185
|
} else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
|
|
3271
4186
|
const identName = firstDecoratorArg.getText();
|
|
3272
|
-
const
|
|
3273
|
-
if (!
|
|
4187
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
4188
|
+
if (!resolvedVar) {
|
|
3274
4189
|
console.warn(
|
|
3275
|
-
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()}
|
|
4190
|
+
`[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
|
|
3276
4191
|
);
|
|
3277
4192
|
return null;
|
|
3278
4193
|
}
|
|
4194
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
3279
4195
|
const initializer = varDecl.getInitializer();
|
|
3280
4196
|
if (!initializer) return null;
|
|
3281
4197
|
contractDef = parseDefineContractCall(initializer);
|
|
3282
4198
|
if (contractDef && varDecl.isExported()) {
|
|
3283
|
-
const filePath =
|
|
4199
|
+
const filePath = declFile.getFilePath();
|
|
3284
4200
|
if (contractDef.body !== null) {
|
|
3285
4201
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3286
4202
|
}
|
|
@@ -3313,6 +4229,7 @@ function extractContractRoute(args) {
|
|
|
3313
4229
|
query: contractDef.query,
|
|
3314
4230
|
body: contractDef.body,
|
|
3315
4231
|
response: contractDef.response,
|
|
4232
|
+
error: contractDef.error,
|
|
3316
4233
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3317
4234
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3318
4235
|
// would drag server-only deps into the client bundle).
|
|
@@ -3344,15 +4261,18 @@ function extractDtoRoute(args) {
|
|
|
3344
4261
|
query: dtoContract?.query ?? null,
|
|
3345
4262
|
body: dtoContract?.body ?? null,
|
|
3346
4263
|
response: dtoContract?.response ?? "unknown",
|
|
4264
|
+
error: dtoContract?.error ?? null,
|
|
3347
4265
|
queryRef: dtoContract?.queryRef ?? null,
|
|
3348
4266
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3349
4267
|
responseRef: dtoContract?.responseRef ?? null,
|
|
4268
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
3350
4269
|
filterFields: dtoContract?.filterFields ?? null,
|
|
3351
4270
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3352
4271
|
filterSource: dtoContract?.filterSource ?? null,
|
|
3353
4272
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3354
4273
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3355
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
4274
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
4275
|
+
stream: dtoContract?.stream ?? false
|
|
3356
4276
|
}
|
|
3357
4277
|
});
|
|
3358
4278
|
}
|
|
@@ -3376,6 +4296,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3376
4296
|
prefix,
|
|
3377
4297
|
className,
|
|
3378
4298
|
sourceFile,
|
|
4299
|
+
project,
|
|
3379
4300
|
seenNames
|
|
3380
4301
|
}) : extractDtoRoute({
|
|
3381
4302
|
cls,
|
|
@@ -3394,9 +4315,9 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3394
4315
|
}
|
|
3395
4316
|
|
|
3396
4317
|
// src/watch/lock-file.ts
|
|
3397
|
-
var
|
|
3398
|
-
var
|
|
3399
|
-
var
|
|
4318
|
+
var import_promises12 = require("fs/promises");
|
|
4319
|
+
var import_promises13 = require("fs/promises");
|
|
4320
|
+
var import_node_path15 = require("path");
|
|
3400
4321
|
var LOCK_FILE = ".watcher.lock";
|
|
3401
4322
|
function isProcessAlive(pid) {
|
|
3402
4323
|
try {
|
|
@@ -3407,21 +4328,21 @@ function isProcessAlive(pid) {
|
|
|
3407
4328
|
}
|
|
3408
4329
|
}
|
|
3409
4330
|
async function acquireLock(outDir) {
|
|
3410
|
-
await (0,
|
|
3411
|
-
const lockPath = (0,
|
|
4331
|
+
await (0, import_promises13.mkdir)(outDir, { recursive: true });
|
|
4332
|
+
const lockPath = (0, import_node_path15.join)(outDir, LOCK_FILE);
|
|
3412
4333
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3413
4334
|
try {
|
|
3414
|
-
const fd = await (0,
|
|
4335
|
+
const fd = await (0, import_promises12.open)(lockPath, "wx");
|
|
3415
4336
|
await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
|
|
3416
4337
|
`, "utf8");
|
|
3417
4338
|
await fd.close();
|
|
3418
4339
|
} catch (err) {
|
|
3419
4340
|
if (err.code === "EEXIST") {
|
|
3420
4341
|
try {
|
|
3421
|
-
const raw = await (0,
|
|
4342
|
+
const raw = await (0, import_promises13.readFile)(lockPath, "utf8");
|
|
3422
4343
|
const existing = JSON.parse(raw);
|
|
3423
4344
|
if (isProcessAlive(existing.pid)) return null;
|
|
3424
|
-
await (0,
|
|
4345
|
+
await (0, import_promises13.unlink)(lockPath);
|
|
3425
4346
|
return acquireLock(outDir);
|
|
3426
4347
|
} catch {
|
|
3427
4348
|
return null;
|
|
@@ -3432,7 +4353,7 @@ async function acquireLock(outDir) {
|
|
|
3432
4353
|
return {
|
|
3433
4354
|
release: async () => {
|
|
3434
4355
|
try {
|
|
3435
|
-
await (0,
|
|
4356
|
+
await (0, import_promises13.unlink)(lockPath);
|
|
3436
4357
|
} catch {
|
|
3437
4358
|
}
|
|
3438
4359
|
}
|
|
@@ -3448,7 +4369,7 @@ async function watch(config, onChange) {
|
|
|
3448
4369
|
if (lock === null) {
|
|
3449
4370
|
let holderPid = "unknown";
|
|
3450
4371
|
try {
|
|
3451
|
-
const raw = await (0,
|
|
4372
|
+
const raw = await (0, import_promises14.readFile)((0, import_node_path16.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3452
4373
|
const data = JSON.parse(raw);
|
|
3453
4374
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3454
4375
|
} catch {
|
|
@@ -3458,12 +4379,20 @@ async function watch(config, onChange) {
|
|
|
3458
4379
|
);
|
|
3459
4380
|
return NO_OP_WATCHER;
|
|
3460
4381
|
}
|
|
4382
|
+
let discovery = null;
|
|
4383
|
+
async function getDiscovery() {
|
|
4384
|
+
if (discovery === null) {
|
|
4385
|
+
discovery = await PersistentDiscovery.create({
|
|
4386
|
+
cwd: config.codegen.cwd,
|
|
4387
|
+
glob: config.contracts.glob,
|
|
4388
|
+
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
4389
|
+
});
|
|
4390
|
+
return discovery;
|
|
4391
|
+
}
|
|
4392
|
+
return discovery;
|
|
4393
|
+
}
|
|
3461
4394
|
try {
|
|
3462
|
-
const initialRoutes = await
|
|
3463
|
-
cwd: config.codegen.cwd,
|
|
3464
|
-
glob: config.contracts.glob,
|
|
3465
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3466
|
-
});
|
|
4395
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3467
4396
|
await generate(config, initialRoutes);
|
|
3468
4397
|
} catch (err) {
|
|
3469
4398
|
console.warn(
|
|
@@ -3476,7 +4405,7 @@ async function watch(config, onChange) {
|
|
|
3476
4405
|
}
|
|
3477
4406
|
let pagesDebounceTimer;
|
|
3478
4407
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3479
|
-
const pagesWatcher = import_chokidar.default.watch((0,
|
|
4408
|
+
const pagesWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, pagesGlob), {
|
|
3480
4409
|
ignoreInitial: true,
|
|
3481
4410
|
persistent: true,
|
|
3482
4411
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3502,23 +4431,23 @@ async function watch(config, onChange) {
|
|
|
3502
4431
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3503
4432
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3504
4433
|
let contractsDebounceTimer;
|
|
3505
|
-
const
|
|
4434
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4435
|
+
const contractsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.contracts.glob), {
|
|
3506
4436
|
ignoreInitial: true,
|
|
3507
4437
|
persistent: true,
|
|
3508
4438
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3509
4439
|
});
|
|
3510
|
-
function scheduleContractsRegenerate() {
|
|
4440
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4441
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3511
4442
|
if (contractsDebounceTimer !== void 0) {
|
|
3512
4443
|
clearTimeout(contractsDebounceTimer);
|
|
3513
4444
|
}
|
|
3514
4445
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3515
4446
|
contractsDebounceTimer = void 0;
|
|
4447
|
+
const changed = [...pendingChangedPaths];
|
|
4448
|
+
pendingChangedPaths.clear();
|
|
3516
4449
|
try {
|
|
3517
|
-
const routes = await
|
|
3518
|
-
cwd: config.codegen.cwd,
|
|
3519
|
-
glob: config.contracts.glob,
|
|
3520
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3521
|
-
});
|
|
4450
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3522
4451
|
await generate(config, routes);
|
|
3523
4452
|
} catch (err) {
|
|
3524
4453
|
console.error(
|
|
@@ -3529,17 +4458,17 @@ async function watch(config, onChange) {
|
|
|
3529
4458
|
onChange?.();
|
|
3530
4459
|
}, config.contracts.debounceMs);
|
|
3531
4460
|
}
|
|
3532
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3533
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3534
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3535
|
-
const formsWatcher = import_chokidar.default.watch((0,
|
|
4461
|
+
contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4462
|
+
contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4463
|
+
contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
4464
|
+
const formsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.forms.watch), {
|
|
3536
4465
|
ignoreInitial: true,
|
|
3537
4466
|
persistent: true,
|
|
3538
4467
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3539
4468
|
});
|
|
3540
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3541
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3542
|
-
formsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
4469
|
+
formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4470
|
+
formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4471
|
+
formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
3543
4472
|
return {
|
|
3544
4473
|
close: async () => {
|
|
3545
4474
|
if (pagesDebounceTimer !== void 0) {
|
|
@@ -3608,17 +4537,21 @@ function renderTsType(node, ctx) {
|
|
|
3608
4537
|
}
|
|
3609
4538
|
|
|
3610
4539
|
// src/index.ts
|
|
3611
|
-
var VERSION = "0.
|
|
4540
|
+
var VERSION = "0.5.1";
|
|
3612
4541
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3613
4542
|
0 && (module.exports = {
|
|
3614
4543
|
CodegenError,
|
|
3615
4544
|
ConfigError,
|
|
3616
4545
|
VERSION,
|
|
3617
4546
|
acquireLock,
|
|
4547
|
+
buildMocksFile,
|
|
4548
|
+
buildOpenApiSpec,
|
|
3618
4549
|
defineConfig,
|
|
3619
4550
|
discoverContractsFast,
|
|
3620
4551
|
emitApi,
|
|
3621
4552
|
emitForms,
|
|
4553
|
+
emitMocks,
|
|
4554
|
+
emitOpenApi,
|
|
3622
4555
|
emitRoutes,
|
|
3623
4556
|
extractSchemaFromDto,
|
|
3624
4557
|
generate,
|
|
@@ -3626,6 +4559,8 @@ var VERSION = "0.4.1";
|
|
|
3626
4559
|
renderTsType,
|
|
3627
4560
|
resolveAdapter,
|
|
3628
4561
|
resolveConfig,
|
|
4562
|
+
schemaModuleToJsonSchema,
|
|
4563
|
+
schemaNodeToJsonSchema,
|
|
3629
4564
|
watch
|
|
3630
4565
|
});
|
|
3631
4566
|
//# sourceMappingURL=index.cjs.map
|