@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/cli/main.js
CHANGED
|
@@ -126,6 +126,19 @@ function applyDefaults(userConfig, cwd) {
|
|
|
126
126
|
enabled: userConfig.forms?.enabled ?? true,
|
|
127
127
|
watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
|
|
128
128
|
zodImport: userConfig.forms?.zodImport ?? "zod"
|
|
129
|
+
},
|
|
130
|
+
openapi: {
|
|
131
|
+
enabled: userConfig.openapi?.enabled ?? false,
|
|
132
|
+
fileName: userConfig.openapi?.fileName ?? "openapi.json",
|
|
133
|
+
title: userConfig.openapi?.title ?? "NestJS API",
|
|
134
|
+
version: userConfig.openapi?.version ?? "1.0.0",
|
|
135
|
+
description: userConfig.openapi?.description ?? null
|
|
136
|
+
},
|
|
137
|
+
mocks: {
|
|
138
|
+
enabled: userConfig.mocks?.enabled ?? false,
|
|
139
|
+
fileName: userConfig.mocks?.fileName ?? "mocks.ts",
|
|
140
|
+
seed: userConfig.mocks?.seed ?? 1,
|
|
141
|
+
baseUrl: userConfig.mocks?.baseUrl ?? ""
|
|
129
142
|
}
|
|
130
143
|
};
|
|
131
144
|
}
|
|
@@ -163,8 +176,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
// src/generate.ts
|
|
166
|
-
import { mkdir as
|
|
167
|
-
import { dirname as dirname2, join as
|
|
179
|
+
import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
|
|
180
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
168
181
|
|
|
169
182
|
// src/discovery/pages.ts
|
|
170
183
|
import { readFile } from "fs/promises";
|
|
@@ -693,17 +706,28 @@ function emitFilterQueryType(c) {
|
|
|
693
706
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
694
707
|
}
|
|
695
708
|
function buildResponseType(c, outDir) {
|
|
709
|
+
const respRef = c.contractSource.responseRef;
|
|
710
|
+
if (c.contractSource.stream) {
|
|
711
|
+
if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
712
|
+
return c.contractSource.response;
|
|
713
|
+
}
|
|
696
714
|
if (c.controllerRef) {
|
|
697
715
|
let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
698
716
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
699
717
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
700
718
|
}
|
|
701
|
-
const respRef = c.contractSource.responseRef;
|
|
702
719
|
if (respRef) {
|
|
703
720
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
704
721
|
}
|
|
705
722
|
return c.contractSource.response;
|
|
706
723
|
}
|
|
724
|
+
function buildErrorType(c) {
|
|
725
|
+
const errRef = c.contractSource.errorRef;
|
|
726
|
+
if (errRef) {
|
|
727
|
+
return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
|
|
728
|
+
}
|
|
729
|
+
return c.contractSource.error ?? "unknown";
|
|
730
|
+
}
|
|
707
731
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
708
732
|
const pad = " ".repeat(indent);
|
|
709
733
|
const lines = [];
|
|
@@ -718,12 +742,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
718
742
|
const bodyRef = c.contractSource.bodyRef;
|
|
719
743
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
720
744
|
const response = buildResponseType(c, outDir);
|
|
745
|
+
const error = buildErrorType(c);
|
|
721
746
|
const params = buildParamsType(c.params);
|
|
722
747
|
const safeMethod = JSON.stringify(method);
|
|
723
748
|
const safeUrl = JSON.stringify(c.path);
|
|
724
749
|
const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
|
|
750
|
+
const stream = c.contractSource.stream ? "true" : "false";
|
|
725
751
|
lines.push(
|
|
726
|
-
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
|
|
752
|
+
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
|
|
727
753
|
);
|
|
728
754
|
} else {
|
|
729
755
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -795,15 +821,21 @@ function emitReqHelper() {
|
|
|
795
821
|
""
|
|
796
822
|
];
|
|
797
823
|
}
|
|
798
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
824
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
799
825
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
800
826
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
827
|
+
if (streamExpr) {
|
|
828
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
829
|
+
}
|
|
801
830
|
for (const [name, value] of Object.entries(members)) {
|
|
802
831
|
lines.push(`${pad} ${name}: ${value},`);
|
|
803
832
|
}
|
|
804
833
|
lines.push(`${pad}}),`);
|
|
805
834
|
return lines;
|
|
806
835
|
}
|
|
836
|
+
function renderStreamExpr(req) {
|
|
837
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
838
|
+
}
|
|
807
839
|
function emitApiObjectBlock(tree, indent, p) {
|
|
808
840
|
const pad = " ".repeat(indent);
|
|
809
841
|
const lines = [];
|
|
@@ -838,7 +870,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
838
870
|
}
|
|
839
871
|
const members = {};
|
|
840
872
|
for (const [name, { value }] of owned) members[name] = value;
|
|
841
|
-
|
|
873
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
874
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
842
875
|
}
|
|
843
876
|
return lines;
|
|
844
877
|
}
|
|
@@ -876,6 +909,8 @@ var ROUTE_NAMESPACE = [
|
|
|
876
909
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
877
910
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
878
911
|
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
912
|
+
" /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
|
|
913
|
+
' export type Stream<K extends string> = ResolveByName<K, "response">;',
|
|
879
914
|
" export type Request<K extends string> = {",
|
|
880
915
|
" body: Body<K>;",
|
|
881
916
|
" query: Query<K>;",
|
|
@@ -892,6 +927,7 @@ var PATH_NAMESPACE = [
|
|
|
892
927
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
893
928
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
894
929
|
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
930
|
+
' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
895
931
|
"}",
|
|
896
932
|
""
|
|
897
933
|
];
|
|
@@ -903,6 +939,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
903
939
|
" export type Params<K extends string> = never;",
|
|
904
940
|
" export type Error<K extends string> = never;",
|
|
905
941
|
" export type FilterFields<K extends string> = never;",
|
|
942
|
+
" export type Stream<K extends string> = never;",
|
|
906
943
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
907
944
|
"}",
|
|
908
945
|
""
|
|
@@ -915,6 +952,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
915
952
|
" export type Params<M extends string, U extends string> = never;",
|
|
916
953
|
" export type Error<M extends string, U extends string> = never;",
|
|
917
954
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
955
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
918
956
|
"}",
|
|
919
957
|
""
|
|
920
958
|
];
|
|
@@ -938,7 +976,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
938
976
|
for (const r of contracted) {
|
|
939
977
|
const cs = r.contract?.contractSource;
|
|
940
978
|
if (!cs) continue;
|
|
941
|
-
const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
|
|
979
|
+
const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
|
|
942
980
|
for (const ref of refs) {
|
|
943
981
|
if (!ref) continue;
|
|
944
982
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -1116,18 +1154,27 @@ function refRootIdentifier(refName) {
|
|
|
1116
1154
|
function hasSource(src) {
|
|
1117
1155
|
return !!(src.schema || src.zodText || src.zodRef);
|
|
1118
1156
|
}
|
|
1157
|
+
function escapeRegExp(s) {
|
|
1158
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1159
|
+
}
|
|
1160
|
+
var wordBoundaryRegexCache = /* @__PURE__ */ new Map();
|
|
1161
|
+
function wordBoundaryRegex(token) {
|
|
1162
|
+
let re = wordBoundaryRegexCache.get(token);
|
|
1163
|
+
if (re === void 0) {
|
|
1164
|
+
re = new RegExp(`\\b${escapeRegExp(token)}\\b`, "g");
|
|
1165
|
+
wordBoundaryRegexCache.set(token, re);
|
|
1166
|
+
}
|
|
1167
|
+
return re;
|
|
1168
|
+
}
|
|
1119
1169
|
function applyRenames(text, renames) {
|
|
1120
1170
|
if (!renames || renames.size === 0) return text;
|
|
1121
1171
|
let out = text;
|
|
1122
1172
|
for (const [from, to] of renames) {
|
|
1123
1173
|
if (from === to) continue;
|
|
1124
|
-
out = out.replace(
|
|
1174
|
+
out = out.replace(wordBoundaryRegex(from), to);
|
|
1125
1175
|
}
|
|
1126
1176
|
return out;
|
|
1127
1177
|
}
|
|
1128
|
-
function escapeRegExp(s) {
|
|
1129
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1130
|
-
}
|
|
1131
1178
|
function isSelfReferential(name, text) {
|
|
1132
1179
|
return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
|
|
1133
1180
|
}
|
|
@@ -1139,7 +1186,23 @@ function planNestedSchemas(entries) {
|
|
|
1139
1186
|
const local = Object.entries(entry.nestedSchemas);
|
|
1140
1187
|
if (local.length === 0) continue;
|
|
1141
1188
|
const rename = /* @__PURE__ */ new Map();
|
|
1142
|
-
|
|
1189
|
+
const renameValues = /* @__PURE__ */ new Set();
|
|
1190
|
+
const setRename = (key, value) => {
|
|
1191
|
+
const prev = rename.get(key);
|
|
1192
|
+
rename.set(key, value);
|
|
1193
|
+
if (prev !== void 0 && prev !== value) {
|
|
1194
|
+
let stillUsed = false;
|
|
1195
|
+
for (const v of rename.values()) {
|
|
1196
|
+
if (v === prev) {
|
|
1197
|
+
stillUsed = true;
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (!stillUsed) renameValues.delete(prev);
|
|
1202
|
+
}
|
|
1203
|
+
renameValues.add(value);
|
|
1204
|
+
};
|
|
1205
|
+
for (const [name] of local) setRename(name, name);
|
|
1143
1206
|
const textFor = (name) => {
|
|
1144
1207
|
const raw = entry.nestedSchemas?.[name] ?? "";
|
|
1145
1208
|
return applyRenames(raw, rename);
|
|
@@ -1157,11 +1220,11 @@ function planNestedSchemas(entries) {
|
|
|
1157
1220
|
if (existing === text) continue;
|
|
1158
1221
|
let i = 2;
|
|
1159
1222
|
let candidate = `${name}_${i}`;
|
|
1160
|
-
while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) ||
|
|
1223
|
+
while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || renameValues.has(candidate)) {
|
|
1161
1224
|
i += 1;
|
|
1162
1225
|
candidate = `${name}_${i}`;
|
|
1163
1226
|
}
|
|
1164
|
-
|
|
1227
|
+
setRename(name, candidate);
|
|
1165
1228
|
changed = true;
|
|
1166
1229
|
}
|
|
1167
1230
|
}
|
|
@@ -1371,11 +1434,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
1371
1434
|
await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
|
|
1372
1435
|
}
|
|
1373
1436
|
|
|
1374
|
-
// src/emit/emit-
|
|
1437
|
+
// src/emit/emit-mocks.ts
|
|
1375
1438
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1376
|
-
import { join as join8
|
|
1377
|
-
|
|
1439
|
+
import { join as join8 } from "path";
|
|
1440
|
+
|
|
1441
|
+
// src/ir/schema-node-to-json-schema.ts
|
|
1442
|
+
var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
|
|
1443
|
+
function parseLiteral(raw) {
|
|
1444
|
+
const t = raw.trim();
|
|
1445
|
+
if (t === "true") return true;
|
|
1446
|
+
if (t === "false") return false;
|
|
1447
|
+
if (t === "null") return null;
|
|
1448
|
+
const q = t[0];
|
|
1449
|
+
if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
|
|
1450
|
+
return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
|
|
1451
|
+
}
|
|
1452
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
|
|
1453
|
+
return Number(t);
|
|
1454
|
+
}
|
|
1455
|
+
return t;
|
|
1456
|
+
}
|
|
1457
|
+
function literalsType(values) {
|
|
1458
|
+
const types = new Set(values.map((v) => v === null ? "null" : typeof v));
|
|
1459
|
+
if (types.size === 1) {
|
|
1460
|
+
const only = [...types][0];
|
|
1461
|
+
if (only === "string") return "string";
|
|
1462
|
+
if (only === "number") return "number";
|
|
1463
|
+
if (only === "boolean") return "boolean";
|
|
1464
|
+
}
|
|
1465
|
+
return void 0;
|
|
1466
|
+
}
|
|
1467
|
+
function convert(node, ctx) {
|
|
1468
|
+
switch (node.kind) {
|
|
1469
|
+
case "string": {
|
|
1470
|
+
const out = { type: "string" };
|
|
1471
|
+
for (const c of node.checks) {
|
|
1472
|
+
if (c.check === "email") out.format = "email";
|
|
1473
|
+
else if (c.check === "url") out.format = "uri";
|
|
1474
|
+
else if (c.check === "uuid") out.format = "uuid";
|
|
1475
|
+
else if (c.check === "min") out.minLength = Number(c.value);
|
|
1476
|
+
else if (c.check === "max") out.maxLength = Number(c.value);
|
|
1477
|
+
else if (c.check === "regex") {
|
|
1478
|
+
const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
|
|
1479
|
+
out.pattern = m ? m[1] : c.pattern;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return out;
|
|
1483
|
+
}
|
|
1484
|
+
case "number": {
|
|
1485
|
+
const out = { type: "number" };
|
|
1486
|
+
for (const c of node.checks) {
|
|
1487
|
+
if (c.check === "int") out.type = "integer";
|
|
1488
|
+
else if (c.check === "min") out.minimum = Number(c.value);
|
|
1489
|
+
else if (c.check === "max") out.maximum = Number(c.value);
|
|
1490
|
+
else if (c.check === "positive") out.exclusiveMinimum = 0;
|
|
1491
|
+
else if (c.check === "negative") out.exclusiveMaximum = 0;
|
|
1492
|
+
}
|
|
1493
|
+
return out;
|
|
1494
|
+
}
|
|
1495
|
+
case "boolean":
|
|
1496
|
+
return { type: "boolean" };
|
|
1497
|
+
case "date":
|
|
1498
|
+
return { type: "string", format: "date-time" };
|
|
1499
|
+
case "unknown":
|
|
1500
|
+
return node.note ? { description: node.note } : {};
|
|
1501
|
+
case "instanceof":
|
|
1502
|
+
return { type: "object", description: `instanceof ${node.ctor}` };
|
|
1503
|
+
case "enum": {
|
|
1504
|
+
const values = node.literals.map(parseLiteral);
|
|
1505
|
+
const t = literalsType(values);
|
|
1506
|
+
const out = { enum: values };
|
|
1507
|
+
if (t) out.type = t;
|
|
1508
|
+
return out;
|
|
1509
|
+
}
|
|
1510
|
+
case "literal": {
|
|
1511
|
+
const value = parseLiteral(node.raw);
|
|
1512
|
+
const out = { const: value };
|
|
1513
|
+
const t = literalsType([value]);
|
|
1514
|
+
if (t) out.type = t;
|
|
1515
|
+
return out;
|
|
1516
|
+
}
|
|
1517
|
+
case "union": {
|
|
1518
|
+
const options = node.options.map((o) => convert(o, ctx));
|
|
1519
|
+
const out = { oneOf: options };
|
|
1520
|
+
if (node.discriminator) {
|
|
1521
|
+
out.discriminator = { propertyName: node.discriminator };
|
|
1522
|
+
}
|
|
1523
|
+
return out;
|
|
1524
|
+
}
|
|
1525
|
+
case "object": {
|
|
1526
|
+
const properties = {};
|
|
1527
|
+
const required = [];
|
|
1528
|
+
for (const f of node.fields) {
|
|
1529
|
+
if (f.value.kind === "optional") {
|
|
1530
|
+
properties[f.key] = convert(f.value.inner, ctx);
|
|
1531
|
+
} else {
|
|
1532
|
+
properties[f.key] = convert(f.value, ctx);
|
|
1533
|
+
required.push(f.key);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const out = { type: "object", properties };
|
|
1537
|
+
if (required.length > 0) out.required = required;
|
|
1538
|
+
out.additionalProperties = node.passthrough;
|
|
1539
|
+
return out;
|
|
1540
|
+
}
|
|
1541
|
+
case "array":
|
|
1542
|
+
return { type: "array", items: convert(node.element, ctx) };
|
|
1543
|
+
case "optional":
|
|
1544
|
+
return widenNullable(convert(node.inner, ctx));
|
|
1545
|
+
case "ref":
|
|
1546
|
+
case "lazyRef":
|
|
1547
|
+
return { $ref: `${ctx.refPrefix}${node.name}` };
|
|
1548
|
+
case "annotated":
|
|
1549
|
+
return convert(node.inner, ctx);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
function widenNullable(schema) {
|
|
1553
|
+
if (schema.$ref) {
|
|
1554
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1555
|
+
}
|
|
1556
|
+
if (typeof schema.type === "string") {
|
|
1557
|
+
return { ...schema, type: [schema.type, "null"] };
|
|
1558
|
+
}
|
|
1559
|
+
if (Array.isArray(schema.type)) {
|
|
1560
|
+
return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
|
|
1561
|
+
}
|
|
1562
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1563
|
+
}
|
|
1564
|
+
function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
|
|
1565
|
+
const named = {};
|
|
1566
|
+
for (const [name, node] of mod.named) {
|
|
1567
|
+
named[name] = convert(node, ctx);
|
|
1568
|
+
}
|
|
1569
|
+
return { root: convert(mod.root, ctx), named };
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/emit/mock-gen-runtime.ts
|
|
1573
|
+
var MOCK_GEN_RUNTIME = `
|
|
1574
|
+
/** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
|
|
1575
|
+
function makeRng(seed) {
|
|
1576
|
+
let a = seed >>> 0;
|
|
1577
|
+
return {
|
|
1578
|
+
next() {
|
|
1579
|
+
a |= 0;
|
|
1580
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
1581
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
1582
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
1583
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1584
|
+
},
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function __pick(rng, items) {
|
|
1589
|
+
return items[Math.floor(rng.next() * items.length)];
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function __intBetween(rng, min, max) {
|
|
1593
|
+
return Math.floor(rng.next() * (max - min + 1)) + min;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
|
|
1597
|
+
const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
|
|
1598
|
+
const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
|
|
1599
|
+
|
|
1600
|
+
function __fakeWords(rng, count) {
|
|
1601
|
+
let out = [];
|
|
1602
|
+
for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
|
|
1603
|
+
return out.join(' ');
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function __hex(rng, len) {
|
|
1607
|
+
let s = '';
|
|
1608
|
+
for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
|
|
1609
|
+
return s;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function __fakeUuid(rng) {
|
|
1613
|
+
return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function __fakeString(rng, schema) {
|
|
1617
|
+
switch (schema.format) {
|
|
1618
|
+
case 'email':
|
|
1619
|
+
return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
|
|
1620
|
+
case 'uri':
|
|
1621
|
+
case 'url':
|
|
1622
|
+
return 'https://example.com/' + __pick(rng, __WORDS);
|
|
1623
|
+
case 'uuid':
|
|
1624
|
+
return __fakeUuid(rng);
|
|
1625
|
+
case 'date-time':
|
|
1626
|
+
return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
|
|
1627
|
+
default:
|
|
1628
|
+
return __fakeWords(rng, __intBetween(rng, 1, 3));
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
|
|
1633
|
+
function generateMock(schema, rng, defs, depth) {
|
|
1634
|
+
defs = defs || {};
|
|
1635
|
+
depth = depth || 0;
|
|
1636
|
+
if (schema.$ref) {
|
|
1637
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
1638
|
+
const target = defs[name];
|
|
1639
|
+
if (!target || depth > 4) return null;
|
|
1640
|
+
return generateMock(target, rng, defs, depth + 1);
|
|
1641
|
+
}
|
|
1642
|
+
if ('const' in schema) return schema.const;
|
|
1643
|
+
if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
|
|
1644
|
+
if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
|
|
1645
|
+
if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
|
|
1646
|
+
let type = Array.isArray(schema.type)
|
|
1647
|
+
? (schema.type.filter((t) => t !== 'null')[0] || 'null')
|
|
1648
|
+
: schema.type;
|
|
1649
|
+
switch (type) {
|
|
1650
|
+
case 'string':
|
|
1651
|
+
return __fakeString(rng, schema);
|
|
1652
|
+
case 'integer':
|
|
1653
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
|
|
1654
|
+
case 'number':
|
|
1655
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
|
|
1656
|
+
case 'boolean':
|
|
1657
|
+
return rng.next() < 0.5;
|
|
1658
|
+
case 'null':
|
|
1659
|
+
return null;
|
|
1660
|
+
case 'array': {
|
|
1661
|
+
const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
|
|
1662
|
+
const items = schema.items || {};
|
|
1663
|
+
let arr = [];
|
|
1664
|
+
for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
|
|
1665
|
+
return arr;
|
|
1666
|
+
}
|
|
1667
|
+
case 'object': {
|
|
1668
|
+
const out = {};
|
|
1669
|
+
const props = schema.properties || {};
|
|
1670
|
+
for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
|
|
1671
|
+
return out;
|
|
1672
|
+
}
|
|
1673
|
+
default:
|
|
1674
|
+
return {};
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
`.trim();
|
|
1678
|
+
|
|
1679
|
+
// src/emit/emit-mocks.ts
|
|
1680
|
+
var REF_PREFIX = "#/components/schemas/";
|
|
1681
|
+
function toMswPath(path, baseUrl) {
|
|
1682
|
+
return `${baseUrl}${path}`;
|
|
1683
|
+
}
|
|
1684
|
+
function responseSchemaFor(route, defs) {
|
|
1685
|
+
const cs = route.contract.contractSource;
|
|
1686
|
+
if (cs.responseSchema) {
|
|
1687
|
+
const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
|
|
1688
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1689
|
+
if (!(name in defs)) defs[name] = node;
|
|
1690
|
+
}
|
|
1691
|
+
return root;
|
|
1692
|
+
}
|
|
1693
|
+
return {};
|
|
1694
|
+
}
|
|
1695
|
+
function buildMocksFile(routes, opts = {}) {
|
|
1696
|
+
const seed = opts.seed ?? 1;
|
|
1697
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
1698
|
+
const contracted = routes.filter((r) => r.contract);
|
|
1699
|
+
const defs = {};
|
|
1700
|
+
const handlers = [];
|
|
1701
|
+
for (const r of contracted) {
|
|
1702
|
+
const schema = responseSchemaFor(r, defs);
|
|
1703
|
+
const method = r.method.toLowerCase();
|
|
1704
|
+
const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
|
|
1705
|
+
const path = toMswPath(r.path, baseUrl);
|
|
1706
|
+
const cs = r.contract.contractSource;
|
|
1707
|
+
const schemaLiteral = JSON.stringify(schema);
|
|
1708
|
+
const pathLit = JSON.stringify(path);
|
|
1709
|
+
if (cs.stream) {
|
|
1710
|
+
handlers.push(
|
|
1711
|
+
[
|
|
1712
|
+
` // ${r.name} (stream)`,
|
|
1713
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1714
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1715
|
+
" const body = `data: ${JSON.stringify(value)}\\n\\n`;",
|
|
1716
|
+
" return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
|
|
1717
|
+
" }),"
|
|
1718
|
+
].join("\n")
|
|
1719
|
+
);
|
|
1720
|
+
} else {
|
|
1721
|
+
handlers.push(
|
|
1722
|
+
[
|
|
1723
|
+
` // ${r.name}`,
|
|
1724
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1725
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1726
|
+
" return HttpResponse.json(value);",
|
|
1727
|
+
" }),"
|
|
1728
|
+
].join("\n")
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
const lines = [
|
|
1733
|
+
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1734
|
+
"// MSW handlers returning deterministic, schema-shaped mock data.",
|
|
1735
|
+
"/* eslint-disable */",
|
|
1736
|
+
"// @ts-nocheck",
|
|
1737
|
+
"",
|
|
1738
|
+
"import { http, HttpResponse } from 'msw';",
|
|
1739
|
+
"",
|
|
1740
|
+
`const SEED = ${seed};`,
|
|
1741
|
+
"",
|
|
1742
|
+
"// ---------------------------------------------------------------------------",
|
|
1743
|
+
"// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
|
|
1744
|
+
"// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
|
|
1745
|
+
"// ---------------------------------------------------------------------------",
|
|
1746
|
+
MOCK_GEN_RUNTIME,
|
|
1747
|
+
"",
|
|
1748
|
+
"// Shared component schemas referenced by $ref.",
|
|
1749
|
+
`const DEFS = ${JSON.stringify(defs, null, 2)};`,
|
|
1750
|
+
"",
|
|
1751
|
+
"/** MSW request handlers, one per contracted route. */",
|
|
1752
|
+
"export const handlers = [",
|
|
1753
|
+
...handlers,
|
|
1754
|
+
"];",
|
|
1755
|
+
""
|
|
1756
|
+
];
|
|
1757
|
+
return lines.join("\n");
|
|
1758
|
+
}
|
|
1759
|
+
async function emitMocks(routes, outDir, opts = {}) {
|
|
1378
1760
|
await mkdir5(outDir, { recursive: true });
|
|
1761
|
+
const content = buildMocksFile(routes, opts);
|
|
1762
|
+
const fileName = opts.fileName ?? "mocks.ts";
|
|
1763
|
+
await writeFile5(join8(outDir, fileName), content, "utf8");
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// src/emit/emit-openapi.ts
|
|
1767
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1768
|
+
import { join as join9 } from "path";
|
|
1769
|
+
var REF_PREFIX2 = "#/components/schemas/";
|
|
1770
|
+
function toOpenApiPath(path) {
|
|
1771
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
1772
|
+
}
|
|
1773
|
+
function positionSchema(schema, tsType, components) {
|
|
1774
|
+
if (schema) {
|
|
1775
|
+
const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
|
|
1776
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1777
|
+
if (!(name in components)) components[name] = node;
|
|
1778
|
+
}
|
|
1779
|
+
return root;
|
|
1780
|
+
}
|
|
1781
|
+
return tsType ? { description: tsType } : {};
|
|
1782
|
+
}
|
|
1783
|
+
function buildParameters(route) {
|
|
1784
|
+
const params = [];
|
|
1785
|
+
for (const p of route.params) {
|
|
1786
|
+
if (p.source === "path") {
|
|
1787
|
+
params.push({
|
|
1788
|
+
name: p.name,
|
|
1789
|
+
in: "path",
|
|
1790
|
+
required: true,
|
|
1791
|
+
schema: { type: "string" }
|
|
1792
|
+
});
|
|
1793
|
+
} else if (p.source === "query") {
|
|
1794
|
+
params.push({
|
|
1795
|
+
name: p.name,
|
|
1796
|
+
in: "query",
|
|
1797
|
+
required: false,
|
|
1798
|
+
schema: { type: "string" }
|
|
1799
|
+
});
|
|
1800
|
+
} else if (p.source === "header") {
|
|
1801
|
+
params.push({
|
|
1802
|
+
name: p.name,
|
|
1803
|
+
in: "header",
|
|
1804
|
+
required: false,
|
|
1805
|
+
schema: { type: "string" }
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return params;
|
|
1810
|
+
}
|
|
1811
|
+
function buildResponses(cs, components) {
|
|
1812
|
+
const responses = {};
|
|
1813
|
+
const successSchema = positionSchema(
|
|
1814
|
+
// Prefer rich response IR when present; otherwise fall back to the TS type.
|
|
1815
|
+
cs.responseSchema ?? null,
|
|
1816
|
+
cs.response,
|
|
1817
|
+
components
|
|
1818
|
+
);
|
|
1819
|
+
const successContentType = cs.stream ? "text/event-stream" : "application/json";
|
|
1820
|
+
responses["200"] = {
|
|
1821
|
+
description: cs.stream ? "Server-sent event stream" : "Successful response",
|
|
1822
|
+
content: { [successContentType]: { schema: successSchema } }
|
|
1823
|
+
};
|
|
1824
|
+
const errorSchema = positionSchema(null, cs.error ?? null, components);
|
|
1825
|
+
const errorBody = {
|
|
1826
|
+
description: "Error response",
|
|
1827
|
+
content: { "application/json": { schema: errorSchema } }
|
|
1828
|
+
};
|
|
1829
|
+
if (cs.error || cs.errorRef) {
|
|
1830
|
+
responses["400"] = errorBody;
|
|
1831
|
+
responses.default = errorBody;
|
|
1832
|
+
} else {
|
|
1833
|
+
responses.default = {
|
|
1834
|
+
description: "Error response",
|
|
1835
|
+
content: { "application/json": { schema: {} } }
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
return responses;
|
|
1839
|
+
}
|
|
1840
|
+
function buildOperation(route, components) {
|
|
1841
|
+
const cs = route.contract.contractSource;
|
|
1842
|
+
const op = {
|
|
1843
|
+
operationId: route.name,
|
|
1844
|
+
parameters: buildParameters(route),
|
|
1845
|
+
responses: buildResponses(cs, components)
|
|
1846
|
+
};
|
|
1847
|
+
const method = route.method.toUpperCase();
|
|
1848
|
+
const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
|
|
1849
|
+
if (hasBody && (cs.bodySchema || cs.body)) {
|
|
1850
|
+
const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
|
|
1851
|
+
op.requestBody = {
|
|
1852
|
+
required: true,
|
|
1853
|
+
content: { "application/json": { schema: bodySchema } }
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
return op;
|
|
1857
|
+
}
|
|
1858
|
+
function buildOpenApiSpec(routes, opts = {}) {
|
|
1859
|
+
const components = {};
|
|
1860
|
+
const paths = {};
|
|
1861
|
+
for (const route of routes) {
|
|
1862
|
+
if (!route.contract) continue;
|
|
1863
|
+
const oaPath = toOpenApiPath(route.path);
|
|
1864
|
+
const method = route.method.toLowerCase();
|
|
1865
|
+
let pathItem = paths[oaPath];
|
|
1866
|
+
if (!pathItem) {
|
|
1867
|
+
pathItem = {};
|
|
1868
|
+
paths[oaPath] = pathItem;
|
|
1869
|
+
}
|
|
1870
|
+
pathItem[method] = buildOperation(route, components);
|
|
1871
|
+
}
|
|
1872
|
+
const info = opts.info ?? {};
|
|
1873
|
+
const doc = {
|
|
1874
|
+
openapi: "3.1.0",
|
|
1875
|
+
info: {
|
|
1876
|
+
title: info.title ?? "NestJS API",
|
|
1877
|
+
version: info.version ?? "1.0.0",
|
|
1878
|
+
...info.description ? { description: info.description } : {}
|
|
1879
|
+
},
|
|
1880
|
+
paths,
|
|
1881
|
+
components: { schemas: components }
|
|
1882
|
+
};
|
|
1883
|
+
return doc;
|
|
1884
|
+
}
|
|
1885
|
+
async function emitOpenApi(routes, outDir, opts = {}) {
|
|
1886
|
+
await mkdir6(outDir, { recursive: true });
|
|
1887
|
+
const doc = buildOpenApiSpec(routes, opts);
|
|
1888
|
+
const fileName = opts.fileName ?? "openapi.json";
|
|
1889
|
+
await writeFile6(join9(outDir, fileName), `${JSON.stringify(doc, null, 2)}
|
|
1890
|
+
`, "utf8");
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// src/emit/emit-pages.ts
|
|
1894
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
1895
|
+
import { join as join10, relative as relative5 } from "path";
|
|
1896
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
1897
|
+
await mkdir7(outDir, { recursive: true });
|
|
1379
1898
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
1380
1899
|
const augBody = pages.map((p) => {
|
|
1381
1900
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -1394,7 +1913,7 @@ ${augBody}
|
|
|
1394
1913
|
}
|
|
1395
1914
|
${sharedPropsBlock}}
|
|
1396
1915
|
`;
|
|
1397
|
-
await
|
|
1916
|
+
await writeFile7(join10(outDir, "pages.d.ts"), content, "utf8");
|
|
1398
1917
|
}
|
|
1399
1918
|
function buildSharedPropsBlock(sharedProps) {
|
|
1400
1919
|
if (!sharedProps) return "";
|
|
@@ -1424,12 +1943,12 @@ function needsQuotes(name) {
|
|
|
1424
1943
|
}
|
|
1425
1944
|
|
|
1426
1945
|
// src/emit/emit-routes.ts
|
|
1427
|
-
import { mkdir as
|
|
1428
|
-
import { join as
|
|
1946
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
1947
|
+
import { join as join11 } from "path";
|
|
1429
1948
|
async function emitRoutes(routes, outDir) {
|
|
1430
|
-
await
|
|
1949
|
+
await mkdir8(outDir, { recursive: true });
|
|
1431
1950
|
const content = buildRoutesFile(routes);
|
|
1432
|
-
await
|
|
1951
|
+
await writeFile8(join11(outDir, "routes.ts"), content, "utf8");
|
|
1433
1952
|
}
|
|
1434
1953
|
function buildRoutesFile(routes) {
|
|
1435
1954
|
if (routes.length === 0) {
|
|
@@ -1577,24 +2096,41 @@ async function generate(config, inputRoutes = []) {
|
|
|
1577
2096
|
});
|
|
1578
2097
|
}
|
|
1579
2098
|
const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
|
|
2099
|
+
if (hasContracts && config.openapi.enabled) {
|
|
2100
|
+
await emitOpenApi(routes, config.codegen.outDir, {
|
|
2101
|
+
fileName: config.openapi.fileName,
|
|
2102
|
+
info: {
|
|
2103
|
+
title: config.openapi.title,
|
|
2104
|
+
version: config.openapi.version,
|
|
2105
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
if (hasContracts && config.mocks.enabled) {
|
|
2110
|
+
await emitMocks(routes, config.codegen.outDir, {
|
|
2111
|
+
fileName: config.mocks.fileName,
|
|
2112
|
+
seed: config.mocks.seed,
|
|
2113
|
+
baseUrl: config.mocks.baseUrl
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
1580
2116
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
1581
2117
|
if (extensions.length > 0) {
|
|
1582
2118
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1583
2119
|
for (const file of extraFiles) {
|
|
1584
|
-
const dest =
|
|
1585
|
-
await
|
|
1586
|
-
await
|
|
2120
|
+
const dest = join12(config.codegen.outDir, file.path);
|
|
2121
|
+
await mkdir9(dirname2(dest), { recursive: true });
|
|
2122
|
+
await writeFile9(dest, file.contents, "utf8");
|
|
1587
2123
|
}
|
|
1588
2124
|
}
|
|
1589
2125
|
}
|
|
1590
2126
|
|
|
1591
2127
|
// src/watch/watcher.ts
|
|
1592
2128
|
import { readFile as readFile3 } from "fs/promises";
|
|
1593
|
-
import { join as
|
|
2129
|
+
import { join as join15 } from "path";
|
|
1594
2130
|
import chokidar from "chokidar";
|
|
1595
2131
|
|
|
1596
2132
|
// src/discovery/contracts-fast.ts
|
|
1597
|
-
import { join as
|
|
2133
|
+
import { join as join13, resolve as resolve3 } from "path";
|
|
1598
2134
|
import fg2 from "fast-glob";
|
|
1599
2135
|
import {
|
|
1600
2136
|
Node as Node8,
|
|
@@ -1776,7 +2312,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
|
|
|
1776
2312
|
}
|
|
1777
2313
|
return null;
|
|
1778
2314
|
}
|
|
2315
|
+
function resolveImportedVariable(name, sourceFile, project) {
|
|
2316
|
+
const local = sourceFile.getVariableDeclaration(name);
|
|
2317
|
+
if (local) return { decl: local, file: sourceFile };
|
|
2318
|
+
return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
|
|
2319
|
+
}
|
|
2320
|
+
function resolveVariableViaImports(name, sourceFile, project, seen) {
|
|
2321
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
2322
|
+
const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
|
|
2323
|
+
if (!namedImport) continue;
|
|
2324
|
+
const sourceName = namedImport.getName();
|
|
2325
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
2326
|
+
const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
|
|
2327
|
+
if (found) return found;
|
|
2328
|
+
}
|
|
2329
|
+
return null;
|
|
2330
|
+
}
|
|
2331
|
+
function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
|
|
2332
|
+
const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
|
|
2333
|
+
for (const candidate of candidates) {
|
|
2334
|
+
let importedFile = project.getSourceFile(candidate);
|
|
2335
|
+
if (!importedFile) {
|
|
2336
|
+
try {
|
|
2337
|
+
importedFile = project.addSourceFileAtPath(candidate);
|
|
2338
|
+
} catch {
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
const found = resolveVariableInFile(name, importedFile, project, seen);
|
|
2343
|
+
if (found) return found;
|
|
2344
|
+
}
|
|
2345
|
+
return null;
|
|
2346
|
+
}
|
|
2347
|
+
function resolveVariableInFile(name, file, project, seen) {
|
|
2348
|
+
const filePath = file.getFilePath();
|
|
2349
|
+
if (seen.has(filePath)) return null;
|
|
2350
|
+
seen.add(filePath);
|
|
2351
|
+
const local = file.getVariableDeclaration(name);
|
|
2352
|
+
if (local) return { decl: local, file };
|
|
2353
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
2354
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
2355
|
+
const namedExports = exportDecl.getNamedExports();
|
|
2356
|
+
if (moduleSpecifier) {
|
|
2357
|
+
const hasStar = namedExports.length === 0;
|
|
2358
|
+
const reExport2 = namedExports.find(
|
|
2359
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2360
|
+
);
|
|
2361
|
+
if (!hasStar && !reExport2) continue;
|
|
2362
|
+
const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
|
|
2363
|
+
const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
|
|
2364
|
+
if (found) return found;
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2367
|
+
const reExport = namedExports.find(
|
|
2368
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2369
|
+
);
|
|
2370
|
+
if (!reExport) continue;
|
|
2371
|
+
const sourceName = reExport.getName();
|
|
2372
|
+
const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
|
|
2373
|
+
if (viaImports) return viaImports;
|
|
2374
|
+
}
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
1779
2377
|
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
2378
|
+
function clearTypeResolutionCaches(project) {
|
|
2379
|
+
_findTypeCache.delete(project);
|
|
2380
|
+
_resolveNamedRefCache.delete(project);
|
|
2381
|
+
}
|
|
1780
2382
|
function findType(name, sourceFile, project) {
|
|
1781
2383
|
let byKey = _findTypeCache.get(project);
|
|
1782
2384
|
if (byKey === void 0) {
|
|
@@ -1917,9 +2519,11 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
1917
2519
|
warnings: [],
|
|
1918
2520
|
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
1919
2521
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
2522
|
+
usedSchemaNames: /* @__PURE__ */ new Set(),
|
|
1920
2523
|
visiting: /* @__PURE__ */ new Set(),
|
|
1921
2524
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
1922
|
-
depth: 0
|
|
2525
|
+
depth: 0,
|
|
2526
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
1923
2527
|
};
|
|
1924
2528
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
1925
2529
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -1943,11 +2547,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
1943
2547
|
const typeNode = prop.getTypeNode();
|
|
1944
2548
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
1945
2549
|
const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
|
|
2550
|
+
const discriminator = resolveDiscriminator(dec("Type"));
|
|
2551
|
+
if (discriminator) {
|
|
2552
|
+
const options = discriminator.subTypes.map(
|
|
2553
|
+
(name) => buildNestedReference(name, classFile, ctx)
|
|
2554
|
+
);
|
|
2555
|
+
const unionNode = {
|
|
2556
|
+
kind: "union",
|
|
2557
|
+
options,
|
|
2558
|
+
discriminator: discriminator.property
|
|
2559
|
+
};
|
|
2560
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2561
|
+
const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
|
|
2562
|
+
return applyPresence(node2, decorators);
|
|
2563
|
+
}
|
|
2564
|
+
const propTypeParam = singularClassName(typeText);
|
|
2565
|
+
if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
|
|
2566
|
+
const bound = ctx.typeBindings.get(propTypeParam);
|
|
2567
|
+
const childNode = buildNestedReference(bound, classFile, ctx);
|
|
2568
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2569
|
+
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
2570
|
+
return applyPresence(node2, decorators);
|
|
2571
|
+
}
|
|
1946
2572
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
1947
2573
|
if (has("ValidateNested") || typeRefName) {
|
|
2574
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
1948
2575
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
1949
2576
|
if (childName) {
|
|
1950
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
2577
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
1951
2578
|
const wrapArray = has("IsArray") || isArrayType;
|
|
1952
2579
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
1953
2580
|
return applyPresence(node2, decorators);
|
|
@@ -2072,10 +2699,13 @@ function baseFromType(typeText, isArrayType) {
|
|
|
2072
2699
|
return { kind: "unknown" };
|
|
2073
2700
|
}
|
|
2074
2701
|
}
|
|
2075
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2702
|
+
function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
|
|
2703
|
+
const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
|
|
2704
|
+
const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
|
|
2705
|
+
if (ctx.visiting.has(cacheKey)) {
|
|
2706
|
+
const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
|
|
2707
|
+
ctx.emittedClasses.set(cacheKey, reserved);
|
|
2708
|
+
ctx.usedSchemaNames.add(reserved);
|
|
2079
2709
|
ctx.recursiveSchemas.add(reserved);
|
|
2080
2710
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2081
2711
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -2094,29 +2724,37 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
2094
2724
|
}
|
|
2095
2725
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
2096
2726
|
}
|
|
2097
|
-
const existing = ctx.emittedClasses.get(
|
|
2727
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
2098
2728
|
if (existing) return { kind: "ref", name: existing };
|
|
2099
|
-
const schemaName = aliasFor(
|
|
2729
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
2100
2730
|
const resolved = findType(className, fromFile, ctx.project);
|
|
2101
2731
|
if (!resolved || resolved.kind !== "class") {
|
|
2102
2732
|
return { kind: "object", fields: [], passthrough: true };
|
|
2103
2733
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2734
|
+
const params = resolved.decl.getTypeParameters().map((p) => p.getName());
|
|
2735
|
+
const newBindings = [];
|
|
2736
|
+
params.forEach((param, i) => {
|
|
2737
|
+
const arg = typeArgs[i];
|
|
2738
|
+
if (arg) newBindings.push([param, arg]);
|
|
2739
|
+
});
|
|
2740
|
+
for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
|
|
2741
|
+
ctx.emittedClasses.set(cacheKey, schemaName);
|
|
2742
|
+
ctx.usedSchemaNames.add(schemaName);
|
|
2743
|
+
ctx.visiting.add(cacheKey);
|
|
2106
2744
|
ctx.depth += 1;
|
|
2107
2745
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
2108
2746
|
ctx.depth -= 1;
|
|
2109
|
-
ctx.visiting.delete(
|
|
2747
|
+
ctx.visiting.delete(cacheKey);
|
|
2748
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
2110
2749
|
ctx.named.set(schemaName, childNode);
|
|
2750
|
+
ctx.usedSchemaNames.add(schemaName);
|
|
2111
2751
|
return { kind: "ref", name: schemaName };
|
|
2112
2752
|
}
|
|
2113
2753
|
function aliasFor(className, ctx) {
|
|
2114
2754
|
const baseName = `${className}Schema`;
|
|
2115
2755
|
let candidate = baseName;
|
|
2116
2756
|
let i = 1;
|
|
2117
|
-
|
|
2118
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
2119
|
-
while (used.has(candidate)) {
|
|
2757
|
+
while (ctx.usedSchemaNames.has(candidate)) {
|
|
2120
2758
|
candidate = `${baseName}_${i}`;
|
|
2121
2759
|
i += 1;
|
|
2122
2760
|
}
|
|
@@ -2153,6 +2791,39 @@ function messageRaw(decorator) {
|
|
|
2153
2791
|
}
|
|
2154
2792
|
return void 0;
|
|
2155
2793
|
}
|
|
2794
|
+
function resolveDiscriminator(decorator) {
|
|
2795
|
+
const optsArg = decorator?.getArguments()[1];
|
|
2796
|
+
if (!optsArg || !Node3.isObjectLiteralExpression(optsArg)) return null;
|
|
2797
|
+
let discProp;
|
|
2798
|
+
for (const prop of optsArg.getProperties()) {
|
|
2799
|
+
if (Node3.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
|
|
2800
|
+
discProp = prop.getInitializer();
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
if (!discProp || !Node3.isObjectLiteralExpression(discProp)) return null;
|
|
2804
|
+
let property = null;
|
|
2805
|
+
const subTypes = [];
|
|
2806
|
+
for (const prop of discProp.getProperties()) {
|
|
2807
|
+
if (!Node3.isPropertyAssignment(prop)) continue;
|
|
2808
|
+
const name = prop.getName();
|
|
2809
|
+
const init = prop.getInitializer();
|
|
2810
|
+
if (!init) continue;
|
|
2811
|
+
if (name === "property" && Node3.isStringLiteral(init)) {
|
|
2812
|
+
property = init.getLiteralValue();
|
|
2813
|
+
} else if (name === "subTypes" && Node3.isArrayLiteralExpression(init)) {
|
|
2814
|
+
for (const el of init.getElements()) {
|
|
2815
|
+
if (!Node3.isObjectLiteralExpression(el)) continue;
|
|
2816
|
+
for (const p of el.getProperties()) {
|
|
2817
|
+
if (!Node3.isPropertyAssignment(p) || p.getName() !== "name") continue;
|
|
2818
|
+
const nameInit = p.getInitializer();
|
|
2819
|
+
if (nameInit && Node3.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
if (!property || subTypes.length === 0) return null;
|
|
2825
|
+
return { property, subTypes };
|
|
2826
|
+
}
|
|
2156
2827
|
function resolveTypeFactoryName(decorator) {
|
|
2157
2828
|
const arg = firstArg(decorator);
|
|
2158
2829
|
if (!arg) return null;
|
|
@@ -2166,6 +2837,17 @@ function singularClassName(typeText) {
|
|
|
2166
2837
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2167
2838
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2168
2839
|
}
|
|
2840
|
+
function genericTypeArgNames(typeNode) {
|
|
2841
|
+
if (!typeNode || !Node3.isTypeReference(typeNode)) return [];
|
|
2842
|
+
const names = [];
|
|
2843
|
+
for (const arg of typeNode.getTypeArguments()) {
|
|
2844
|
+
if (!Node3.isTypeReference(arg)) return [];
|
|
2845
|
+
const tn = arg.getTypeName();
|
|
2846
|
+
if (!Node3.isIdentifier(tn)) return [];
|
|
2847
|
+
names.push(tn.getText());
|
|
2848
|
+
}
|
|
2849
|
+
return names;
|
|
2850
|
+
}
|
|
2169
2851
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
2170
2852
|
const arg = firstArg(decorator);
|
|
2171
2853
|
if (!arg) return null;
|
|
@@ -2225,6 +2907,9 @@ import {
|
|
|
2225
2907
|
|
|
2226
2908
|
// src/discovery/enum-resolution.ts
|
|
2227
2909
|
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
2910
|
+
function clearEnumCache(project) {
|
|
2911
|
+
_enumCache.delete(project);
|
|
2912
|
+
}
|
|
2228
2913
|
function resolveEnumValues(name, sourceFile, project) {
|
|
2229
2914
|
let byKey = _enumCache.get(project);
|
|
2230
2915
|
if (byKey === void 0) {
|
|
@@ -2693,24 +3378,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
2693
3378
|
"Map",
|
|
2694
3379
|
"Set"
|
|
2695
3380
|
]);
|
|
2696
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3381
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2697
3382
|
if (depth <= 0) return "unknown";
|
|
2698
3383
|
if (Node6.isArrayTypeNode(typeNode)) {
|
|
2699
3384
|
const elementType = typeNode.getElementTypeNode();
|
|
2700
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3385
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
2701
3386
|
}
|
|
2702
3387
|
if (Node6.isUnionTypeNode(typeNode)) {
|
|
2703
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3388
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
2704
3389
|
}
|
|
2705
3390
|
if (Node6.isIntersectionTypeNode(typeNode)) {
|
|
2706
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3391
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
2707
3392
|
}
|
|
2708
3393
|
if (Node6.isParenthesizedTypeNode(typeNode)) {
|
|
2709
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3394
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
2710
3395
|
}
|
|
2711
3396
|
if (Node6.isTypeReference(typeNode)) {
|
|
2712
3397
|
const typeName = typeNode.getTypeName();
|
|
2713
3398
|
const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3399
|
+
const bound = subst.get(name);
|
|
3400
|
+
if (bound !== void 0) return bound;
|
|
2714
3401
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
2715
3402
|
if (name === "Date") return "string";
|
|
2716
3403
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -2718,14 +3405,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2718
3405
|
return "unknown";
|
|
2719
3406
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
2720
3407
|
if (wrapperMode) {
|
|
2721
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3408
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
2722
3409
|
}
|
|
2723
3410
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
2724
3411
|
return typeNode.getText();
|
|
2725
3412
|
}
|
|
2726
3413
|
const resolved = findType(name, sourceFile, project);
|
|
2727
3414
|
if (resolved) {
|
|
2728
|
-
|
|
3415
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
3416
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
2729
3417
|
}
|
|
2730
3418
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
2731
3419
|
return "unknown";
|
|
@@ -2738,32 +3426,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2738
3426
|
if (kind === SyntaxKind3.AnyKeyword) return "unknown";
|
|
2739
3427
|
return typeNode.getText();
|
|
2740
3428
|
}
|
|
2741
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
3429
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
2742
3430
|
const typeArgs = typeNode.getTypeArguments();
|
|
2743
3431
|
const firstTypeArg = typeArgs[0];
|
|
2744
3432
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2745
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3433
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
2746
3434
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2747
3435
|
}
|
|
2748
3436
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2749
3437
|
}
|
|
2750
|
-
function
|
|
3438
|
+
function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
|
|
3439
|
+
if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
|
|
3440
|
+
const params = result.decl.getTypeParameters().map((p) => p.getName());
|
|
3441
|
+
if (params.length === 0) return /* @__PURE__ */ new Map();
|
|
3442
|
+
const args = typeNode.getTypeArguments();
|
|
3443
|
+
const subst = /* @__PURE__ */ new Map();
|
|
3444
|
+
params.forEach((param, i) => {
|
|
3445
|
+
const arg = args[i];
|
|
3446
|
+
if (arg)
|
|
3447
|
+
subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
|
|
3448
|
+
});
|
|
3449
|
+
return subst;
|
|
3450
|
+
}
|
|
3451
|
+
function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2751
3452
|
if (depth < 0) return "unknown";
|
|
2752
3453
|
switch (result.kind) {
|
|
2753
3454
|
case "class":
|
|
2754
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3455
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2755
3456
|
case "interface":
|
|
2756
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3457
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2757
3458
|
case "typeAlias":
|
|
2758
3459
|
if (result.typeNode) {
|
|
2759
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
3460
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
2760
3461
|
}
|
|
2761
3462
|
return result.text;
|
|
2762
3463
|
case "enum":
|
|
2763
3464
|
return result.members.join(" | ");
|
|
2764
3465
|
}
|
|
2765
3466
|
}
|
|
2766
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
3467
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2767
3468
|
if (depth < 0) return "unknown";
|
|
2768
3469
|
const lines = [];
|
|
2769
3470
|
for (const prop of decl.getProperties()) {
|
|
@@ -2772,7 +3473,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
2772
3473
|
const propTypeNode = prop.getTypeNode();
|
|
2773
3474
|
let propType = "unknown";
|
|
2774
3475
|
if (propTypeNode) {
|
|
2775
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
3476
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
2776
3477
|
}
|
|
2777
3478
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
2778
3479
|
}
|
|
@@ -2821,7 +3522,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
2821
3522
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
2822
3523
|
}
|
|
2823
3524
|
function extractResponseType(method, sourceFile, project) {
|
|
2824
|
-
const apiResponseDecorator = method.
|
|
3525
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2825
3526
|
if (apiResponseDecorator) {
|
|
2826
3527
|
const args = apiResponseDecorator.getArguments();
|
|
2827
3528
|
const optsArg = args[0];
|
|
@@ -2850,6 +3551,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
2850
3551
|
}
|
|
2851
3552
|
return "unknown";
|
|
2852
3553
|
}
|
|
3554
|
+
function apiResponseStatus(decorator) {
|
|
3555
|
+
const optsArg = decorator.getArguments()[0];
|
|
3556
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
3557
|
+
for (const prop of optsArg.getProperties()) {
|
|
3558
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3559
|
+
if (prop.getName() !== "status") continue;
|
|
3560
|
+
const val = prop.getInitializer();
|
|
3561
|
+
if (val && Node6.isNumericLiteral(val)) return Number(val.getLiteralValue());
|
|
3562
|
+
}
|
|
3563
|
+
return null;
|
|
3564
|
+
}
|
|
3565
|
+
function apiResponseTypeNode(decorator) {
|
|
3566
|
+
const optsArg = decorator.getArguments()[0];
|
|
3567
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
3568
|
+
for (const prop of optsArg.getProperties()) {
|
|
3569
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3570
|
+
if (prop.getName() !== "type") continue;
|
|
3571
|
+
const val = prop.getInitializer();
|
|
3572
|
+
if (!val) return null;
|
|
3573
|
+
if (Node6.isArrayLiteralExpression(val)) {
|
|
3574
|
+
const first = val.getElements()[0];
|
|
3575
|
+
return first ? { node: first, isArray: true } : null;
|
|
3576
|
+
}
|
|
3577
|
+
return { node: val, isArray: false };
|
|
3578
|
+
}
|
|
3579
|
+
return null;
|
|
3580
|
+
}
|
|
3581
|
+
function extractErrorType(method, sourceFile, project) {
|
|
3582
|
+
for (const decorator of method.getDecorators()) {
|
|
3583
|
+
if (decorator.getName() !== "ApiResponse") continue;
|
|
3584
|
+
const status = apiResponseStatus(decorator);
|
|
3585
|
+
if (status === null || status < 400) continue;
|
|
3586
|
+
const typeInfo = apiResponseTypeNode(decorator);
|
|
3587
|
+
if (!typeInfo) continue;
|
|
3588
|
+
const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
|
|
3589
|
+
const type = typeInfo.isArray ? `Array<${inner}>` : inner;
|
|
3590
|
+
let ref = null;
|
|
3591
|
+
if (Node6.isIdentifier(typeInfo.node)) {
|
|
3592
|
+
const name = typeInfo.node.getText();
|
|
3593
|
+
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3594
|
+
if (localDecl?.isExported()) {
|
|
3595
|
+
ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
|
|
3596
|
+
} else {
|
|
3597
|
+
const resolved = resolveImportedType(name, sourceFile, project);
|
|
3598
|
+
if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
|
|
3599
|
+
ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
return { type, ref };
|
|
3604
|
+
}
|
|
3605
|
+
return null;
|
|
3606
|
+
}
|
|
2853
3607
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
2854
3608
|
if (!Node6.isIdentifier(node)) return "unknown";
|
|
2855
3609
|
const name = node.getText();
|
|
@@ -2865,17 +3619,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
2865
3619
|
unwrapContainers: true
|
|
2866
3620
|
});
|
|
2867
3621
|
}
|
|
3622
|
+
var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
|
|
3623
|
+
var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
|
|
3624
|
+
var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
|
|
3625
|
+
function detectStreamElement(method) {
|
|
3626
|
+
const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
|
|
3627
|
+
let node = method.getReturnTypeNode();
|
|
3628
|
+
node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
|
|
3629
|
+
const containerEl = streamContainerElement(node);
|
|
3630
|
+
if (containerEl) {
|
|
3631
|
+
return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
|
|
3632
|
+
}
|
|
3633
|
+
if (hasSse) return node ?? null;
|
|
3634
|
+
return null;
|
|
3635
|
+
}
|
|
3636
|
+
function streamContainerElement(node) {
|
|
3637
|
+
if (!node || !Node6.isTypeReference(node)) return null;
|
|
3638
|
+
const typeName = node.getTypeName();
|
|
3639
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3640
|
+
if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
|
|
3641
|
+
return node.getTypeArguments()[0] ?? null;
|
|
3642
|
+
}
|
|
3643
|
+
return null;
|
|
3644
|
+
}
|
|
3645
|
+
function unwrapNamedContainer(node, names) {
|
|
3646
|
+
if (!node || !Node6.isTypeReference(node)) return node;
|
|
3647
|
+
const typeName = node.getTypeName();
|
|
3648
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3649
|
+
if (names.has(name)) {
|
|
3650
|
+
return node.getTypeArguments()[0] ?? node;
|
|
3651
|
+
}
|
|
3652
|
+
return node;
|
|
3653
|
+
}
|
|
2868
3654
|
function extractDtoContract(method, sourceFile, project) {
|
|
2869
3655
|
let body = extractBodyType(method, sourceFile, project);
|
|
2870
3656
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
2871
3657
|
const query = extractQueryType(method, sourceFile, project);
|
|
3658
|
+
const streamElement = detectStreamElement(method);
|
|
3659
|
+
const isStream = streamElement !== null;
|
|
2872
3660
|
if (filterInfo && filterInfo.source === "body") {
|
|
2873
3661
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
2874
3662
|
body = body ?? bodyType;
|
|
2875
3663
|
}
|
|
2876
3664
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
2877
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
2878
|
-
|
|
3665
|
+
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
3666
|
+
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
3667
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
2879
3668
|
return null;
|
|
2880
3669
|
}
|
|
2881
3670
|
let bodyRef = null;
|
|
@@ -2889,12 +3678,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2889
3678
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
2890
3679
|
}
|
|
2891
3680
|
}
|
|
2892
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
3681
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
2893
3682
|
if (returnTypeNode) {
|
|
2894
3683
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
2895
3684
|
}
|
|
2896
|
-
if (!responseRef) {
|
|
2897
|
-
const apiResp = method.
|
|
3685
|
+
if (!responseRef && !isStream) {
|
|
3686
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2898
3687
|
if (apiResp) {
|
|
2899
3688
|
const args = apiResp.getArguments();
|
|
2900
3689
|
const optsArg = args[0];
|
|
@@ -2936,16 +3725,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2936
3725
|
query,
|
|
2937
3726
|
body,
|
|
2938
3727
|
response,
|
|
3728
|
+
error: errorInfo?.type ?? null,
|
|
2939
3729
|
params: paramsType,
|
|
2940
3730
|
queryRef,
|
|
2941
3731
|
bodyRef,
|
|
2942
3732
|
responseRef,
|
|
3733
|
+
errorRef: errorInfo?.ref ?? null,
|
|
2943
3734
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2944
3735
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2945
3736
|
filterSource: filterInfo?.source ?? null,
|
|
2946
3737
|
formWarnings,
|
|
2947
3738
|
bodySchema,
|
|
2948
|
-
querySchema
|
|
3739
|
+
querySchema,
|
|
3740
|
+
stream: isStream
|
|
2949
3741
|
};
|
|
2950
3742
|
}
|
|
2951
3743
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -3063,6 +3855,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
3063
3855
|
let query = null;
|
|
3064
3856
|
let body = null;
|
|
3065
3857
|
let response = "unknown";
|
|
3858
|
+
let error = null;
|
|
3066
3859
|
let bodyZodText = null;
|
|
3067
3860
|
let queryZodText = null;
|
|
3068
3861
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -3078,25 +3871,38 @@ function parseDefineContractCall(callExpr) {
|
|
|
3078
3871
|
bodyZodText = val.getText();
|
|
3079
3872
|
} else if (propName === "response") {
|
|
3080
3873
|
response = zodAstToTs(val);
|
|
3874
|
+
} else if (propName === "error") {
|
|
3875
|
+
error = zodAstToTs(val);
|
|
3081
3876
|
}
|
|
3082
3877
|
}
|
|
3083
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3878
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
3084
3879
|
}
|
|
3085
3880
|
|
|
3086
3881
|
// src/discovery/contracts-fast.ts
|
|
3087
3882
|
async function discoverContractsFast(opts) {
|
|
3088
3883
|
const { cwd, glob, tsconfig } = opts;
|
|
3089
|
-
const tsconfigPath =
|
|
3090
|
-
|
|
3884
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3885
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3886
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3887
|
+
for (const f of files) {
|
|
3888
|
+
project.addSourceFileAtPath(f);
|
|
3889
|
+
}
|
|
3890
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3891
|
+
return extractAllRoutes(project);
|
|
3892
|
+
}
|
|
3893
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
3894
|
+
return tsconfig ? resolve3(tsconfig) : join13(cwd, "tsconfig.json");
|
|
3895
|
+
}
|
|
3896
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
3091
3897
|
try {
|
|
3092
|
-
|
|
3898
|
+
return new Project3({
|
|
3093
3899
|
tsConfigFilePath: tsconfigPath,
|
|
3094
3900
|
skipAddingFilesFromTsConfig: true,
|
|
3095
3901
|
skipLoadingLibFiles: true,
|
|
3096
3902
|
skipFileDependencyResolution: true
|
|
3097
3903
|
});
|
|
3098
3904
|
} catch {
|
|
3099
|
-
|
|
3905
|
+
return new Project3({
|
|
3100
3906
|
skipAddingFilesFromTsConfig: true,
|
|
3101
3907
|
skipLoadingLibFiles: true,
|
|
3102
3908
|
skipFileDependencyResolution: true,
|
|
@@ -3107,20 +3913,105 @@ async function discoverContractsFast(opts) {
|
|
|
3107
3913
|
}
|
|
3108
3914
|
});
|
|
3109
3915
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
project.addSourceFileAtPath(f);
|
|
3113
|
-
}
|
|
3114
|
-
const routes = [];
|
|
3916
|
+
}
|
|
3917
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
3115
3918
|
setDiscoveryContext(project, {
|
|
3116
3919
|
projectRoot: cwd,
|
|
3117
3920
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3118
3921
|
});
|
|
3922
|
+
}
|
|
3923
|
+
function extractRoutesFrom(project, controllerPaths) {
|
|
3924
|
+
const routes = [];
|
|
3925
|
+
for (const path of controllerPaths) {
|
|
3926
|
+
const sourceFile = project.getSourceFile(path);
|
|
3927
|
+
if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3928
|
+
}
|
|
3929
|
+
return routes;
|
|
3930
|
+
}
|
|
3931
|
+
function extractAllRoutes(project) {
|
|
3932
|
+
const routes = [];
|
|
3119
3933
|
for (const sourceFile of project.getSourceFiles()) {
|
|
3120
3934
|
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3121
3935
|
}
|
|
3122
3936
|
return routes;
|
|
3123
3937
|
}
|
|
3938
|
+
var PersistentDiscovery = class _PersistentDiscovery {
|
|
3939
|
+
project;
|
|
3940
|
+
cwd;
|
|
3941
|
+
glob;
|
|
3942
|
+
/** Absolute paths of the controllers currently loaded as extraction roots. */
|
|
3943
|
+
controllerPaths = /* @__PURE__ */ new Set();
|
|
3944
|
+
constructor(project, cwd, glob) {
|
|
3945
|
+
this.project = project;
|
|
3946
|
+
this.cwd = cwd;
|
|
3947
|
+
this.glob = glob;
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* Build the initial persistent Project: create it, glob + add all controllers,
|
|
3951
|
+
* bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
|
|
3952
|
+
*/
|
|
3953
|
+
static async create(opts) {
|
|
3954
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3955
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3956
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3957
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3958
|
+
const instance = new _PersistentDiscovery(project, cwd, glob);
|
|
3959
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3960
|
+
for (const f of files) {
|
|
3961
|
+
project.addSourceFileAtPath(f);
|
|
3962
|
+
instance.controllerPaths.add(f);
|
|
3963
|
+
}
|
|
3964
|
+
return instance;
|
|
3965
|
+
}
|
|
3966
|
+
/** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
|
|
3967
|
+
discover() {
|
|
3968
|
+
return this.runExtraction();
|
|
3969
|
+
}
|
|
3970
|
+
/**
|
|
3971
|
+
* Re-discover after one or more files changed. Refreshes the changed file(s)
|
|
3972
|
+
* from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
|
|
3973
|
+
* to pick up added/removed controllers, clears the per-Project caches, then
|
|
3974
|
+
* re-extracts. `changedPaths` is a hint; correctness does not depend on it
|
|
3975
|
+
* being exhaustive because re-globbing + refresh-on-presence covers the set.
|
|
3976
|
+
*/
|
|
3977
|
+
async rediscover(changedPaths) {
|
|
3978
|
+
if (changedPaths) {
|
|
3979
|
+
for (const p of changedPaths) {
|
|
3980
|
+
const abs = resolve3(p);
|
|
3981
|
+
const sf = this.project.getSourceFile(abs);
|
|
3982
|
+
if (sf) {
|
|
3983
|
+
await sf.refreshFromFileSystem();
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
const globbed = new Set(
|
|
3988
|
+
await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
|
|
3989
|
+
);
|
|
3990
|
+
for (const f of globbed) {
|
|
3991
|
+
if (!this.controllerPaths.has(f)) {
|
|
3992
|
+
try {
|
|
3993
|
+
this.project.addSourceFileAtPath(f);
|
|
3994
|
+
this.controllerPaths.add(f);
|
|
3995
|
+
} catch {
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
for (const f of this.controllerPaths) {
|
|
4000
|
+
if (!globbed.has(f)) {
|
|
4001
|
+
const sf = this.project.getSourceFile(f);
|
|
4002
|
+
if (sf) this.project.removeSourceFile(sf);
|
|
4003
|
+
this.controllerPaths.delete(f);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
return this.runExtraction();
|
|
4007
|
+
}
|
|
4008
|
+
/** Clear stale per-Project caches, then extract over the controller set. */
|
|
4009
|
+
runExtraction() {
|
|
4010
|
+
clearTypeResolutionCaches(this.project);
|
|
4011
|
+
clearEnumCache(this.project);
|
|
4012
|
+
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
3124
4015
|
function decoratorStringArg(decoratorExpr) {
|
|
3125
4016
|
if (!decoratorExpr) return void 0;
|
|
3126
4017
|
if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -3176,6 +4067,11 @@ function resolveVerb(method) {
|
|
|
3176
4067
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3177
4068
|
}
|
|
3178
4069
|
}
|
|
4070
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
4071
|
+
if (sseDecorator) {
|
|
4072
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
4073
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
4074
|
+
}
|
|
3179
4075
|
return null;
|
|
3180
4076
|
}
|
|
3181
4077
|
function readAsDecorator(node, label) {
|
|
@@ -3218,7 +4114,17 @@ function buildRoute(args) {
|
|
|
3218
4114
|
};
|
|
3219
4115
|
}
|
|
3220
4116
|
function extractContractRoute(args) {
|
|
3221
|
-
const {
|
|
4117
|
+
const {
|
|
4118
|
+
cls,
|
|
4119
|
+
method,
|
|
4120
|
+
applyContractDecorator,
|
|
4121
|
+
verb,
|
|
4122
|
+
prefix,
|
|
4123
|
+
className,
|
|
4124
|
+
sourceFile,
|
|
4125
|
+
project,
|
|
4126
|
+
seenNames
|
|
4127
|
+
} = args;
|
|
3222
4128
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3223
4129
|
if (!firstDecoratorArg) return null;
|
|
3224
4130
|
let contractDef = null;
|
|
@@ -3228,18 +4134,19 @@ function extractContractRoute(args) {
|
|
|
3228
4134
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3229
4135
|
} else if (Node8.isIdentifier(firstDecoratorArg)) {
|
|
3230
4136
|
const identName = firstDecoratorArg.getText();
|
|
3231
|
-
const
|
|
3232
|
-
if (!
|
|
4137
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
4138
|
+
if (!resolvedVar) {
|
|
3233
4139
|
console.warn(
|
|
3234
|
-
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()}
|
|
4140
|
+
`[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
|
|
3235
4141
|
);
|
|
3236
4142
|
return null;
|
|
3237
4143
|
}
|
|
4144
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
3238
4145
|
const initializer = varDecl.getInitializer();
|
|
3239
4146
|
if (!initializer) return null;
|
|
3240
4147
|
contractDef = parseDefineContractCall(initializer);
|
|
3241
4148
|
if (contractDef && varDecl.isExported()) {
|
|
3242
|
-
const filePath =
|
|
4149
|
+
const filePath = declFile.getFilePath();
|
|
3243
4150
|
if (contractDef.body !== null) {
|
|
3244
4151
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3245
4152
|
}
|
|
@@ -3272,6 +4179,7 @@ function extractContractRoute(args) {
|
|
|
3272
4179
|
query: contractDef.query,
|
|
3273
4180
|
body: contractDef.body,
|
|
3274
4181
|
response: contractDef.response,
|
|
4182
|
+
error: contractDef.error,
|
|
3275
4183
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3276
4184
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3277
4185
|
// would drag server-only deps into the client bundle).
|
|
@@ -3303,15 +4211,18 @@ function extractDtoRoute(args) {
|
|
|
3303
4211
|
query: dtoContract?.query ?? null,
|
|
3304
4212
|
body: dtoContract?.body ?? null,
|
|
3305
4213
|
response: dtoContract?.response ?? "unknown",
|
|
4214
|
+
error: dtoContract?.error ?? null,
|
|
3306
4215
|
queryRef: dtoContract?.queryRef ?? null,
|
|
3307
4216
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3308
4217
|
responseRef: dtoContract?.responseRef ?? null,
|
|
4218
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
3309
4219
|
filterFields: dtoContract?.filterFields ?? null,
|
|
3310
4220
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3311
4221
|
filterSource: dtoContract?.filterSource ?? null,
|
|
3312
4222
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3313
4223
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3314
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
4224
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
4225
|
+
stream: dtoContract?.stream ?? false
|
|
3315
4226
|
}
|
|
3316
4227
|
});
|
|
3317
4228
|
}
|
|
@@ -3335,6 +4246,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3335
4246
|
prefix,
|
|
3336
4247
|
className,
|
|
3337
4248
|
sourceFile,
|
|
4249
|
+
project,
|
|
3338
4250
|
seenNames
|
|
3339
4251
|
}) : extractDtoRoute({
|
|
3340
4252
|
cls,
|
|
@@ -3354,8 +4266,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3354
4266
|
|
|
3355
4267
|
// src/watch/lock-file.ts
|
|
3356
4268
|
import { open } from "fs/promises";
|
|
3357
|
-
import { mkdir as
|
|
3358
|
-
import { join as
|
|
4269
|
+
import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
|
|
4270
|
+
import { join as join14 } from "path";
|
|
3359
4271
|
var LOCK_FILE = ".watcher.lock";
|
|
3360
4272
|
function isProcessAlive(pid) {
|
|
3361
4273
|
try {
|
|
@@ -3366,8 +4278,8 @@ function isProcessAlive(pid) {
|
|
|
3366
4278
|
}
|
|
3367
4279
|
}
|
|
3368
4280
|
async function acquireLock(outDir) {
|
|
3369
|
-
await
|
|
3370
|
-
const lockPath =
|
|
4281
|
+
await mkdir10(outDir, { recursive: true });
|
|
4282
|
+
const lockPath = join14(outDir, LOCK_FILE);
|
|
3371
4283
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3372
4284
|
try {
|
|
3373
4285
|
const fd = await open(lockPath, "wx");
|
|
@@ -3407,7 +4319,7 @@ async function watch(config, onChange) {
|
|
|
3407
4319
|
if (lock === null) {
|
|
3408
4320
|
let holderPid = "unknown";
|
|
3409
4321
|
try {
|
|
3410
|
-
const raw = await readFile3(
|
|
4322
|
+
const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3411
4323
|
const data = JSON.parse(raw);
|
|
3412
4324
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3413
4325
|
} catch {
|
|
@@ -3417,12 +4329,20 @@ async function watch(config, onChange) {
|
|
|
3417
4329
|
);
|
|
3418
4330
|
return NO_OP_WATCHER;
|
|
3419
4331
|
}
|
|
4332
|
+
let discovery = null;
|
|
4333
|
+
async function getDiscovery() {
|
|
4334
|
+
if (discovery === null) {
|
|
4335
|
+
discovery = await PersistentDiscovery.create({
|
|
4336
|
+
cwd: config.codegen.cwd,
|
|
4337
|
+
glob: config.contracts.glob,
|
|
4338
|
+
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
4339
|
+
});
|
|
4340
|
+
return discovery;
|
|
4341
|
+
}
|
|
4342
|
+
return discovery;
|
|
4343
|
+
}
|
|
3420
4344
|
try {
|
|
3421
|
-
const initialRoutes = await
|
|
3422
|
-
cwd: config.codegen.cwd,
|
|
3423
|
-
glob: config.contracts.glob,
|
|
3424
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3425
|
-
});
|
|
4345
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3426
4346
|
await generate(config, initialRoutes);
|
|
3427
4347
|
} catch (err) {
|
|
3428
4348
|
console.warn(
|
|
@@ -3435,7 +4355,7 @@ async function watch(config, onChange) {
|
|
|
3435
4355
|
}
|
|
3436
4356
|
let pagesDebounceTimer;
|
|
3437
4357
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3438
|
-
const pagesWatcher = chokidar.watch(
|
|
4358
|
+
const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
|
|
3439
4359
|
ignoreInitial: true,
|
|
3440
4360
|
persistent: true,
|
|
3441
4361
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3461,23 +4381,23 @@ async function watch(config, onChange) {
|
|
|
3461
4381
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3462
4382
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3463
4383
|
let contractsDebounceTimer;
|
|
3464
|
-
const
|
|
4384
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4385
|
+
const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
|
|
3465
4386
|
ignoreInitial: true,
|
|
3466
4387
|
persistent: true,
|
|
3467
4388
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3468
4389
|
});
|
|
3469
|
-
function scheduleContractsRegenerate() {
|
|
4390
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4391
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3470
4392
|
if (contractsDebounceTimer !== void 0) {
|
|
3471
4393
|
clearTimeout(contractsDebounceTimer);
|
|
3472
4394
|
}
|
|
3473
4395
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3474
4396
|
contractsDebounceTimer = void 0;
|
|
4397
|
+
const changed = [...pendingChangedPaths];
|
|
4398
|
+
pendingChangedPaths.clear();
|
|
3475
4399
|
try {
|
|
3476
|
-
const routes = await
|
|
3477
|
-
cwd: config.codegen.cwd,
|
|
3478
|
-
glob: config.contracts.glob,
|
|
3479
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3480
|
-
});
|
|
4400
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3481
4401
|
await generate(config, routes);
|
|
3482
4402
|
} catch (err) {
|
|
3483
4403
|
console.error(
|
|
@@ -3488,17 +4408,17 @@ async function watch(config, onChange) {
|
|
|
3488
4408
|
onChange?.();
|
|
3489
4409
|
}, config.contracts.debounceMs);
|
|
3490
4410
|
}
|
|
3491
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3492
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3493
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3494
|
-
const formsWatcher = chokidar.watch(
|
|
4411
|
+
contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4412
|
+
contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4413
|
+
contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
4414
|
+
const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
|
|
3495
4415
|
ignoreInitial: true,
|
|
3496
4416
|
persistent: true,
|
|
3497
4417
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3498
4418
|
});
|
|
3499
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3500
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3501
|
-
formsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
4419
|
+
formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4420
|
+
formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4421
|
+
formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
3502
4422
|
return {
|
|
3503
4423
|
close: async () => {
|
|
3504
4424
|
if (pagesDebounceTimer !== void 0) {
|
|
@@ -3518,7 +4438,7 @@ async function watch(config, onChange) {
|
|
|
3518
4438
|
}
|
|
3519
4439
|
|
|
3520
4440
|
// src/index.ts
|
|
3521
|
-
var VERSION = "0.
|
|
4441
|
+
var VERSION = "0.5.1";
|
|
3522
4442
|
|
|
3523
4443
|
// src/cli/codegen.ts
|
|
3524
4444
|
async function runCodegen(opts = {}) {
|
|
@@ -3547,13 +4467,13 @@ async function runCodegen(opts = {}) {
|
|
|
3547
4467
|
// src/cli/doctor.ts
|
|
3548
4468
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3549
4469
|
import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
3550
|
-
import { join as
|
|
4470
|
+
import { join as join17 } from "path";
|
|
3551
4471
|
|
|
3552
4472
|
// src/cli/init.ts
|
|
3553
4473
|
import { execFileSync } from "child_process";
|
|
3554
4474
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
3555
|
-
import { access as access2, mkdir as
|
|
3556
|
-
import { join as
|
|
4475
|
+
import { access as access2, mkdir as mkdir11, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
|
|
4476
|
+
import { join as join16 } from "path";
|
|
3557
4477
|
import { createInterface } from "readline";
|
|
3558
4478
|
|
|
3559
4479
|
// src/cli/patch-utils.ts
|
|
@@ -3615,7 +4535,7 @@ ${bold(title)}`);
|
|
|
3615
4535
|
}
|
|
3616
4536
|
async function readPackageJson(cwd) {
|
|
3617
4537
|
try {
|
|
3618
|
-
const raw = await readFile4(
|
|
4538
|
+
const raw = await readFile4(join16(cwd, "package.json"), "utf8");
|
|
3619
4539
|
return JSON.parse(raw);
|
|
3620
4540
|
} catch {
|
|
3621
4541
|
return {};
|
|
@@ -3646,7 +4566,7 @@ async function detectTemplateEngine(cwd) {
|
|
|
3646
4566
|
async function detectPackageManager(cwd) {
|
|
3647
4567
|
async function exists(file) {
|
|
3648
4568
|
try {
|
|
3649
|
-
await access2(
|
|
4569
|
+
await access2(join16(cwd, file));
|
|
3650
4570
|
return true;
|
|
3651
4571
|
} catch {
|
|
3652
4572
|
return false;
|
|
@@ -3687,13 +4607,13 @@ async function writeIfNotExists(filePath, content, label) {
|
|
|
3687
4607
|
}
|
|
3688
4608
|
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
3689
4609
|
if (dir) {
|
|
3690
|
-
await
|
|
4610
|
+
await mkdir11(dir, { recursive: true });
|
|
3691
4611
|
}
|
|
3692
|
-
await
|
|
4612
|
+
await writeFile10(filePath, content, "utf8");
|
|
3693
4613
|
logCreated(label);
|
|
3694
4614
|
}
|
|
3695
4615
|
async function handleViteConfig(cwd, framework) {
|
|
3696
|
-
const filePath =
|
|
4616
|
+
const filePath = join16(cwd, "vite.config.ts");
|
|
3697
4617
|
if (await fileExists2(filePath)) {
|
|
3698
4618
|
const existing = await readFile4(filePath, "utf8");
|
|
3699
4619
|
const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
|
|
@@ -3711,9 +4631,9 @@ async function handleViteConfig(cwd, framework) {
|
|
|
3711
4631
|
}
|
|
3712
4632
|
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
3713
4633
|
if (dir) {
|
|
3714
|
-
await
|
|
4634
|
+
await mkdir11(dir, { recursive: true });
|
|
3715
4635
|
}
|
|
3716
|
-
await
|
|
4636
|
+
await writeFile10(filePath, viteConfigTemplate(framework), "utf8");
|
|
3717
4637
|
logCreated("vite.config.ts");
|
|
3718
4638
|
}
|
|
3719
4639
|
async function patchGitignore(gitignorePath) {
|
|
@@ -3729,7 +4649,7 @@ async function patchGitignore(gitignorePath) {
|
|
|
3729
4649
|
` : `${existing}
|
|
3730
4650
|
${GITIGNORE_ENTRY}
|
|
3731
4651
|
`;
|
|
3732
|
-
await
|
|
4652
|
+
await writeFile10(gitignorePath, newContent, "utf8");
|
|
3733
4653
|
logPatched(".gitignore", "added .nestjs-inertia/");
|
|
3734
4654
|
}
|
|
3735
4655
|
function installDeps(pkgManager, deps, dev) {
|
|
@@ -3751,7 +4671,7 @@ function installDeps(pkgManager, deps, dev) {
|
|
|
3751
4671
|
}
|
|
3752
4672
|
}
|
|
3753
4673
|
async function patchPackageJsonScripts(cwd, scripts) {
|
|
3754
|
-
const pkgPath =
|
|
4674
|
+
const pkgPath = join16(cwd, "package.json");
|
|
3755
4675
|
let pkg = {};
|
|
3756
4676
|
try {
|
|
3757
4677
|
pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
@@ -3773,7 +4693,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
|
|
|
3773
4693
|
return;
|
|
3774
4694
|
}
|
|
3775
4695
|
pkg.scripts = existing;
|
|
3776
|
-
await
|
|
4696
|
+
await writeFile10(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
3777
4697
|
`, "utf8");
|
|
3778
4698
|
}
|
|
3779
4699
|
function patchAppModule(filePath, rootView) {
|
|
@@ -4068,7 +4988,7 @@ export class HomeController {
|
|
|
4068
4988
|
}
|
|
4069
4989
|
`;
|
|
4070
4990
|
function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
|
|
4071
|
-
const filePath =
|
|
4991
|
+
const filePath = join16(cwd, filename);
|
|
4072
4992
|
return patchJsonFile(
|
|
4073
4993
|
filePath,
|
|
4074
4994
|
(json) => {
|
|
@@ -4083,7 +5003,7 @@ function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
|
|
|
4083
5003
|
);
|
|
4084
5004
|
}
|
|
4085
5005
|
function patchNestCliJson(cwd, shellDir) {
|
|
4086
|
-
const filePath =
|
|
5006
|
+
const filePath = join16(cwd, "nest-cli.json");
|
|
4087
5007
|
return patchJsonFile(filePath, (json) => {
|
|
4088
5008
|
const compiler = json.compilerOptions ?? {};
|
|
4089
5009
|
const assets = compiler.assets ?? [];
|
|
@@ -4106,46 +5026,46 @@ async function scaffoldFiles(ctx) {
|
|
|
4106
5026
|
const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
|
|
4107
5027
|
logSection("Scaffold files");
|
|
4108
5028
|
await writeIfNotExists(
|
|
4109
|
-
|
|
5029
|
+
join16(cwd, "nestjs-inertia.config.ts"),
|
|
4110
5030
|
configTemplate(framework),
|
|
4111
5031
|
"nestjs-inertia.config.ts"
|
|
4112
5032
|
);
|
|
4113
|
-
await writeIfNotExists(
|
|
5033
|
+
await writeIfNotExists(join16(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
|
|
4114
5034
|
await writeIfNotExists(
|
|
4115
|
-
|
|
5035
|
+
join16(cwd, "tsconfig.inertia.json"),
|
|
4116
5036
|
TSCONFIG_INERTIA_TEMPLATE,
|
|
4117
5037
|
"tsconfig.inertia.json"
|
|
4118
5038
|
);
|
|
4119
5039
|
await writeIfNotExists(
|
|
4120
|
-
|
|
5040
|
+
join16(cwd, "inertia", "tsconfig.json"),
|
|
4121
5041
|
INERTIA_TSCONFIG_TEMPLATE,
|
|
4122
5042
|
"inertia/tsconfig.json"
|
|
4123
5043
|
);
|
|
4124
5044
|
await writeIfNotExists(
|
|
4125
|
-
|
|
5045
|
+
join16(cwd, "inertia", shellFileName),
|
|
4126
5046
|
htmlShellTemplate(framework, engine),
|
|
4127
5047
|
`inertia/${shellFileName}`
|
|
4128
5048
|
);
|
|
4129
5049
|
await handleViteConfig(cwd, framework);
|
|
4130
5050
|
await writeIfNotExists(
|
|
4131
|
-
|
|
5051
|
+
join16(cwd, "inertia", "app", `client.${entryExt}`),
|
|
4132
5052
|
entryPointTemplate(framework),
|
|
4133
5053
|
`inertia/app/client.${entryExt}`
|
|
4134
5054
|
);
|
|
4135
5055
|
await writeIfNotExists(
|
|
4136
|
-
|
|
5056
|
+
join16(cwd, "inertia", "pages", `Home.${pageExt}`),
|
|
4137
5057
|
samplePageTemplate(framework),
|
|
4138
5058
|
`inertia/pages/Home.${pageExt}`
|
|
4139
5059
|
);
|
|
4140
5060
|
await writeIfNotExists(
|
|
4141
|
-
|
|
5061
|
+
join16(cwd, "src", "home.controller.ts"),
|
|
4142
5062
|
SAMPLE_CONTROLLER,
|
|
4143
5063
|
"src/home.controller.ts"
|
|
4144
5064
|
);
|
|
4145
5065
|
}
|
|
4146
5066
|
function patchServerAppModule(ctx) {
|
|
4147
5067
|
const { cwd, rootView } = ctx;
|
|
4148
|
-
const appModulePath =
|
|
5068
|
+
const appModulePath = join16(cwd, "src", "app.module.ts");
|
|
4149
5069
|
const appModuleResult = patchAppModule(appModulePath, rootView);
|
|
4150
5070
|
if (appModuleResult === "patched") {
|
|
4151
5071
|
logPatched("src/app.module.ts", "added InertiaModule.forRoot");
|
|
@@ -4159,7 +5079,7 @@ function patchServerAppModule(ctx) {
|
|
|
4159
5079
|
}
|
|
4160
5080
|
}
|
|
4161
5081
|
function patchServerMainTs(ctx) {
|
|
4162
|
-
const mainTsPath =
|
|
5082
|
+
const mainTsPath = join16(ctx.cwd, "src", "main.ts");
|
|
4163
5083
|
const mainTsResult = patchMainTs(mainTsPath);
|
|
4164
5084
|
if (mainTsResult === "patched") {
|
|
4165
5085
|
logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
|
|
@@ -4194,7 +5114,7 @@ function patchBuildConfigs(ctx) {
|
|
|
4194
5114
|
}
|
|
4195
5115
|
async function patchGitignoreAndDist(ctx) {
|
|
4196
5116
|
const { cwd } = ctx;
|
|
4197
|
-
await patchGitignore(
|
|
5117
|
+
await patchGitignore(join16(cwd, ".gitignore"));
|
|
4198
5118
|
const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
|
|
4199
5119
|
if (tsconfigDistResult === "patched") {
|
|
4200
5120
|
logPatched("tsconfig.json", "excluded dist/ from server compilation");
|
|
@@ -4300,7 +5220,7 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
|
|
|
4300
5220
|
|
|
4301
5221
|
// src/cli/doctor.ts
|
|
4302
5222
|
function checkFileExists(cwd, file) {
|
|
4303
|
-
return existsSync(
|
|
5223
|
+
return existsSync(join17(cwd, file));
|
|
4304
5224
|
}
|
|
4305
5225
|
function readJson(path) {
|
|
4306
5226
|
try {
|
|
@@ -4336,15 +5256,15 @@ function writeJsonField(filePath, dotPath, value) {
|
|
|
4336
5256
|
}
|
|
4337
5257
|
function getPackageVersion(cwd, pkg) {
|
|
4338
5258
|
try {
|
|
4339
|
-
const pkgJson = readJson(
|
|
5259
|
+
const pkgJson = readJson(join17(cwd, "node_modules", pkg, "package.json"));
|
|
4340
5260
|
return pkgJson?.version ?? null;
|
|
4341
5261
|
} catch {
|
|
4342
5262
|
return null;
|
|
4343
5263
|
}
|
|
4344
5264
|
}
|
|
4345
5265
|
function detectPkgManager(cwd) {
|
|
4346
|
-
if (existsSync(
|
|
4347
|
-
if (existsSync(
|
|
5266
|
+
if (existsSync(join17(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
5267
|
+
if (existsSync(join17(cwd, "yarn.lock"))) return "yarn";
|
|
4348
5268
|
return "npm";
|
|
4349
5269
|
}
|
|
4350
5270
|
async function runDoctor(opts) {
|
|
@@ -4382,7 +5302,7 @@ async function runDoctor(opts) {
|
|
|
4382
5302
|
autoFix: () => runInit({ cwd })
|
|
4383
5303
|
});
|
|
4384
5304
|
if (foundShellDir) {
|
|
4385
|
-
const nestCliPath =
|
|
5305
|
+
const nestCliPath = join17(cwd, "nest-cli.json");
|
|
4386
5306
|
const nestCli = readJson(nestCliPath);
|
|
4387
5307
|
const compiler = nestCli?.compilerOptions ?? {};
|
|
4388
5308
|
const assets = compiler.assets ?? [];
|
|
@@ -4421,7 +5341,7 @@ async function runDoctor(opts) {
|
|
|
4421
5341
|
fix: "Run: nestjs-codegen codegen",
|
|
4422
5342
|
autoFix: () => runCodegen({ cwd })
|
|
4423
5343
|
});
|
|
4424
|
-
const tsconfigPath =
|
|
5344
|
+
const tsconfigPath = join17(cwd, "tsconfig.json");
|
|
4425
5345
|
const tsconfig = readJson(tsconfigPath);
|
|
4426
5346
|
const paths = tsconfig?.compilerOptions?.paths;
|
|
4427
5347
|
checks.push({
|
|
@@ -4432,7 +5352,7 @@ async function runDoctor(opts) {
|
|
|
4432
5352
|
});
|
|
4433
5353
|
const inertiaDir = foundShellDir ?? "inertia";
|
|
4434
5354
|
for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
|
|
4435
|
-
const tsc = readJson(
|
|
5355
|
+
const tsc = readJson(join17(cwd, tsconfigFile));
|
|
4436
5356
|
if (!tsc) continue;
|
|
4437
5357
|
const excl = tsc.exclude ?? [];
|
|
4438
5358
|
const excludesIt = excl.includes(inertiaDir);
|
|
@@ -4458,7 +5378,7 @@ async function runDoctor(opts) {
|
|
|
4458
5378
|
}
|
|
4459
5379
|
});
|
|
4460
5380
|
}
|
|
4461
|
-
const inertiaTsconfigPath =
|
|
5381
|
+
const inertiaTsconfigPath = join17(cwd, "tsconfig.inertia.json");
|
|
4462
5382
|
const inertiaTsconfig = readJson(inertiaTsconfigPath);
|
|
4463
5383
|
checks.push({
|
|
4464
5384
|
name: "tsconfig.inertia.json exists",
|
|
@@ -4510,7 +5430,7 @@ async function runDoctor(opts) {
|
|
|
4510
5430
|
fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
|
|
4511
5431
|
});
|
|
4512
5432
|
}
|
|
4513
|
-
const innerTsconfigPath =
|
|
5433
|
+
const innerTsconfigPath = join17(cwd, "inertia", "tsconfig.json");
|
|
4514
5434
|
checks.push({
|
|
4515
5435
|
name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
|
|
4516
5436
|
pass: existsSync(innerTsconfigPath),
|
|
@@ -4520,7 +5440,7 @@ async function runDoctor(opts) {
|
|
|
4520
5440
|
}
|
|
4521
5441
|
});
|
|
4522
5442
|
if (checkFileExists(cwd, "vite.config.ts")) {
|
|
4523
|
-
const viteContent = readFileSync4(
|
|
5443
|
+
const viteContent = readFileSync4(join17(cwd, "vite.config.ts"), "utf8");
|
|
4524
5444
|
checks.push({
|
|
4525
5445
|
name: "vite.config.ts has resolve.alias",
|
|
4526
5446
|
pass: viteContent.includes("resolve") && viteContent.includes("alias"),
|
|
@@ -4585,7 +5505,7 @@ async function runDoctor(opts) {
|
|
|
4585
5505
|
});
|
|
4586
5506
|
}
|
|
4587
5507
|
if (checkFileExists(cwd, ".gitignore")) {
|
|
4588
|
-
const gitignorePath =
|
|
5508
|
+
const gitignorePath = join17(cwd, ".gitignore");
|
|
4589
5509
|
const gitignore = readFileSync4(gitignorePath, "utf8");
|
|
4590
5510
|
checks.push({
|
|
4591
5511
|
name: ".gitignore includes .nestjs-inertia/",
|
|
@@ -4594,7 +5514,7 @@ async function runDoctor(opts) {
|
|
|
4594
5514
|
autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
|
|
4595
5515
|
});
|
|
4596
5516
|
}
|
|
4597
|
-
const pkgJsonPath =
|
|
5517
|
+
const pkgJsonPath = join17(cwd, "package.json");
|
|
4598
5518
|
const pkgJson = readJson(pkgJsonPath);
|
|
4599
5519
|
const scripts = pkgJson?.scripts ?? {};
|
|
4600
5520
|
checks.push({
|