@dudousxd/nestjs-codegen 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/cli/main.cjs +1097 -161
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1080 -144
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/{index-DA4uySjo.d.cts → index-B0mS84Jj.d.cts} +83 -1
- package/dist/{index-DA4uySjo.d.ts → index-B0mS84Jj.d.ts} +83 -1
- package/dist/index.cjs +1070 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -4
- package/dist/index.d.ts +104 -4
- package/dist/index.js +1051 -106
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +1032 -114
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +1 -1
- package/dist/nest/index.d.ts +1 -1
- package/dist/nest/index.js +1026 -108
- package/dist/nest/index.js.map +1 -1
- package/package.json +30 -11
package/dist/index.js
CHANGED
|
@@ -131,6 +131,19 @@ function applyDefaults(userConfig, cwd) {
|
|
|
131
131
|
enabled: userConfig.forms?.enabled ?? true,
|
|
132
132
|
watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
|
|
133
133
|
zodImport: userConfig.forms?.zodImport ?? "zod"
|
|
134
|
+
},
|
|
135
|
+
openapi: {
|
|
136
|
+
enabled: userConfig.openapi?.enabled ?? false,
|
|
137
|
+
fileName: userConfig.openapi?.fileName ?? "openapi.json",
|
|
138
|
+
title: userConfig.openapi?.title ?? "NestJS API",
|
|
139
|
+
version: userConfig.openapi?.version ?? "1.0.0",
|
|
140
|
+
description: userConfig.openapi?.description ?? null
|
|
141
|
+
},
|
|
142
|
+
mocks: {
|
|
143
|
+
enabled: userConfig.mocks?.enabled ?? false,
|
|
144
|
+
fileName: userConfig.mocks?.fileName ?? "mocks.ts",
|
|
145
|
+
seed: userConfig.mocks?.seed ?? 1,
|
|
146
|
+
baseUrl: userConfig.mocks?.baseUrl ?? ""
|
|
134
147
|
}
|
|
135
148
|
};
|
|
136
149
|
}
|
|
@@ -168,8 +181,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
168
181
|
}
|
|
169
182
|
|
|
170
183
|
// src/generate.ts
|
|
171
|
-
import { mkdir as
|
|
172
|
-
import { dirname as dirname2, join as
|
|
184
|
+
import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
|
|
185
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
173
186
|
|
|
174
187
|
// src/discovery/pages.ts
|
|
175
188
|
import { readFile } from "fs/promises";
|
|
@@ -698,17 +711,28 @@ function emitFilterQueryType(c) {
|
|
|
698
711
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
699
712
|
}
|
|
700
713
|
function buildResponseType(c, outDir) {
|
|
714
|
+
const respRef = c.contractSource.responseRef;
|
|
715
|
+
if (c.contractSource.stream) {
|
|
716
|
+
if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
717
|
+
return c.contractSource.response;
|
|
718
|
+
}
|
|
701
719
|
if (c.controllerRef) {
|
|
702
720
|
let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
703
721
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
704
722
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
705
723
|
}
|
|
706
|
-
const respRef = c.contractSource.responseRef;
|
|
707
724
|
if (respRef) {
|
|
708
725
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
709
726
|
}
|
|
710
727
|
return c.contractSource.response;
|
|
711
728
|
}
|
|
729
|
+
function buildErrorType(c) {
|
|
730
|
+
const errRef = c.contractSource.errorRef;
|
|
731
|
+
if (errRef) {
|
|
732
|
+
return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
|
|
733
|
+
}
|
|
734
|
+
return c.contractSource.error ?? "unknown";
|
|
735
|
+
}
|
|
712
736
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
713
737
|
const pad = " ".repeat(indent);
|
|
714
738
|
const lines = [];
|
|
@@ -723,12 +747,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
723
747
|
const bodyRef = c.contractSource.bodyRef;
|
|
724
748
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
725
749
|
const response = buildResponseType(c, outDir);
|
|
750
|
+
const error = buildErrorType(c);
|
|
726
751
|
const params = buildParamsType(c.params);
|
|
727
752
|
const safeMethod = JSON.stringify(method);
|
|
728
753
|
const safeUrl = JSON.stringify(c.path);
|
|
729
754
|
const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
|
|
755
|
+
const stream = c.contractSource.stream ? "true" : "false";
|
|
730
756
|
lines.push(
|
|
731
|
-
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
|
|
757
|
+
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
|
|
732
758
|
);
|
|
733
759
|
} else {
|
|
734
760
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -800,15 +826,21 @@ function emitReqHelper() {
|
|
|
800
826
|
""
|
|
801
827
|
];
|
|
802
828
|
}
|
|
803
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
829
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
804
830
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
805
831
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
832
|
+
if (streamExpr) {
|
|
833
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
834
|
+
}
|
|
806
835
|
for (const [name, value] of Object.entries(members)) {
|
|
807
836
|
lines.push(`${pad} ${name}: ${value},`);
|
|
808
837
|
}
|
|
809
838
|
lines.push(`${pad}}),`);
|
|
810
839
|
return lines;
|
|
811
840
|
}
|
|
841
|
+
function renderStreamExpr(req) {
|
|
842
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
843
|
+
}
|
|
812
844
|
function emitApiObjectBlock(tree, indent, p) {
|
|
813
845
|
const pad = " ".repeat(indent);
|
|
814
846
|
const lines = [];
|
|
@@ -843,7 +875,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
843
875
|
}
|
|
844
876
|
const members = {};
|
|
845
877
|
for (const [name, { value }] of owned) members[name] = value;
|
|
846
|
-
|
|
878
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
879
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
847
880
|
}
|
|
848
881
|
return lines;
|
|
849
882
|
}
|
|
@@ -881,6 +914,8 @@ var ROUTE_NAMESPACE = [
|
|
|
881
914
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
882
915
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
883
916
|
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
917
|
+
" /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
|
|
918
|
+
' export type Stream<K extends string> = ResolveByName<K, "response">;',
|
|
884
919
|
" export type Request<K extends string> = {",
|
|
885
920
|
" body: Body<K>;",
|
|
886
921
|
" query: Query<K>;",
|
|
@@ -897,6 +932,7 @@ var PATH_NAMESPACE = [
|
|
|
897
932
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
898
933
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
899
934
|
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
935
|
+
' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
900
936
|
"}",
|
|
901
937
|
""
|
|
902
938
|
];
|
|
@@ -908,6 +944,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
908
944
|
" export type Params<K extends string> = never;",
|
|
909
945
|
" export type Error<K extends string> = never;",
|
|
910
946
|
" export type FilterFields<K extends string> = never;",
|
|
947
|
+
" export type Stream<K extends string> = never;",
|
|
911
948
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
912
949
|
"}",
|
|
913
950
|
""
|
|
@@ -920,6 +957,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
920
957
|
" export type Params<M extends string, U extends string> = never;",
|
|
921
958
|
" export type Error<M extends string, U extends string> = never;",
|
|
922
959
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
960
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
923
961
|
"}",
|
|
924
962
|
""
|
|
925
963
|
];
|
|
@@ -943,7 +981,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
943
981
|
for (const r of contracted) {
|
|
944
982
|
const cs = r.contract?.contractSource;
|
|
945
983
|
if (!cs) continue;
|
|
946
|
-
const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
|
|
984
|
+
const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
|
|
947
985
|
for (const ref of refs) {
|
|
948
986
|
if (!ref) continue;
|
|
949
987
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -1376,11 +1414,470 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
1376
1414
|
await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
|
|
1377
1415
|
}
|
|
1378
1416
|
|
|
1379
|
-
// src/emit/emit-
|
|
1417
|
+
// src/emit/emit-mocks.ts
|
|
1380
1418
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1381
|
-
import { join as join8
|
|
1382
|
-
|
|
1419
|
+
import { join as join8 } from "path";
|
|
1420
|
+
|
|
1421
|
+
// src/ir/schema-node-to-json-schema.ts
|
|
1422
|
+
var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
|
|
1423
|
+
function parseLiteral(raw) {
|
|
1424
|
+
const t = raw.trim();
|
|
1425
|
+
if (t === "true") return true;
|
|
1426
|
+
if (t === "false") return false;
|
|
1427
|
+
if (t === "null") return null;
|
|
1428
|
+
const q = t[0];
|
|
1429
|
+
if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
|
|
1430
|
+
return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
|
|
1431
|
+
}
|
|
1432
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
|
|
1433
|
+
return Number(t);
|
|
1434
|
+
}
|
|
1435
|
+
return t;
|
|
1436
|
+
}
|
|
1437
|
+
function literalsType(values) {
|
|
1438
|
+
const types = new Set(values.map((v) => v === null ? "null" : typeof v));
|
|
1439
|
+
if (types.size === 1) {
|
|
1440
|
+
const only = [...types][0];
|
|
1441
|
+
if (only === "string") return "string";
|
|
1442
|
+
if (only === "number") return "number";
|
|
1443
|
+
if (only === "boolean") return "boolean";
|
|
1444
|
+
}
|
|
1445
|
+
return void 0;
|
|
1446
|
+
}
|
|
1447
|
+
function convert(node, ctx) {
|
|
1448
|
+
switch (node.kind) {
|
|
1449
|
+
case "string": {
|
|
1450
|
+
const out = { type: "string" };
|
|
1451
|
+
for (const c of node.checks) {
|
|
1452
|
+
if (c.check === "email") out.format = "email";
|
|
1453
|
+
else if (c.check === "url") out.format = "uri";
|
|
1454
|
+
else if (c.check === "uuid") out.format = "uuid";
|
|
1455
|
+
else if (c.check === "min") out.minLength = Number(c.value);
|
|
1456
|
+
else if (c.check === "max") out.maxLength = Number(c.value);
|
|
1457
|
+
else if (c.check === "regex") {
|
|
1458
|
+
const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
|
|
1459
|
+
out.pattern = m ? m[1] : c.pattern;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return out;
|
|
1463
|
+
}
|
|
1464
|
+
case "number": {
|
|
1465
|
+
const out = { type: "number" };
|
|
1466
|
+
for (const c of node.checks) {
|
|
1467
|
+
if (c.check === "int") out.type = "integer";
|
|
1468
|
+
else if (c.check === "min") out.minimum = Number(c.value);
|
|
1469
|
+
else if (c.check === "max") out.maximum = Number(c.value);
|
|
1470
|
+
else if (c.check === "positive") out.exclusiveMinimum = 0;
|
|
1471
|
+
else if (c.check === "negative") out.exclusiveMaximum = 0;
|
|
1472
|
+
}
|
|
1473
|
+
return out;
|
|
1474
|
+
}
|
|
1475
|
+
case "boolean":
|
|
1476
|
+
return { type: "boolean" };
|
|
1477
|
+
case "date":
|
|
1478
|
+
return { type: "string", format: "date-time" };
|
|
1479
|
+
case "unknown":
|
|
1480
|
+
return node.note ? { description: node.note } : {};
|
|
1481
|
+
case "instanceof":
|
|
1482
|
+
return { type: "object", description: `instanceof ${node.ctor}` };
|
|
1483
|
+
case "enum": {
|
|
1484
|
+
const values = node.literals.map(parseLiteral);
|
|
1485
|
+
const t = literalsType(values);
|
|
1486
|
+
const out = { enum: values };
|
|
1487
|
+
if (t) out.type = t;
|
|
1488
|
+
return out;
|
|
1489
|
+
}
|
|
1490
|
+
case "literal": {
|
|
1491
|
+
const value = parseLiteral(node.raw);
|
|
1492
|
+
const out = { const: value };
|
|
1493
|
+
const t = literalsType([value]);
|
|
1494
|
+
if (t) out.type = t;
|
|
1495
|
+
return out;
|
|
1496
|
+
}
|
|
1497
|
+
case "union": {
|
|
1498
|
+
const options = node.options.map((o) => convert(o, ctx));
|
|
1499
|
+
const out = { oneOf: options };
|
|
1500
|
+
if (node.discriminator) {
|
|
1501
|
+
out.discriminator = { propertyName: node.discriminator };
|
|
1502
|
+
}
|
|
1503
|
+
return out;
|
|
1504
|
+
}
|
|
1505
|
+
case "object": {
|
|
1506
|
+
const properties = {};
|
|
1507
|
+
const required = [];
|
|
1508
|
+
for (const f of node.fields) {
|
|
1509
|
+
if (f.value.kind === "optional") {
|
|
1510
|
+
properties[f.key] = convert(f.value.inner, ctx);
|
|
1511
|
+
} else {
|
|
1512
|
+
properties[f.key] = convert(f.value, ctx);
|
|
1513
|
+
required.push(f.key);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const out = { type: "object", properties };
|
|
1517
|
+
if (required.length > 0) out.required = required;
|
|
1518
|
+
out.additionalProperties = node.passthrough;
|
|
1519
|
+
return out;
|
|
1520
|
+
}
|
|
1521
|
+
case "array":
|
|
1522
|
+
return { type: "array", items: convert(node.element, ctx) };
|
|
1523
|
+
case "optional":
|
|
1524
|
+
return widenNullable(convert(node.inner, ctx));
|
|
1525
|
+
case "ref":
|
|
1526
|
+
case "lazyRef":
|
|
1527
|
+
return { $ref: `${ctx.refPrefix}${node.name}` };
|
|
1528
|
+
case "annotated":
|
|
1529
|
+
return convert(node.inner, ctx);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function widenNullable(schema) {
|
|
1533
|
+
if (schema.$ref) {
|
|
1534
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1535
|
+
}
|
|
1536
|
+
if (typeof schema.type === "string") {
|
|
1537
|
+
return { ...schema, type: [schema.type, "null"] };
|
|
1538
|
+
}
|
|
1539
|
+
if (Array.isArray(schema.type)) {
|
|
1540
|
+
return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
|
|
1541
|
+
}
|
|
1542
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1543
|
+
}
|
|
1544
|
+
function schemaNodeToJsonSchema(node, ctx = DEFAULT_CTX) {
|
|
1545
|
+
return convert(node, ctx);
|
|
1546
|
+
}
|
|
1547
|
+
function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
|
|
1548
|
+
const named = {};
|
|
1549
|
+
for (const [name, node] of mod.named) {
|
|
1550
|
+
named[name] = convert(node, ctx);
|
|
1551
|
+
}
|
|
1552
|
+
return { root: convert(mod.root, ctx), named };
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/emit/mock-gen-runtime.ts
|
|
1556
|
+
var MOCK_GEN_RUNTIME = `
|
|
1557
|
+
/** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
|
|
1558
|
+
function makeRng(seed) {
|
|
1559
|
+
let a = seed >>> 0;
|
|
1560
|
+
return {
|
|
1561
|
+
next() {
|
|
1562
|
+
a |= 0;
|
|
1563
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
1564
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
1565
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
1566
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1567
|
+
},
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function __pick(rng, items) {
|
|
1572
|
+
return items[Math.floor(rng.next() * items.length)];
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function __intBetween(rng, min, max) {
|
|
1576
|
+
return Math.floor(rng.next() * (max - min + 1)) + min;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
|
|
1580
|
+
const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
|
|
1581
|
+
const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
|
|
1582
|
+
|
|
1583
|
+
function __fakeWords(rng, count) {
|
|
1584
|
+
let out = [];
|
|
1585
|
+
for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
|
|
1586
|
+
return out.join(' ');
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function __hex(rng, len) {
|
|
1590
|
+
let s = '';
|
|
1591
|
+
for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
|
|
1592
|
+
return s;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function __fakeUuid(rng) {
|
|
1596
|
+
return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function __fakeString(rng, schema) {
|
|
1600
|
+
switch (schema.format) {
|
|
1601
|
+
case 'email':
|
|
1602
|
+
return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
|
|
1603
|
+
case 'uri':
|
|
1604
|
+
case 'url':
|
|
1605
|
+
return 'https://example.com/' + __pick(rng, __WORDS);
|
|
1606
|
+
case 'uuid':
|
|
1607
|
+
return __fakeUuid(rng);
|
|
1608
|
+
case 'date-time':
|
|
1609
|
+
return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
|
|
1610
|
+
default:
|
|
1611
|
+
return __fakeWords(rng, __intBetween(rng, 1, 3));
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
|
|
1616
|
+
function generateMock(schema, rng, defs, depth) {
|
|
1617
|
+
defs = defs || {};
|
|
1618
|
+
depth = depth || 0;
|
|
1619
|
+
if (schema.$ref) {
|
|
1620
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
1621
|
+
const target = defs[name];
|
|
1622
|
+
if (!target || depth > 4) return null;
|
|
1623
|
+
return generateMock(target, rng, defs, depth + 1);
|
|
1624
|
+
}
|
|
1625
|
+
if ('const' in schema) return schema.const;
|
|
1626
|
+
if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
|
|
1627
|
+
if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
|
|
1628
|
+
if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
|
|
1629
|
+
let type = Array.isArray(schema.type)
|
|
1630
|
+
? (schema.type.filter((t) => t !== 'null')[0] || 'null')
|
|
1631
|
+
: schema.type;
|
|
1632
|
+
switch (type) {
|
|
1633
|
+
case 'string':
|
|
1634
|
+
return __fakeString(rng, schema);
|
|
1635
|
+
case 'integer':
|
|
1636
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
|
|
1637
|
+
case 'number':
|
|
1638
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
|
|
1639
|
+
case 'boolean':
|
|
1640
|
+
return rng.next() < 0.5;
|
|
1641
|
+
case 'null':
|
|
1642
|
+
return null;
|
|
1643
|
+
case 'array': {
|
|
1644
|
+
const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
|
|
1645
|
+
const items = schema.items || {};
|
|
1646
|
+
let arr = [];
|
|
1647
|
+
for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
|
|
1648
|
+
return arr;
|
|
1649
|
+
}
|
|
1650
|
+
case 'object': {
|
|
1651
|
+
const out = {};
|
|
1652
|
+
const props = schema.properties || {};
|
|
1653
|
+
for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
|
|
1654
|
+
return out;
|
|
1655
|
+
}
|
|
1656
|
+
default:
|
|
1657
|
+
return {};
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
`.trim();
|
|
1661
|
+
|
|
1662
|
+
// src/emit/emit-mocks.ts
|
|
1663
|
+
var REF_PREFIX = "#/components/schemas/";
|
|
1664
|
+
function toMswPath(path, baseUrl) {
|
|
1665
|
+
return `${baseUrl}${path}`;
|
|
1666
|
+
}
|
|
1667
|
+
function responseSchemaFor(route, defs) {
|
|
1668
|
+
const cs = route.contract.contractSource;
|
|
1669
|
+
if (cs.responseSchema) {
|
|
1670
|
+
const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
|
|
1671
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1672
|
+
if (!(name in defs)) defs[name] = node;
|
|
1673
|
+
}
|
|
1674
|
+
return root;
|
|
1675
|
+
}
|
|
1676
|
+
return {};
|
|
1677
|
+
}
|
|
1678
|
+
function buildMocksFile(routes, opts = {}) {
|
|
1679
|
+
const seed = opts.seed ?? 1;
|
|
1680
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
1681
|
+
const contracted = routes.filter((r) => r.contract);
|
|
1682
|
+
const defs = {};
|
|
1683
|
+
const handlers = [];
|
|
1684
|
+
for (const r of contracted) {
|
|
1685
|
+
const schema = responseSchemaFor(r, defs);
|
|
1686
|
+
const method = r.method.toLowerCase();
|
|
1687
|
+
const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
|
|
1688
|
+
const path = toMswPath(r.path, baseUrl);
|
|
1689
|
+
const cs = r.contract.contractSource;
|
|
1690
|
+
const schemaLiteral = JSON.stringify(schema);
|
|
1691
|
+
const pathLit = JSON.stringify(path);
|
|
1692
|
+
if (cs.stream) {
|
|
1693
|
+
handlers.push(
|
|
1694
|
+
[
|
|
1695
|
+
` // ${r.name} (stream)`,
|
|
1696
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1697
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1698
|
+
" const body = `data: ${JSON.stringify(value)}\\n\\n`;",
|
|
1699
|
+
" return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
|
|
1700
|
+
" }),"
|
|
1701
|
+
].join("\n")
|
|
1702
|
+
);
|
|
1703
|
+
} else {
|
|
1704
|
+
handlers.push(
|
|
1705
|
+
[
|
|
1706
|
+
` // ${r.name}`,
|
|
1707
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1708
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1709
|
+
" return HttpResponse.json(value);",
|
|
1710
|
+
" }),"
|
|
1711
|
+
].join("\n")
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const lines = [
|
|
1716
|
+
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1717
|
+
"// MSW handlers returning deterministic, schema-shaped mock data.",
|
|
1718
|
+
"/* eslint-disable */",
|
|
1719
|
+
"// @ts-nocheck",
|
|
1720
|
+
"",
|
|
1721
|
+
"import { http, HttpResponse } from 'msw';",
|
|
1722
|
+
"",
|
|
1723
|
+
`const SEED = ${seed};`,
|
|
1724
|
+
"",
|
|
1725
|
+
"// ---------------------------------------------------------------------------",
|
|
1726
|
+
"// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
|
|
1727
|
+
"// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
|
|
1728
|
+
"// ---------------------------------------------------------------------------",
|
|
1729
|
+
MOCK_GEN_RUNTIME,
|
|
1730
|
+
"",
|
|
1731
|
+
"// Shared component schemas referenced by $ref.",
|
|
1732
|
+
`const DEFS = ${JSON.stringify(defs, null, 2)};`,
|
|
1733
|
+
"",
|
|
1734
|
+
"/** MSW request handlers, one per contracted route. */",
|
|
1735
|
+
"export const handlers = [",
|
|
1736
|
+
...handlers,
|
|
1737
|
+
"];",
|
|
1738
|
+
""
|
|
1739
|
+
];
|
|
1740
|
+
return lines.join("\n");
|
|
1741
|
+
}
|
|
1742
|
+
async function emitMocks(routes, outDir, opts = {}) {
|
|
1383
1743
|
await mkdir5(outDir, { recursive: true });
|
|
1744
|
+
const content = buildMocksFile(routes, opts);
|
|
1745
|
+
const fileName = opts.fileName ?? "mocks.ts";
|
|
1746
|
+
await writeFile5(join8(outDir, fileName), content, "utf8");
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// src/emit/emit-openapi.ts
|
|
1750
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1751
|
+
import { join as join9 } from "path";
|
|
1752
|
+
var REF_PREFIX2 = "#/components/schemas/";
|
|
1753
|
+
function toOpenApiPath(path) {
|
|
1754
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
1755
|
+
}
|
|
1756
|
+
function positionSchema(schema, tsType, components) {
|
|
1757
|
+
if (schema) {
|
|
1758
|
+
const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
|
|
1759
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1760
|
+
if (!(name in components)) components[name] = node;
|
|
1761
|
+
}
|
|
1762
|
+
return root;
|
|
1763
|
+
}
|
|
1764
|
+
return tsType ? { description: tsType } : {};
|
|
1765
|
+
}
|
|
1766
|
+
function buildParameters(route) {
|
|
1767
|
+
const params = [];
|
|
1768
|
+
for (const p of route.params) {
|
|
1769
|
+
if (p.source === "path") {
|
|
1770
|
+
params.push({
|
|
1771
|
+
name: p.name,
|
|
1772
|
+
in: "path",
|
|
1773
|
+
required: true,
|
|
1774
|
+
schema: { type: "string" }
|
|
1775
|
+
});
|
|
1776
|
+
} else if (p.source === "query") {
|
|
1777
|
+
params.push({
|
|
1778
|
+
name: p.name,
|
|
1779
|
+
in: "query",
|
|
1780
|
+
required: false,
|
|
1781
|
+
schema: { type: "string" }
|
|
1782
|
+
});
|
|
1783
|
+
} else if (p.source === "header") {
|
|
1784
|
+
params.push({
|
|
1785
|
+
name: p.name,
|
|
1786
|
+
in: "header",
|
|
1787
|
+
required: false,
|
|
1788
|
+
schema: { type: "string" }
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return params;
|
|
1793
|
+
}
|
|
1794
|
+
function buildResponses(cs, components) {
|
|
1795
|
+
const responses = {};
|
|
1796
|
+
const successSchema = positionSchema(
|
|
1797
|
+
// Prefer rich response IR when present; otherwise fall back to the TS type.
|
|
1798
|
+
cs.responseSchema ?? null,
|
|
1799
|
+
cs.response,
|
|
1800
|
+
components
|
|
1801
|
+
);
|
|
1802
|
+
const successContentType = cs.stream ? "text/event-stream" : "application/json";
|
|
1803
|
+
responses["200"] = {
|
|
1804
|
+
description: cs.stream ? "Server-sent event stream" : "Successful response",
|
|
1805
|
+
content: { [successContentType]: { schema: successSchema } }
|
|
1806
|
+
};
|
|
1807
|
+
const errorSchema = positionSchema(null, cs.error ?? null, components);
|
|
1808
|
+
const errorBody = {
|
|
1809
|
+
description: "Error response",
|
|
1810
|
+
content: { "application/json": { schema: errorSchema } }
|
|
1811
|
+
};
|
|
1812
|
+
if (cs.error || cs.errorRef) {
|
|
1813
|
+
responses["400"] = errorBody;
|
|
1814
|
+
responses.default = errorBody;
|
|
1815
|
+
} else {
|
|
1816
|
+
responses.default = {
|
|
1817
|
+
description: "Error response",
|
|
1818
|
+
content: { "application/json": { schema: {} } }
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
return responses;
|
|
1822
|
+
}
|
|
1823
|
+
function buildOperation(route, components) {
|
|
1824
|
+
const cs = route.contract.contractSource;
|
|
1825
|
+
const op = {
|
|
1826
|
+
operationId: route.name,
|
|
1827
|
+
parameters: buildParameters(route),
|
|
1828
|
+
responses: buildResponses(cs, components)
|
|
1829
|
+
};
|
|
1830
|
+
const method = route.method.toUpperCase();
|
|
1831
|
+
const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
|
|
1832
|
+
if (hasBody && (cs.bodySchema || cs.body)) {
|
|
1833
|
+
const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
|
|
1834
|
+
op.requestBody = {
|
|
1835
|
+
required: true,
|
|
1836
|
+
content: { "application/json": { schema: bodySchema } }
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
return op;
|
|
1840
|
+
}
|
|
1841
|
+
function buildOpenApiSpec(routes, opts = {}) {
|
|
1842
|
+
const components = {};
|
|
1843
|
+
const paths = {};
|
|
1844
|
+
for (const route of routes) {
|
|
1845
|
+
if (!route.contract) continue;
|
|
1846
|
+
const oaPath = toOpenApiPath(route.path);
|
|
1847
|
+
const method = route.method.toLowerCase();
|
|
1848
|
+
let pathItem = paths[oaPath];
|
|
1849
|
+
if (!pathItem) {
|
|
1850
|
+
pathItem = {};
|
|
1851
|
+
paths[oaPath] = pathItem;
|
|
1852
|
+
}
|
|
1853
|
+
pathItem[method] = buildOperation(route, components);
|
|
1854
|
+
}
|
|
1855
|
+
const info = opts.info ?? {};
|
|
1856
|
+
const doc = {
|
|
1857
|
+
openapi: "3.1.0",
|
|
1858
|
+
info: {
|
|
1859
|
+
title: info.title ?? "NestJS API",
|
|
1860
|
+
version: info.version ?? "1.0.0",
|
|
1861
|
+
...info.description ? { description: info.description } : {}
|
|
1862
|
+
},
|
|
1863
|
+
paths,
|
|
1864
|
+
components: { schemas: components }
|
|
1865
|
+
};
|
|
1866
|
+
return doc;
|
|
1867
|
+
}
|
|
1868
|
+
async function emitOpenApi(routes, outDir, opts = {}) {
|
|
1869
|
+
await mkdir6(outDir, { recursive: true });
|
|
1870
|
+
const doc = buildOpenApiSpec(routes, opts);
|
|
1871
|
+
const fileName = opts.fileName ?? "openapi.json";
|
|
1872
|
+
await writeFile6(join9(outDir, fileName), `${JSON.stringify(doc, null, 2)}
|
|
1873
|
+
`, "utf8");
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/emit/emit-pages.ts
|
|
1877
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
1878
|
+
import { join as join10, relative as relative5 } from "path";
|
|
1879
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
1880
|
+
await mkdir7(outDir, { recursive: true });
|
|
1384
1881
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
1385
1882
|
const augBody = pages.map((p) => {
|
|
1386
1883
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -1399,7 +1896,7 @@ ${augBody}
|
|
|
1399
1896
|
}
|
|
1400
1897
|
${sharedPropsBlock}}
|
|
1401
1898
|
`;
|
|
1402
|
-
await
|
|
1899
|
+
await writeFile7(join10(outDir, "pages.d.ts"), content, "utf8");
|
|
1403
1900
|
}
|
|
1404
1901
|
function buildSharedPropsBlock(sharedProps) {
|
|
1405
1902
|
if (!sharedProps) return "";
|
|
@@ -1429,12 +1926,12 @@ function needsQuotes(name) {
|
|
|
1429
1926
|
}
|
|
1430
1927
|
|
|
1431
1928
|
// src/emit/emit-routes.ts
|
|
1432
|
-
import { mkdir as
|
|
1433
|
-
import { join as
|
|
1929
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
1930
|
+
import { join as join11 } from "path";
|
|
1434
1931
|
async function emitRoutes(routes, outDir) {
|
|
1435
|
-
await
|
|
1932
|
+
await mkdir8(outDir, { recursive: true });
|
|
1436
1933
|
const content = buildRoutesFile(routes);
|
|
1437
|
-
await
|
|
1934
|
+
await writeFile8(join11(outDir, "routes.ts"), content, "utf8");
|
|
1438
1935
|
}
|
|
1439
1936
|
function buildRoutesFile(routes) {
|
|
1440
1937
|
if (routes.length === 0) {
|
|
@@ -1582,24 +2079,41 @@ async function generate(config, inputRoutes = []) {
|
|
|
1582
2079
|
});
|
|
1583
2080
|
}
|
|
1584
2081
|
const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
|
|
2082
|
+
if (hasContracts && config.openapi.enabled) {
|
|
2083
|
+
await emitOpenApi(routes, config.codegen.outDir, {
|
|
2084
|
+
fileName: config.openapi.fileName,
|
|
2085
|
+
info: {
|
|
2086
|
+
title: config.openapi.title,
|
|
2087
|
+
version: config.openapi.version,
|
|
2088
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
if (hasContracts && config.mocks.enabled) {
|
|
2093
|
+
await emitMocks(routes, config.codegen.outDir, {
|
|
2094
|
+
fileName: config.mocks.fileName,
|
|
2095
|
+
seed: config.mocks.seed,
|
|
2096
|
+
baseUrl: config.mocks.baseUrl
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
1585
2099
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
1586
2100
|
if (extensions.length > 0) {
|
|
1587
2101
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1588
2102
|
for (const file of extraFiles) {
|
|
1589
|
-
const dest =
|
|
1590
|
-
await
|
|
1591
|
-
await
|
|
2103
|
+
const dest = join12(config.codegen.outDir, file.path);
|
|
2104
|
+
await mkdir9(dirname2(dest), { recursive: true });
|
|
2105
|
+
await writeFile9(dest, file.contents, "utf8");
|
|
1592
2106
|
}
|
|
1593
2107
|
}
|
|
1594
2108
|
}
|
|
1595
2109
|
|
|
1596
2110
|
// src/watch/watcher.ts
|
|
1597
2111
|
import { readFile as readFile3 } from "fs/promises";
|
|
1598
|
-
import { join as
|
|
2112
|
+
import { join as join15 } from "path";
|
|
1599
2113
|
import chokidar from "chokidar";
|
|
1600
2114
|
|
|
1601
2115
|
// src/discovery/contracts-fast.ts
|
|
1602
|
-
import { join as
|
|
2116
|
+
import { join as join13, resolve as resolve3 } from "path";
|
|
1603
2117
|
import fg2 from "fast-glob";
|
|
1604
2118
|
import {
|
|
1605
2119
|
Node as Node8,
|
|
@@ -1781,10 +2295,85 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
|
|
|
1781
2295
|
}
|
|
1782
2296
|
return null;
|
|
1783
2297
|
}
|
|
2298
|
+
function resolveImportedVariable(name, sourceFile, project) {
|
|
2299
|
+
const local = sourceFile.getVariableDeclaration(name);
|
|
2300
|
+
if (local) return { decl: local, file: sourceFile };
|
|
2301
|
+
return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
|
|
2302
|
+
}
|
|
2303
|
+
function resolveVariableViaImports(name, sourceFile, project, seen) {
|
|
2304
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
2305
|
+
const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
|
|
2306
|
+
if (!namedImport) continue;
|
|
2307
|
+
const sourceName = namedImport.getName();
|
|
2308
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
2309
|
+
const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
|
|
2310
|
+
if (found) return found;
|
|
2311
|
+
}
|
|
2312
|
+
return null;
|
|
2313
|
+
}
|
|
2314
|
+
function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
|
|
2315
|
+
const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
|
|
2316
|
+
for (const candidate of candidates) {
|
|
2317
|
+
let importedFile = project.getSourceFile(candidate);
|
|
2318
|
+
if (!importedFile) {
|
|
2319
|
+
try {
|
|
2320
|
+
importedFile = project.addSourceFileAtPath(candidate);
|
|
2321
|
+
} catch {
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
const found = resolveVariableInFile(name, importedFile, project, seen);
|
|
2326
|
+
if (found) return found;
|
|
2327
|
+
}
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
function resolveVariableInFile(name, file, project, seen) {
|
|
2331
|
+
const filePath = file.getFilePath();
|
|
2332
|
+
if (seen.has(filePath)) return null;
|
|
2333
|
+
seen.add(filePath);
|
|
2334
|
+
const local = file.getVariableDeclaration(name);
|
|
2335
|
+
if (local) return { decl: local, file };
|
|
2336
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
2337
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
2338
|
+
const namedExports = exportDecl.getNamedExports();
|
|
2339
|
+
if (moduleSpecifier) {
|
|
2340
|
+
const hasStar = namedExports.length === 0;
|
|
2341
|
+
const reExport2 = namedExports.find(
|
|
2342
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2343
|
+
);
|
|
2344
|
+
if (!hasStar && !reExport2) continue;
|
|
2345
|
+
const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
|
|
2346
|
+
const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
|
|
2347
|
+
if (found) return found;
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
const reExport = namedExports.find(
|
|
2351
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2352
|
+
);
|
|
2353
|
+
if (!reExport) continue;
|
|
2354
|
+
const sourceName = reExport.getName();
|
|
2355
|
+
const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
|
|
2356
|
+
if (viaImports) return viaImports;
|
|
2357
|
+
}
|
|
2358
|
+
return null;
|
|
2359
|
+
}
|
|
2360
|
+
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
2361
|
+
function clearTypeResolutionCaches(project) {
|
|
2362
|
+
_findTypeCache.delete(project);
|
|
2363
|
+
_resolveNamedRefCache.delete(project);
|
|
2364
|
+
}
|
|
1784
2365
|
function findType(name, sourceFile, project) {
|
|
2366
|
+
let byKey = _findTypeCache.get(project);
|
|
2367
|
+
if (byKey === void 0) {
|
|
2368
|
+
byKey = /* @__PURE__ */ new Map();
|
|
2369
|
+
_findTypeCache.set(project, byKey);
|
|
2370
|
+
}
|
|
2371
|
+
const key = `${sourceFile.getFilePath()}\0${name}`;
|
|
2372
|
+
if (byKey.has(key)) return byKey.get(key) ?? null;
|
|
1785
2373
|
const local = findTypeInFile(name, sourceFile);
|
|
1786
|
-
|
|
1787
|
-
|
|
2374
|
+
const result = local ?? resolveImportedType(name, sourceFile, project);
|
|
2375
|
+
byKey.set(key, result);
|
|
2376
|
+
return result;
|
|
1788
2377
|
}
|
|
1789
2378
|
var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
|
|
1790
2379
|
function _localDeclForKinds(name, file, kinds) {
|
|
@@ -1821,6 +2410,26 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
1821
2410
|
if (_NON_REF_NAMES.has(refName)) return null;
|
|
1822
2411
|
name = refName;
|
|
1823
2412
|
}
|
|
2413
|
+
return _resolveNamedRef(name, sourceFile, project, opts);
|
|
2414
|
+
}
|
|
2415
|
+
var _resolveNamedRefCache = /* @__PURE__ */ new WeakMap();
|
|
2416
|
+
function _resolveNamedRef(name, sourceFile, project, opts) {
|
|
2417
|
+
let byKey = _resolveNamedRefCache.get(project);
|
|
2418
|
+
if (byKey === void 0) {
|
|
2419
|
+
byKey = /* @__PURE__ */ new Map();
|
|
2420
|
+
_resolveNamedRefCache.set(project, byKey);
|
|
2421
|
+
}
|
|
2422
|
+
const kindsKey = [...opts.kinds].sort().join(",");
|
|
2423
|
+
const key = `${sourceFile.getFilePath()}\0${name}\0${kindsKey}\0${opts.allowBareSpecifier ? 1 : 0}`;
|
|
2424
|
+
if (byKey.has(key)) {
|
|
2425
|
+
const cached = byKey.get(key) ?? null;
|
|
2426
|
+
return cached ? { ...cached } : null;
|
|
2427
|
+
}
|
|
2428
|
+
const computed = _computeNamedRef(name, sourceFile, project, opts);
|
|
2429
|
+
byKey.set(key, computed);
|
|
2430
|
+
return computed ? { ...computed } : null;
|
|
2431
|
+
}
|
|
2432
|
+
function _computeNamedRef(name, sourceFile, project, opts) {
|
|
1824
2433
|
if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
|
|
1825
2434
|
return { name, filePath: sourceFile.getFilePath() };
|
|
1826
2435
|
}
|
|
@@ -1895,7 +2504,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
1895
2504
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
1896
2505
|
visiting: /* @__PURE__ */ new Set(),
|
|
1897
2506
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
1898
|
-
depth: 0
|
|
2507
|
+
depth: 0,
|
|
2508
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
1899
2509
|
};
|
|
1900
2510
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
1901
2511
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -1919,11 +2529,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
1919
2529
|
const typeNode = prop.getTypeNode();
|
|
1920
2530
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
1921
2531
|
const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
|
|
2532
|
+
const discriminator = resolveDiscriminator(dec("Type"));
|
|
2533
|
+
if (discriminator) {
|
|
2534
|
+
const options = discriminator.subTypes.map(
|
|
2535
|
+
(name) => buildNestedReference(name, classFile, ctx)
|
|
2536
|
+
);
|
|
2537
|
+
const unionNode = {
|
|
2538
|
+
kind: "union",
|
|
2539
|
+
options,
|
|
2540
|
+
discriminator: discriminator.property
|
|
2541
|
+
};
|
|
2542
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2543
|
+
const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
|
|
2544
|
+
return applyPresence(node2, decorators);
|
|
2545
|
+
}
|
|
2546
|
+
const propTypeParam = singularClassName(typeText);
|
|
2547
|
+
if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
|
|
2548
|
+
const bound = ctx.typeBindings.get(propTypeParam);
|
|
2549
|
+
const childNode = buildNestedReference(bound, classFile, ctx);
|
|
2550
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2551
|
+
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
2552
|
+
return applyPresence(node2, decorators);
|
|
2553
|
+
}
|
|
1922
2554
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
1923
2555
|
if (has("ValidateNested") || typeRefName) {
|
|
2556
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
1924
2557
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
1925
2558
|
if (childName) {
|
|
1926
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
2559
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
1927
2560
|
const wrapArray = has("IsArray") || isArrayType;
|
|
1928
2561
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
1929
2562
|
return applyPresence(node2, decorators);
|
|
@@ -2048,10 +2681,12 @@ function baseFromType(typeText, isArrayType) {
|
|
|
2048
2681
|
return { kind: "unknown" };
|
|
2049
2682
|
}
|
|
2050
2683
|
}
|
|
2051
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2684
|
+
function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
|
|
2685
|
+
const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
|
|
2686
|
+
const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
|
|
2687
|
+
if (ctx.visiting.has(cacheKey)) {
|
|
2688
|
+
const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
|
|
2689
|
+
ctx.emittedClasses.set(cacheKey, reserved);
|
|
2055
2690
|
ctx.recursiveSchemas.add(reserved);
|
|
2056
2691
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2057
2692
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -2070,19 +2705,27 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
2070
2705
|
}
|
|
2071
2706
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
2072
2707
|
}
|
|
2073
|
-
const existing = ctx.emittedClasses.get(
|
|
2708
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
2074
2709
|
if (existing) return { kind: "ref", name: existing };
|
|
2075
|
-
const schemaName = aliasFor(
|
|
2710
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
2076
2711
|
const resolved = findType(className, fromFile, ctx.project);
|
|
2077
2712
|
if (!resolved || resolved.kind !== "class") {
|
|
2078
2713
|
return { kind: "object", fields: [], passthrough: true };
|
|
2079
2714
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2715
|
+
const params = resolved.decl.getTypeParameters().map((p) => p.getName());
|
|
2716
|
+
const newBindings = [];
|
|
2717
|
+
params.forEach((param, i) => {
|
|
2718
|
+
const arg = typeArgs[i];
|
|
2719
|
+
if (arg) newBindings.push([param, arg]);
|
|
2720
|
+
});
|
|
2721
|
+
for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
|
|
2722
|
+
ctx.emittedClasses.set(cacheKey, schemaName);
|
|
2723
|
+
ctx.visiting.add(cacheKey);
|
|
2082
2724
|
ctx.depth += 1;
|
|
2083
2725
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
2084
2726
|
ctx.depth -= 1;
|
|
2085
|
-
ctx.visiting.delete(
|
|
2727
|
+
ctx.visiting.delete(cacheKey);
|
|
2728
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
2086
2729
|
ctx.named.set(schemaName, childNode);
|
|
2087
2730
|
return { kind: "ref", name: schemaName };
|
|
2088
2731
|
}
|
|
@@ -2129,6 +2772,39 @@ function messageRaw(decorator) {
|
|
|
2129
2772
|
}
|
|
2130
2773
|
return void 0;
|
|
2131
2774
|
}
|
|
2775
|
+
function resolveDiscriminator(decorator) {
|
|
2776
|
+
const optsArg = decorator?.getArguments()[1];
|
|
2777
|
+
if (!optsArg || !Node3.isObjectLiteralExpression(optsArg)) return null;
|
|
2778
|
+
let discProp;
|
|
2779
|
+
for (const prop of optsArg.getProperties()) {
|
|
2780
|
+
if (Node3.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
|
|
2781
|
+
discProp = prop.getInitializer();
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
if (!discProp || !Node3.isObjectLiteralExpression(discProp)) return null;
|
|
2785
|
+
let property = null;
|
|
2786
|
+
const subTypes = [];
|
|
2787
|
+
for (const prop of discProp.getProperties()) {
|
|
2788
|
+
if (!Node3.isPropertyAssignment(prop)) continue;
|
|
2789
|
+
const name = prop.getName();
|
|
2790
|
+
const init = prop.getInitializer();
|
|
2791
|
+
if (!init) continue;
|
|
2792
|
+
if (name === "property" && Node3.isStringLiteral(init)) {
|
|
2793
|
+
property = init.getLiteralValue();
|
|
2794
|
+
} else if (name === "subTypes" && Node3.isArrayLiteralExpression(init)) {
|
|
2795
|
+
for (const el of init.getElements()) {
|
|
2796
|
+
if (!Node3.isObjectLiteralExpression(el)) continue;
|
|
2797
|
+
for (const p of el.getProperties()) {
|
|
2798
|
+
if (!Node3.isPropertyAssignment(p) || p.getName() !== "name") continue;
|
|
2799
|
+
const nameInit = p.getInitializer();
|
|
2800
|
+
if (nameInit && Node3.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
if (!property || subTypes.length === 0) return null;
|
|
2806
|
+
return { property, subTypes };
|
|
2807
|
+
}
|
|
2132
2808
|
function resolveTypeFactoryName(decorator) {
|
|
2133
2809
|
const arg = firstArg(decorator);
|
|
2134
2810
|
if (!arg) return null;
|
|
@@ -2142,6 +2818,17 @@ function singularClassName(typeText) {
|
|
|
2142
2818
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2143
2819
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2144
2820
|
}
|
|
2821
|
+
function genericTypeArgNames(typeNode) {
|
|
2822
|
+
if (!typeNode || !Node3.isTypeReference(typeNode)) return [];
|
|
2823
|
+
const names = [];
|
|
2824
|
+
for (const arg of typeNode.getTypeArguments()) {
|
|
2825
|
+
if (!Node3.isTypeReference(arg)) return [];
|
|
2826
|
+
const tn = arg.getTypeName();
|
|
2827
|
+
if (!Node3.isIdentifier(tn)) return [];
|
|
2828
|
+
names.push(tn.getText());
|
|
2829
|
+
}
|
|
2830
|
+
return names;
|
|
2831
|
+
}
|
|
2145
2832
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
2146
2833
|
const arg = firstArg(decorator);
|
|
2147
2834
|
if (!arg) return null;
|
|
@@ -2200,17 +2887,34 @@ import {
|
|
|
2200
2887
|
} from "ts-morph";
|
|
2201
2888
|
|
|
2202
2889
|
// src/discovery/enum-resolution.ts
|
|
2890
|
+
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
2891
|
+
function clearEnumCache(project) {
|
|
2892
|
+
_enumCache.delete(project);
|
|
2893
|
+
}
|
|
2203
2894
|
function resolveEnumValues(name, sourceFile, project) {
|
|
2895
|
+
let byKey = _enumCache.get(project);
|
|
2896
|
+
if (byKey === void 0) {
|
|
2897
|
+
byKey = /* @__PURE__ */ new Map();
|
|
2898
|
+
_enumCache.set(project, byKey);
|
|
2899
|
+
}
|
|
2900
|
+
const key = `${sourceFile.getFilePath()}\0${name}`;
|
|
2901
|
+
if (byKey.has(key)) {
|
|
2902
|
+
const cached = byKey.get(key) ?? null;
|
|
2903
|
+
return cached ? { values: [...cached.values], numeric: cached.numeric } : null;
|
|
2904
|
+
}
|
|
2204
2905
|
const resolved = findType(name, sourceFile, project);
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2906
|
+
let result = null;
|
|
2907
|
+
if (resolved && resolved.kind === "enum") {
|
|
2908
|
+
let numeric = true;
|
|
2909
|
+
const values = resolved.members.map((m) => {
|
|
2910
|
+
const parsed = JSON.parse(m);
|
|
2911
|
+
if (typeof parsed === "string") numeric = false;
|
|
2912
|
+
return String(parsed);
|
|
2913
|
+
});
|
|
2914
|
+
if (values.length > 0) result = { values, numeric };
|
|
2915
|
+
}
|
|
2916
|
+
byKey.set(key, result);
|
|
2917
|
+
return result ? { values: [...result.values], numeric: result.numeric } : null;
|
|
2214
2918
|
}
|
|
2215
2919
|
|
|
2216
2920
|
// src/discovery/filter-field-types.ts
|
|
@@ -2655,24 +3359,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
2655
3359
|
"Map",
|
|
2656
3360
|
"Set"
|
|
2657
3361
|
]);
|
|
2658
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3362
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2659
3363
|
if (depth <= 0) return "unknown";
|
|
2660
3364
|
if (Node6.isArrayTypeNode(typeNode)) {
|
|
2661
3365
|
const elementType = typeNode.getElementTypeNode();
|
|
2662
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3366
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
2663
3367
|
}
|
|
2664
3368
|
if (Node6.isUnionTypeNode(typeNode)) {
|
|
2665
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3369
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
2666
3370
|
}
|
|
2667
3371
|
if (Node6.isIntersectionTypeNode(typeNode)) {
|
|
2668
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3372
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
2669
3373
|
}
|
|
2670
3374
|
if (Node6.isParenthesizedTypeNode(typeNode)) {
|
|
2671
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3375
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
2672
3376
|
}
|
|
2673
3377
|
if (Node6.isTypeReference(typeNode)) {
|
|
2674
3378
|
const typeName = typeNode.getTypeName();
|
|
2675
3379
|
const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3380
|
+
const bound = subst.get(name);
|
|
3381
|
+
if (bound !== void 0) return bound;
|
|
2676
3382
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
2677
3383
|
if (name === "Date") return "string";
|
|
2678
3384
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -2680,14 +3386,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2680
3386
|
return "unknown";
|
|
2681
3387
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
2682
3388
|
if (wrapperMode) {
|
|
2683
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3389
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
2684
3390
|
}
|
|
2685
3391
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
2686
3392
|
return typeNode.getText();
|
|
2687
3393
|
}
|
|
2688
3394
|
const resolved = findType(name, sourceFile, project);
|
|
2689
3395
|
if (resolved) {
|
|
2690
|
-
|
|
3396
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
3397
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
2691
3398
|
}
|
|
2692
3399
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
2693
3400
|
return "unknown";
|
|
@@ -2700,32 +3407,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2700
3407
|
if (kind === SyntaxKind3.AnyKeyword) return "unknown";
|
|
2701
3408
|
return typeNode.getText();
|
|
2702
3409
|
}
|
|
2703
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
3410
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
2704
3411
|
const typeArgs = typeNode.getTypeArguments();
|
|
2705
3412
|
const firstTypeArg = typeArgs[0];
|
|
2706
3413
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2707
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3414
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
2708
3415
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2709
3416
|
}
|
|
2710
3417
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2711
3418
|
}
|
|
2712
|
-
function
|
|
3419
|
+
function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
|
|
3420
|
+
if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
|
|
3421
|
+
const params = result.decl.getTypeParameters().map((p) => p.getName());
|
|
3422
|
+
if (params.length === 0) return /* @__PURE__ */ new Map();
|
|
3423
|
+
const args = typeNode.getTypeArguments();
|
|
3424
|
+
const subst = /* @__PURE__ */ new Map();
|
|
3425
|
+
params.forEach((param, i) => {
|
|
3426
|
+
const arg = args[i];
|
|
3427
|
+
if (arg)
|
|
3428
|
+
subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
|
|
3429
|
+
});
|
|
3430
|
+
return subst;
|
|
3431
|
+
}
|
|
3432
|
+
function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2713
3433
|
if (depth < 0) return "unknown";
|
|
2714
3434
|
switch (result.kind) {
|
|
2715
3435
|
case "class":
|
|
2716
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3436
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2717
3437
|
case "interface":
|
|
2718
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3438
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2719
3439
|
case "typeAlias":
|
|
2720
3440
|
if (result.typeNode) {
|
|
2721
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
3441
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
2722
3442
|
}
|
|
2723
3443
|
return result.text;
|
|
2724
3444
|
case "enum":
|
|
2725
3445
|
return result.members.join(" | ");
|
|
2726
3446
|
}
|
|
2727
3447
|
}
|
|
2728
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
3448
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2729
3449
|
if (depth < 0) return "unknown";
|
|
2730
3450
|
const lines = [];
|
|
2731
3451
|
for (const prop of decl.getProperties()) {
|
|
@@ -2734,7 +3454,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
2734
3454
|
const propTypeNode = prop.getTypeNode();
|
|
2735
3455
|
let propType = "unknown";
|
|
2736
3456
|
if (propTypeNode) {
|
|
2737
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
3457
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
2738
3458
|
}
|
|
2739
3459
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
2740
3460
|
}
|
|
@@ -2783,7 +3503,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
2783
3503
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
2784
3504
|
}
|
|
2785
3505
|
function extractResponseType(method, sourceFile, project) {
|
|
2786
|
-
const apiResponseDecorator = method.
|
|
3506
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2787
3507
|
if (apiResponseDecorator) {
|
|
2788
3508
|
const args = apiResponseDecorator.getArguments();
|
|
2789
3509
|
const optsArg = args[0];
|
|
@@ -2812,6 +3532,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
2812
3532
|
}
|
|
2813
3533
|
return "unknown";
|
|
2814
3534
|
}
|
|
3535
|
+
function apiResponseStatus(decorator) {
|
|
3536
|
+
const optsArg = decorator.getArguments()[0];
|
|
3537
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
3538
|
+
for (const prop of optsArg.getProperties()) {
|
|
3539
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3540
|
+
if (prop.getName() !== "status") continue;
|
|
3541
|
+
const val = prop.getInitializer();
|
|
3542
|
+
if (val && Node6.isNumericLiteral(val)) return Number(val.getLiteralValue());
|
|
3543
|
+
}
|
|
3544
|
+
return null;
|
|
3545
|
+
}
|
|
3546
|
+
function apiResponseTypeNode(decorator) {
|
|
3547
|
+
const optsArg = decorator.getArguments()[0];
|
|
3548
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
3549
|
+
for (const prop of optsArg.getProperties()) {
|
|
3550
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3551
|
+
if (prop.getName() !== "type") continue;
|
|
3552
|
+
const val = prop.getInitializer();
|
|
3553
|
+
if (!val) return null;
|
|
3554
|
+
if (Node6.isArrayLiteralExpression(val)) {
|
|
3555
|
+
const first = val.getElements()[0];
|
|
3556
|
+
return first ? { node: first, isArray: true } : null;
|
|
3557
|
+
}
|
|
3558
|
+
return { node: val, isArray: false };
|
|
3559
|
+
}
|
|
3560
|
+
return null;
|
|
3561
|
+
}
|
|
3562
|
+
function extractErrorType(method, sourceFile, project) {
|
|
3563
|
+
for (const decorator of method.getDecorators()) {
|
|
3564
|
+
if (decorator.getName() !== "ApiResponse") continue;
|
|
3565
|
+
const status = apiResponseStatus(decorator);
|
|
3566
|
+
if (status === null || status < 400) continue;
|
|
3567
|
+
const typeInfo = apiResponseTypeNode(decorator);
|
|
3568
|
+
if (!typeInfo) continue;
|
|
3569
|
+
const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
|
|
3570
|
+
const type = typeInfo.isArray ? `Array<${inner}>` : inner;
|
|
3571
|
+
let ref = null;
|
|
3572
|
+
if (Node6.isIdentifier(typeInfo.node)) {
|
|
3573
|
+
const name = typeInfo.node.getText();
|
|
3574
|
+
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3575
|
+
if (localDecl?.isExported()) {
|
|
3576
|
+
ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
|
|
3577
|
+
} else {
|
|
3578
|
+
const resolved = resolveImportedType(name, sourceFile, project);
|
|
3579
|
+
if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
|
|
3580
|
+
ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
return { type, ref };
|
|
3585
|
+
}
|
|
3586
|
+
return null;
|
|
3587
|
+
}
|
|
2815
3588
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
2816
3589
|
if (!Node6.isIdentifier(node)) return "unknown";
|
|
2817
3590
|
const name = node.getText();
|
|
@@ -2827,17 +3600,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
2827
3600
|
unwrapContainers: true
|
|
2828
3601
|
});
|
|
2829
3602
|
}
|
|
3603
|
+
var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
|
|
3604
|
+
var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
|
|
3605
|
+
var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
|
|
3606
|
+
function detectStreamElement(method) {
|
|
3607
|
+
const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
|
|
3608
|
+
let node = method.getReturnTypeNode();
|
|
3609
|
+
node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
|
|
3610
|
+
const containerEl = streamContainerElement(node);
|
|
3611
|
+
if (containerEl) {
|
|
3612
|
+
return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
|
|
3613
|
+
}
|
|
3614
|
+
if (hasSse) return node ?? null;
|
|
3615
|
+
return null;
|
|
3616
|
+
}
|
|
3617
|
+
function streamContainerElement(node) {
|
|
3618
|
+
if (!node || !Node6.isTypeReference(node)) return null;
|
|
3619
|
+
const typeName = node.getTypeName();
|
|
3620
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3621
|
+
if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
|
|
3622
|
+
return node.getTypeArguments()[0] ?? null;
|
|
3623
|
+
}
|
|
3624
|
+
return null;
|
|
3625
|
+
}
|
|
3626
|
+
function unwrapNamedContainer(node, names) {
|
|
3627
|
+
if (!node || !Node6.isTypeReference(node)) return node;
|
|
3628
|
+
const typeName = node.getTypeName();
|
|
3629
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3630
|
+
if (names.has(name)) {
|
|
3631
|
+
return node.getTypeArguments()[0] ?? node;
|
|
3632
|
+
}
|
|
3633
|
+
return node;
|
|
3634
|
+
}
|
|
2830
3635
|
function extractDtoContract(method, sourceFile, project) {
|
|
2831
3636
|
let body = extractBodyType(method, sourceFile, project);
|
|
2832
3637
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
2833
3638
|
const query = extractQueryType(method, sourceFile, project);
|
|
3639
|
+
const streamElement = detectStreamElement(method);
|
|
3640
|
+
const isStream = streamElement !== null;
|
|
2834
3641
|
if (filterInfo && filterInfo.source === "body") {
|
|
2835
3642
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
2836
3643
|
body = body ?? bodyType;
|
|
2837
3644
|
}
|
|
2838
3645
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
2839
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
2840
|
-
|
|
3646
|
+
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
3647
|
+
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
3648
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
2841
3649
|
return null;
|
|
2842
3650
|
}
|
|
2843
3651
|
let bodyRef = null;
|
|
@@ -2851,12 +3659,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2851
3659
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
2852
3660
|
}
|
|
2853
3661
|
}
|
|
2854
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
3662
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
2855
3663
|
if (returnTypeNode) {
|
|
2856
3664
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
2857
3665
|
}
|
|
2858
|
-
if (!responseRef) {
|
|
2859
|
-
const apiResp = method.
|
|
3666
|
+
if (!responseRef && !isStream) {
|
|
3667
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2860
3668
|
if (apiResp) {
|
|
2861
3669
|
const args = apiResp.getArguments();
|
|
2862
3670
|
const optsArg = args[0];
|
|
@@ -2898,16 +3706,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2898
3706
|
query,
|
|
2899
3707
|
body,
|
|
2900
3708
|
response,
|
|
3709
|
+
error: errorInfo?.type ?? null,
|
|
2901
3710
|
params: paramsType,
|
|
2902
3711
|
queryRef,
|
|
2903
3712
|
bodyRef,
|
|
2904
3713
|
responseRef,
|
|
3714
|
+
errorRef: errorInfo?.ref ?? null,
|
|
2905
3715
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2906
3716
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2907
3717
|
filterSource: filterInfo?.source ?? null,
|
|
2908
3718
|
formWarnings,
|
|
2909
3719
|
bodySchema,
|
|
2910
|
-
querySchema
|
|
3720
|
+
querySchema,
|
|
3721
|
+
stream: isStream
|
|
2911
3722
|
};
|
|
2912
3723
|
}
|
|
2913
3724
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -3025,6 +3836,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
3025
3836
|
let query = null;
|
|
3026
3837
|
let body = null;
|
|
3027
3838
|
let response = "unknown";
|
|
3839
|
+
let error = null;
|
|
3028
3840
|
let bodyZodText = null;
|
|
3029
3841
|
let queryZodText = null;
|
|
3030
3842
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -3040,25 +3852,38 @@ function parseDefineContractCall(callExpr) {
|
|
|
3040
3852
|
bodyZodText = val.getText();
|
|
3041
3853
|
} else if (propName === "response") {
|
|
3042
3854
|
response = zodAstToTs(val);
|
|
3855
|
+
} else if (propName === "error") {
|
|
3856
|
+
error = zodAstToTs(val);
|
|
3043
3857
|
}
|
|
3044
3858
|
}
|
|
3045
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3859
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
3046
3860
|
}
|
|
3047
3861
|
|
|
3048
3862
|
// src/discovery/contracts-fast.ts
|
|
3049
3863
|
async function discoverContractsFast(opts) {
|
|
3050
3864
|
const { cwd, glob, tsconfig } = opts;
|
|
3051
|
-
const tsconfigPath =
|
|
3052
|
-
|
|
3865
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3866
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3867
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3868
|
+
for (const f of files) {
|
|
3869
|
+
project.addSourceFileAtPath(f);
|
|
3870
|
+
}
|
|
3871
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3872
|
+
return extractAllRoutes(project);
|
|
3873
|
+
}
|
|
3874
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
3875
|
+
return tsconfig ? resolve3(tsconfig) : join13(cwd, "tsconfig.json");
|
|
3876
|
+
}
|
|
3877
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
3053
3878
|
try {
|
|
3054
|
-
|
|
3879
|
+
return new Project3({
|
|
3055
3880
|
tsConfigFilePath: tsconfigPath,
|
|
3056
3881
|
skipAddingFilesFromTsConfig: true,
|
|
3057
3882
|
skipLoadingLibFiles: true,
|
|
3058
3883
|
skipFileDependencyResolution: true
|
|
3059
3884
|
});
|
|
3060
3885
|
} catch {
|
|
3061
|
-
|
|
3886
|
+
return new Project3({
|
|
3062
3887
|
skipAddingFilesFromTsConfig: true,
|
|
3063
3888
|
skipLoadingLibFiles: true,
|
|
3064
3889
|
skipFileDependencyResolution: true,
|
|
@@ -3069,20 +3894,105 @@ async function discoverContractsFast(opts) {
|
|
|
3069
3894
|
}
|
|
3070
3895
|
});
|
|
3071
3896
|
}
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
project.addSourceFileAtPath(f);
|
|
3075
|
-
}
|
|
3076
|
-
const routes = [];
|
|
3897
|
+
}
|
|
3898
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
3077
3899
|
setDiscoveryContext(project, {
|
|
3078
3900
|
projectRoot: cwd,
|
|
3079
3901
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3080
3902
|
});
|
|
3903
|
+
}
|
|
3904
|
+
function extractRoutesFrom(project, controllerPaths) {
|
|
3905
|
+
const routes = [];
|
|
3906
|
+
for (const path of controllerPaths) {
|
|
3907
|
+
const sourceFile = project.getSourceFile(path);
|
|
3908
|
+
if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3909
|
+
}
|
|
3910
|
+
return routes;
|
|
3911
|
+
}
|
|
3912
|
+
function extractAllRoutes(project) {
|
|
3913
|
+
const routes = [];
|
|
3081
3914
|
for (const sourceFile of project.getSourceFiles()) {
|
|
3082
3915
|
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3083
3916
|
}
|
|
3084
3917
|
return routes;
|
|
3085
3918
|
}
|
|
3919
|
+
var PersistentDiscovery = class _PersistentDiscovery {
|
|
3920
|
+
project;
|
|
3921
|
+
cwd;
|
|
3922
|
+
glob;
|
|
3923
|
+
/** Absolute paths of the controllers currently loaded as extraction roots. */
|
|
3924
|
+
controllerPaths = /* @__PURE__ */ new Set();
|
|
3925
|
+
constructor(project, cwd, glob) {
|
|
3926
|
+
this.project = project;
|
|
3927
|
+
this.cwd = cwd;
|
|
3928
|
+
this.glob = glob;
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* Build the initial persistent Project: create it, glob + add all controllers,
|
|
3932
|
+
* bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
|
|
3933
|
+
*/
|
|
3934
|
+
static async create(opts) {
|
|
3935
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3936
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3937
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3938
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3939
|
+
const instance = new _PersistentDiscovery(project, cwd, glob);
|
|
3940
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3941
|
+
for (const f of files) {
|
|
3942
|
+
project.addSourceFileAtPath(f);
|
|
3943
|
+
instance.controllerPaths.add(f);
|
|
3944
|
+
}
|
|
3945
|
+
return instance;
|
|
3946
|
+
}
|
|
3947
|
+
/** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
|
|
3948
|
+
discover() {
|
|
3949
|
+
return this.runExtraction();
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Re-discover after one or more files changed. Refreshes the changed file(s)
|
|
3953
|
+
* from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
|
|
3954
|
+
* to pick up added/removed controllers, clears the per-Project caches, then
|
|
3955
|
+
* re-extracts. `changedPaths` is a hint; correctness does not depend on it
|
|
3956
|
+
* being exhaustive because re-globbing + refresh-on-presence covers the set.
|
|
3957
|
+
*/
|
|
3958
|
+
async rediscover(changedPaths) {
|
|
3959
|
+
if (changedPaths) {
|
|
3960
|
+
for (const p of changedPaths) {
|
|
3961
|
+
const abs = resolve3(p);
|
|
3962
|
+
const sf = this.project.getSourceFile(abs);
|
|
3963
|
+
if (sf) {
|
|
3964
|
+
await sf.refreshFromFileSystem();
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
const globbed = new Set(
|
|
3969
|
+
await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
|
|
3970
|
+
);
|
|
3971
|
+
for (const f of globbed) {
|
|
3972
|
+
if (!this.controllerPaths.has(f)) {
|
|
3973
|
+
try {
|
|
3974
|
+
this.project.addSourceFileAtPath(f);
|
|
3975
|
+
this.controllerPaths.add(f);
|
|
3976
|
+
} catch {
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
for (const f of this.controllerPaths) {
|
|
3981
|
+
if (!globbed.has(f)) {
|
|
3982
|
+
const sf = this.project.getSourceFile(f);
|
|
3983
|
+
if (sf) this.project.removeSourceFile(sf);
|
|
3984
|
+
this.controllerPaths.delete(f);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
return this.runExtraction();
|
|
3988
|
+
}
|
|
3989
|
+
/** Clear stale per-Project caches, then extract over the controller set. */
|
|
3990
|
+
runExtraction() {
|
|
3991
|
+
clearTypeResolutionCaches(this.project);
|
|
3992
|
+
clearEnumCache(this.project);
|
|
3993
|
+
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
3994
|
+
}
|
|
3995
|
+
};
|
|
3086
3996
|
function decoratorStringArg(decoratorExpr) {
|
|
3087
3997
|
if (!decoratorExpr) return void 0;
|
|
3088
3998
|
if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -3138,6 +4048,11 @@ function resolveVerb(method) {
|
|
|
3138
4048
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3139
4049
|
}
|
|
3140
4050
|
}
|
|
4051
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
4052
|
+
if (sseDecorator) {
|
|
4053
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
4054
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
4055
|
+
}
|
|
3141
4056
|
return null;
|
|
3142
4057
|
}
|
|
3143
4058
|
function readAsDecorator(node, label) {
|
|
@@ -3180,7 +4095,17 @@ function buildRoute(args) {
|
|
|
3180
4095
|
};
|
|
3181
4096
|
}
|
|
3182
4097
|
function extractContractRoute(args) {
|
|
3183
|
-
const {
|
|
4098
|
+
const {
|
|
4099
|
+
cls,
|
|
4100
|
+
method,
|
|
4101
|
+
applyContractDecorator,
|
|
4102
|
+
verb,
|
|
4103
|
+
prefix,
|
|
4104
|
+
className,
|
|
4105
|
+
sourceFile,
|
|
4106
|
+
project,
|
|
4107
|
+
seenNames
|
|
4108
|
+
} = args;
|
|
3184
4109
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3185
4110
|
if (!firstDecoratorArg) return null;
|
|
3186
4111
|
let contractDef = null;
|
|
@@ -3190,18 +4115,19 @@ function extractContractRoute(args) {
|
|
|
3190
4115
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3191
4116
|
} else if (Node8.isIdentifier(firstDecoratorArg)) {
|
|
3192
4117
|
const identName = firstDecoratorArg.getText();
|
|
3193
|
-
const
|
|
3194
|
-
if (!
|
|
4118
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
4119
|
+
if (!resolvedVar) {
|
|
3195
4120
|
console.warn(
|
|
3196
|
-
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()}
|
|
4121
|
+
`[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
|
|
3197
4122
|
);
|
|
3198
4123
|
return null;
|
|
3199
4124
|
}
|
|
4125
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
3200
4126
|
const initializer = varDecl.getInitializer();
|
|
3201
4127
|
if (!initializer) return null;
|
|
3202
4128
|
contractDef = parseDefineContractCall(initializer);
|
|
3203
4129
|
if (contractDef && varDecl.isExported()) {
|
|
3204
|
-
const filePath =
|
|
4130
|
+
const filePath = declFile.getFilePath();
|
|
3205
4131
|
if (contractDef.body !== null) {
|
|
3206
4132
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3207
4133
|
}
|
|
@@ -3234,6 +4160,7 @@ function extractContractRoute(args) {
|
|
|
3234
4160
|
query: contractDef.query,
|
|
3235
4161
|
body: contractDef.body,
|
|
3236
4162
|
response: contractDef.response,
|
|
4163
|
+
error: contractDef.error,
|
|
3237
4164
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3238
4165
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3239
4166
|
// would drag server-only deps into the client bundle).
|
|
@@ -3265,15 +4192,18 @@ function extractDtoRoute(args) {
|
|
|
3265
4192
|
query: dtoContract?.query ?? null,
|
|
3266
4193
|
body: dtoContract?.body ?? null,
|
|
3267
4194
|
response: dtoContract?.response ?? "unknown",
|
|
4195
|
+
error: dtoContract?.error ?? null,
|
|
3268
4196
|
queryRef: dtoContract?.queryRef ?? null,
|
|
3269
4197
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3270
4198
|
responseRef: dtoContract?.responseRef ?? null,
|
|
4199
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
3271
4200
|
filterFields: dtoContract?.filterFields ?? null,
|
|
3272
4201
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3273
4202
|
filterSource: dtoContract?.filterSource ?? null,
|
|
3274
4203
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3275
4204
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3276
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
4205
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
4206
|
+
stream: dtoContract?.stream ?? false
|
|
3277
4207
|
}
|
|
3278
4208
|
});
|
|
3279
4209
|
}
|
|
@@ -3297,6 +4227,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3297
4227
|
prefix,
|
|
3298
4228
|
className,
|
|
3299
4229
|
sourceFile,
|
|
4230
|
+
project,
|
|
3300
4231
|
seenNames
|
|
3301
4232
|
}) : extractDtoRoute({
|
|
3302
4233
|
cls,
|
|
@@ -3316,8 +4247,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3316
4247
|
|
|
3317
4248
|
// src/watch/lock-file.ts
|
|
3318
4249
|
import { open } from "fs/promises";
|
|
3319
|
-
import { mkdir as
|
|
3320
|
-
import { join as
|
|
4250
|
+
import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
|
|
4251
|
+
import { join as join14 } from "path";
|
|
3321
4252
|
var LOCK_FILE = ".watcher.lock";
|
|
3322
4253
|
function isProcessAlive(pid) {
|
|
3323
4254
|
try {
|
|
@@ -3328,8 +4259,8 @@ function isProcessAlive(pid) {
|
|
|
3328
4259
|
}
|
|
3329
4260
|
}
|
|
3330
4261
|
async function acquireLock(outDir) {
|
|
3331
|
-
await
|
|
3332
|
-
const lockPath =
|
|
4262
|
+
await mkdir10(outDir, { recursive: true });
|
|
4263
|
+
const lockPath = join14(outDir, LOCK_FILE);
|
|
3333
4264
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3334
4265
|
try {
|
|
3335
4266
|
const fd = await open(lockPath, "wx");
|
|
@@ -3369,7 +4300,7 @@ async function watch(config, onChange) {
|
|
|
3369
4300
|
if (lock === null) {
|
|
3370
4301
|
let holderPid = "unknown";
|
|
3371
4302
|
try {
|
|
3372
|
-
const raw = await readFile3(
|
|
4303
|
+
const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3373
4304
|
const data = JSON.parse(raw);
|
|
3374
4305
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3375
4306
|
} catch {
|
|
@@ -3379,12 +4310,20 @@ async function watch(config, onChange) {
|
|
|
3379
4310
|
);
|
|
3380
4311
|
return NO_OP_WATCHER;
|
|
3381
4312
|
}
|
|
4313
|
+
let discovery = null;
|
|
4314
|
+
async function getDiscovery() {
|
|
4315
|
+
if (discovery === null) {
|
|
4316
|
+
discovery = await PersistentDiscovery.create({
|
|
4317
|
+
cwd: config.codegen.cwd,
|
|
4318
|
+
glob: config.contracts.glob,
|
|
4319
|
+
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
4320
|
+
});
|
|
4321
|
+
return discovery;
|
|
4322
|
+
}
|
|
4323
|
+
return discovery;
|
|
4324
|
+
}
|
|
3382
4325
|
try {
|
|
3383
|
-
const initialRoutes = await
|
|
3384
|
-
cwd: config.codegen.cwd,
|
|
3385
|
-
glob: config.contracts.glob,
|
|
3386
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3387
|
-
});
|
|
4326
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3388
4327
|
await generate(config, initialRoutes);
|
|
3389
4328
|
} catch (err) {
|
|
3390
4329
|
console.warn(
|
|
@@ -3397,7 +4336,7 @@ async function watch(config, onChange) {
|
|
|
3397
4336
|
}
|
|
3398
4337
|
let pagesDebounceTimer;
|
|
3399
4338
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3400
|
-
const pagesWatcher = chokidar.watch(
|
|
4339
|
+
const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
|
|
3401
4340
|
ignoreInitial: true,
|
|
3402
4341
|
persistent: true,
|
|
3403
4342
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3423,23 +4362,23 @@ async function watch(config, onChange) {
|
|
|
3423
4362
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3424
4363
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3425
4364
|
let contractsDebounceTimer;
|
|
3426
|
-
const
|
|
4365
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4366
|
+
const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
|
|
3427
4367
|
ignoreInitial: true,
|
|
3428
4368
|
persistent: true,
|
|
3429
4369
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3430
4370
|
});
|
|
3431
|
-
function scheduleContractsRegenerate() {
|
|
4371
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4372
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3432
4373
|
if (contractsDebounceTimer !== void 0) {
|
|
3433
4374
|
clearTimeout(contractsDebounceTimer);
|
|
3434
4375
|
}
|
|
3435
4376
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3436
4377
|
contractsDebounceTimer = void 0;
|
|
4378
|
+
const changed = [...pendingChangedPaths];
|
|
4379
|
+
pendingChangedPaths.clear();
|
|
3437
4380
|
try {
|
|
3438
|
-
const routes = await
|
|
3439
|
-
cwd: config.codegen.cwd,
|
|
3440
|
-
glob: config.contracts.glob,
|
|
3441
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3442
|
-
});
|
|
4381
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3443
4382
|
await generate(config, routes);
|
|
3444
4383
|
} catch (err) {
|
|
3445
4384
|
console.error(
|
|
@@ -3450,17 +4389,17 @@ async function watch(config, onChange) {
|
|
|
3450
4389
|
onChange?.();
|
|
3451
4390
|
}, config.contracts.debounceMs);
|
|
3452
4391
|
}
|
|
3453
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3454
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3455
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3456
|
-
const formsWatcher = chokidar.watch(
|
|
4392
|
+
contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4393
|
+
contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4394
|
+
contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
4395
|
+
const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
|
|
3457
4396
|
ignoreInitial: true,
|
|
3458
4397
|
persistent: true,
|
|
3459
4398
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3460
4399
|
});
|
|
3461
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3462
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3463
|
-
formsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
4400
|
+
formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4401
|
+
formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4402
|
+
formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
3464
4403
|
return {
|
|
3465
4404
|
close: async () => {
|
|
3466
4405
|
if (pagesDebounceTimer !== void 0) {
|
|
@@ -3529,16 +4468,20 @@ function renderTsType(node, ctx) {
|
|
|
3529
4468
|
}
|
|
3530
4469
|
|
|
3531
4470
|
// src/index.ts
|
|
3532
|
-
var VERSION = "0.
|
|
4471
|
+
var VERSION = "0.5.0";
|
|
3533
4472
|
export {
|
|
3534
4473
|
CodegenError,
|
|
3535
4474
|
ConfigError,
|
|
3536
4475
|
VERSION,
|
|
3537
4476
|
acquireLock,
|
|
4477
|
+
buildMocksFile,
|
|
4478
|
+
buildOpenApiSpec,
|
|
3538
4479
|
defineConfig,
|
|
3539
4480
|
discoverContractsFast,
|
|
3540
4481
|
emitApi,
|
|
3541
4482
|
emitForms,
|
|
4483
|
+
emitMocks,
|
|
4484
|
+
emitOpenApi,
|
|
3542
4485
|
emitRoutes,
|
|
3543
4486
|
extractSchemaFromDto,
|
|
3544
4487
|
generate,
|
|
@@ -3546,6 +4489,8 @@ export {
|
|
|
3546
4489
|
renderTsType,
|
|
3547
4490
|
resolveAdapter,
|
|
3548
4491
|
resolveConfig,
|
|
4492
|
+
schemaModuleToJsonSchema,
|
|
4493
|
+
schemaNodeToJsonSchema,
|
|
3549
4494
|
watch
|
|
3550
4495
|
};
|
|
3551
4496
|
//# sourceMappingURL=index.js.map
|