@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.cjs
CHANGED
|
@@ -34,10 +34,14 @@ __export(src_exports, {
|
|
|
34
34
|
ConfigError: () => ConfigError,
|
|
35
35
|
VERSION: () => VERSION,
|
|
36
36
|
acquireLock: () => acquireLock,
|
|
37
|
+
buildMocksFile: () => buildMocksFile,
|
|
38
|
+
buildOpenApiSpec: () => buildOpenApiSpec,
|
|
37
39
|
defineConfig: () => defineConfig,
|
|
38
40
|
discoverContractsFast: () => discoverContractsFast,
|
|
39
41
|
emitApi: () => emitApi,
|
|
40
42
|
emitForms: () => emitForms,
|
|
43
|
+
emitMocks: () => emitMocks,
|
|
44
|
+
emitOpenApi: () => emitOpenApi,
|
|
41
45
|
emitRoutes: () => emitRoutes,
|
|
42
46
|
extractSchemaFromDto: () => extractSchemaFromDto,
|
|
43
47
|
generate: () => generate,
|
|
@@ -45,6 +49,8 @@ __export(src_exports, {
|
|
|
45
49
|
renderTsType: () => renderTsType,
|
|
46
50
|
resolveAdapter: () => resolveAdapter,
|
|
47
51
|
resolveConfig: () => resolveConfig,
|
|
52
|
+
schemaModuleToJsonSchema: () => schemaModuleToJsonSchema,
|
|
53
|
+
schemaNodeToJsonSchema: () => schemaNodeToJsonSchema,
|
|
48
54
|
watch: () => watch
|
|
49
55
|
});
|
|
50
56
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -182,6 +188,19 @@ function applyDefaults(userConfig, cwd) {
|
|
|
182
188
|
enabled: userConfig.forms?.enabled ?? true,
|
|
183
189
|
watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
|
|
184
190
|
zodImport: userConfig.forms?.zodImport ?? "zod"
|
|
191
|
+
},
|
|
192
|
+
openapi: {
|
|
193
|
+
enabled: userConfig.openapi?.enabled ?? false,
|
|
194
|
+
fileName: userConfig.openapi?.fileName ?? "openapi.json",
|
|
195
|
+
title: userConfig.openapi?.title ?? "NestJS API",
|
|
196
|
+
version: userConfig.openapi?.version ?? "1.0.0",
|
|
197
|
+
description: userConfig.openapi?.description ?? null
|
|
198
|
+
},
|
|
199
|
+
mocks: {
|
|
200
|
+
enabled: userConfig.mocks?.enabled ?? false,
|
|
201
|
+
fileName: userConfig.mocks?.fileName ?? "mocks.ts",
|
|
202
|
+
seed: userConfig.mocks?.seed ?? 1,
|
|
203
|
+
baseUrl: userConfig.mocks?.baseUrl ?? ""
|
|
185
204
|
}
|
|
186
205
|
};
|
|
187
206
|
}
|
|
@@ -219,8 +238,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
219
238
|
}
|
|
220
239
|
|
|
221
240
|
// src/generate.ts
|
|
222
|
-
var
|
|
223
|
-
var
|
|
241
|
+
var import_promises11 = require("fs/promises");
|
|
242
|
+
var import_node_path12 = require("path");
|
|
224
243
|
|
|
225
244
|
// src/discovery/pages.ts
|
|
226
245
|
var import_promises2 = require("fs/promises");
|
|
@@ -749,17 +768,28 @@ function emitFilterQueryType(c) {
|
|
|
749
768
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
750
769
|
}
|
|
751
770
|
function buildResponseType(c, outDir) {
|
|
771
|
+
const respRef = c.contractSource.responseRef;
|
|
772
|
+
if (c.contractSource.stream) {
|
|
773
|
+
if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
774
|
+
return c.contractSource.response;
|
|
775
|
+
}
|
|
752
776
|
if (c.controllerRef) {
|
|
753
777
|
let relPath = (0, import_node_path4.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
754
778
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
755
779
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
756
780
|
}
|
|
757
|
-
const respRef = c.contractSource.responseRef;
|
|
758
781
|
if (respRef) {
|
|
759
782
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
760
783
|
}
|
|
761
784
|
return c.contractSource.response;
|
|
762
785
|
}
|
|
786
|
+
function buildErrorType(c) {
|
|
787
|
+
const errRef = c.contractSource.errorRef;
|
|
788
|
+
if (errRef) {
|
|
789
|
+
return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
|
|
790
|
+
}
|
|
791
|
+
return c.contractSource.error ?? "unknown";
|
|
792
|
+
}
|
|
763
793
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
764
794
|
const pad = " ".repeat(indent);
|
|
765
795
|
const lines = [];
|
|
@@ -774,12 +804,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
774
804
|
const bodyRef = c.contractSource.bodyRef;
|
|
775
805
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
776
806
|
const response = buildResponseType(c, outDir);
|
|
807
|
+
const error = buildErrorType(c);
|
|
777
808
|
const params = buildParamsType(c.params);
|
|
778
809
|
const safeMethod = JSON.stringify(method);
|
|
779
810
|
const safeUrl = JSON.stringify(c.path);
|
|
780
811
|
const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
|
|
812
|
+
const stream = c.contractSource.stream ? "true" : "false";
|
|
781
813
|
lines.push(
|
|
782
|
-
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
|
|
814
|
+
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
|
|
783
815
|
);
|
|
784
816
|
} else {
|
|
785
817
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -851,15 +883,21 @@ function emitReqHelper() {
|
|
|
851
883
|
""
|
|
852
884
|
];
|
|
853
885
|
}
|
|
854
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
886
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
855
887
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
856
888
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
889
|
+
if (streamExpr) {
|
|
890
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
891
|
+
}
|
|
857
892
|
for (const [name, value] of Object.entries(members)) {
|
|
858
893
|
lines.push(`${pad} ${name}: ${value},`);
|
|
859
894
|
}
|
|
860
895
|
lines.push(`${pad}}),`);
|
|
861
896
|
return lines;
|
|
862
897
|
}
|
|
898
|
+
function renderStreamExpr(req) {
|
|
899
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
900
|
+
}
|
|
863
901
|
function emitApiObjectBlock(tree, indent, p) {
|
|
864
902
|
const pad = " ".repeat(indent);
|
|
865
903
|
const lines = [];
|
|
@@ -894,7 +932,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
894
932
|
}
|
|
895
933
|
const members = {};
|
|
896
934
|
for (const [name, { value }] of owned) members[name] = value;
|
|
897
|
-
|
|
935
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
936
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
898
937
|
}
|
|
899
938
|
return lines;
|
|
900
939
|
}
|
|
@@ -932,6 +971,8 @@ var ROUTE_NAMESPACE = [
|
|
|
932
971
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
933
972
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
934
973
|
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
974
|
+
" /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
|
|
975
|
+
' export type Stream<K extends string> = ResolveByName<K, "response">;',
|
|
935
976
|
" export type Request<K extends string> = {",
|
|
936
977
|
" body: Body<K>;",
|
|
937
978
|
" query: Query<K>;",
|
|
@@ -948,6 +989,7 @@ var PATH_NAMESPACE = [
|
|
|
948
989
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
949
990
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
950
991
|
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
992
|
+
' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
951
993
|
"}",
|
|
952
994
|
""
|
|
953
995
|
];
|
|
@@ -959,6 +1001,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
959
1001
|
" export type Params<K extends string> = never;",
|
|
960
1002
|
" export type Error<K extends string> = never;",
|
|
961
1003
|
" export type FilterFields<K extends string> = never;",
|
|
1004
|
+
" export type Stream<K extends string> = never;",
|
|
962
1005
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
963
1006
|
"}",
|
|
964
1007
|
""
|
|
@@ -971,6 +1014,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
971
1014
|
" export type Params<M extends string, U extends string> = never;",
|
|
972
1015
|
" export type Error<M extends string, U extends string> = never;",
|
|
973
1016
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
1017
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
974
1018
|
"}",
|
|
975
1019
|
""
|
|
976
1020
|
];
|
|
@@ -994,7 +1038,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
994
1038
|
for (const r of contracted) {
|
|
995
1039
|
const cs = r.contract?.contractSource;
|
|
996
1040
|
if (!cs) continue;
|
|
997
|
-
const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
|
|
1041
|
+
const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
|
|
998
1042
|
for (const ref of refs) {
|
|
999
1043
|
if (!ref) continue;
|
|
1000
1044
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -1427,11 +1471,470 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
1427
1471
|
await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
|
|
1428
1472
|
}
|
|
1429
1473
|
|
|
1430
|
-
// src/emit/emit-
|
|
1474
|
+
// src/emit/emit-mocks.ts
|
|
1431
1475
|
var import_promises7 = require("fs/promises");
|
|
1432
1476
|
var import_node_path8 = require("path");
|
|
1433
|
-
|
|
1477
|
+
|
|
1478
|
+
// src/ir/schema-node-to-json-schema.ts
|
|
1479
|
+
var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
|
|
1480
|
+
function parseLiteral(raw) {
|
|
1481
|
+
const t = raw.trim();
|
|
1482
|
+
if (t === "true") return true;
|
|
1483
|
+
if (t === "false") return false;
|
|
1484
|
+
if (t === "null") return null;
|
|
1485
|
+
const q = t[0];
|
|
1486
|
+
if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
|
|
1487
|
+
return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
|
|
1488
|
+
}
|
|
1489
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
|
|
1490
|
+
return Number(t);
|
|
1491
|
+
}
|
|
1492
|
+
return t;
|
|
1493
|
+
}
|
|
1494
|
+
function literalsType(values) {
|
|
1495
|
+
const types = new Set(values.map((v) => v === null ? "null" : typeof v));
|
|
1496
|
+
if (types.size === 1) {
|
|
1497
|
+
const only = [...types][0];
|
|
1498
|
+
if (only === "string") return "string";
|
|
1499
|
+
if (only === "number") return "number";
|
|
1500
|
+
if (only === "boolean") return "boolean";
|
|
1501
|
+
}
|
|
1502
|
+
return void 0;
|
|
1503
|
+
}
|
|
1504
|
+
function convert(node, ctx) {
|
|
1505
|
+
switch (node.kind) {
|
|
1506
|
+
case "string": {
|
|
1507
|
+
const out = { type: "string" };
|
|
1508
|
+
for (const c of node.checks) {
|
|
1509
|
+
if (c.check === "email") out.format = "email";
|
|
1510
|
+
else if (c.check === "url") out.format = "uri";
|
|
1511
|
+
else if (c.check === "uuid") out.format = "uuid";
|
|
1512
|
+
else if (c.check === "min") out.minLength = Number(c.value);
|
|
1513
|
+
else if (c.check === "max") out.maxLength = Number(c.value);
|
|
1514
|
+
else if (c.check === "regex") {
|
|
1515
|
+
const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
|
|
1516
|
+
out.pattern = m ? m[1] : c.pattern;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return out;
|
|
1520
|
+
}
|
|
1521
|
+
case "number": {
|
|
1522
|
+
const out = { type: "number" };
|
|
1523
|
+
for (const c of node.checks) {
|
|
1524
|
+
if (c.check === "int") out.type = "integer";
|
|
1525
|
+
else if (c.check === "min") out.minimum = Number(c.value);
|
|
1526
|
+
else if (c.check === "max") out.maximum = Number(c.value);
|
|
1527
|
+
else if (c.check === "positive") out.exclusiveMinimum = 0;
|
|
1528
|
+
else if (c.check === "negative") out.exclusiveMaximum = 0;
|
|
1529
|
+
}
|
|
1530
|
+
return out;
|
|
1531
|
+
}
|
|
1532
|
+
case "boolean":
|
|
1533
|
+
return { type: "boolean" };
|
|
1534
|
+
case "date":
|
|
1535
|
+
return { type: "string", format: "date-time" };
|
|
1536
|
+
case "unknown":
|
|
1537
|
+
return node.note ? { description: node.note } : {};
|
|
1538
|
+
case "instanceof":
|
|
1539
|
+
return { type: "object", description: `instanceof ${node.ctor}` };
|
|
1540
|
+
case "enum": {
|
|
1541
|
+
const values = node.literals.map(parseLiteral);
|
|
1542
|
+
const t = literalsType(values);
|
|
1543
|
+
const out = { enum: values };
|
|
1544
|
+
if (t) out.type = t;
|
|
1545
|
+
return out;
|
|
1546
|
+
}
|
|
1547
|
+
case "literal": {
|
|
1548
|
+
const value = parseLiteral(node.raw);
|
|
1549
|
+
const out = { const: value };
|
|
1550
|
+
const t = literalsType([value]);
|
|
1551
|
+
if (t) out.type = t;
|
|
1552
|
+
return out;
|
|
1553
|
+
}
|
|
1554
|
+
case "union": {
|
|
1555
|
+
const options = node.options.map((o) => convert(o, ctx));
|
|
1556
|
+
const out = { oneOf: options };
|
|
1557
|
+
if (node.discriminator) {
|
|
1558
|
+
out.discriminator = { propertyName: node.discriminator };
|
|
1559
|
+
}
|
|
1560
|
+
return out;
|
|
1561
|
+
}
|
|
1562
|
+
case "object": {
|
|
1563
|
+
const properties = {};
|
|
1564
|
+
const required = [];
|
|
1565
|
+
for (const f of node.fields) {
|
|
1566
|
+
if (f.value.kind === "optional") {
|
|
1567
|
+
properties[f.key] = convert(f.value.inner, ctx);
|
|
1568
|
+
} else {
|
|
1569
|
+
properties[f.key] = convert(f.value, ctx);
|
|
1570
|
+
required.push(f.key);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const out = { type: "object", properties };
|
|
1574
|
+
if (required.length > 0) out.required = required;
|
|
1575
|
+
out.additionalProperties = node.passthrough;
|
|
1576
|
+
return out;
|
|
1577
|
+
}
|
|
1578
|
+
case "array":
|
|
1579
|
+
return { type: "array", items: convert(node.element, ctx) };
|
|
1580
|
+
case "optional":
|
|
1581
|
+
return widenNullable(convert(node.inner, ctx));
|
|
1582
|
+
case "ref":
|
|
1583
|
+
case "lazyRef":
|
|
1584
|
+
return { $ref: `${ctx.refPrefix}${node.name}` };
|
|
1585
|
+
case "annotated":
|
|
1586
|
+
return convert(node.inner, ctx);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function widenNullable(schema) {
|
|
1590
|
+
if (schema.$ref) {
|
|
1591
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1592
|
+
}
|
|
1593
|
+
if (typeof schema.type === "string") {
|
|
1594
|
+
return { ...schema, type: [schema.type, "null"] };
|
|
1595
|
+
}
|
|
1596
|
+
if (Array.isArray(schema.type)) {
|
|
1597
|
+
return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
|
|
1598
|
+
}
|
|
1599
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1600
|
+
}
|
|
1601
|
+
function schemaNodeToJsonSchema(node, ctx = DEFAULT_CTX) {
|
|
1602
|
+
return convert(node, ctx);
|
|
1603
|
+
}
|
|
1604
|
+
function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
|
|
1605
|
+
const named = {};
|
|
1606
|
+
for (const [name, node] of mod.named) {
|
|
1607
|
+
named[name] = convert(node, ctx);
|
|
1608
|
+
}
|
|
1609
|
+
return { root: convert(mod.root, ctx), named };
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/emit/mock-gen-runtime.ts
|
|
1613
|
+
var MOCK_GEN_RUNTIME = `
|
|
1614
|
+
/** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
|
|
1615
|
+
function makeRng(seed) {
|
|
1616
|
+
let a = seed >>> 0;
|
|
1617
|
+
return {
|
|
1618
|
+
next() {
|
|
1619
|
+
a |= 0;
|
|
1620
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
1621
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
1622
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
1623
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1624
|
+
},
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function __pick(rng, items) {
|
|
1629
|
+
return items[Math.floor(rng.next() * items.length)];
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function __intBetween(rng, min, max) {
|
|
1633
|
+
return Math.floor(rng.next() * (max - min + 1)) + min;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
|
|
1637
|
+
const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
|
|
1638
|
+
const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
|
|
1639
|
+
|
|
1640
|
+
function __fakeWords(rng, count) {
|
|
1641
|
+
let out = [];
|
|
1642
|
+
for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
|
|
1643
|
+
return out.join(' ');
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function __hex(rng, len) {
|
|
1647
|
+
let s = '';
|
|
1648
|
+
for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
|
|
1649
|
+
return s;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function __fakeUuid(rng) {
|
|
1653
|
+
return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function __fakeString(rng, schema) {
|
|
1657
|
+
switch (schema.format) {
|
|
1658
|
+
case 'email':
|
|
1659
|
+
return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
|
|
1660
|
+
case 'uri':
|
|
1661
|
+
case 'url':
|
|
1662
|
+
return 'https://example.com/' + __pick(rng, __WORDS);
|
|
1663
|
+
case 'uuid':
|
|
1664
|
+
return __fakeUuid(rng);
|
|
1665
|
+
case 'date-time':
|
|
1666
|
+
return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
|
|
1667
|
+
default:
|
|
1668
|
+
return __fakeWords(rng, __intBetween(rng, 1, 3));
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
|
|
1673
|
+
function generateMock(schema, rng, defs, depth) {
|
|
1674
|
+
defs = defs || {};
|
|
1675
|
+
depth = depth || 0;
|
|
1676
|
+
if (schema.$ref) {
|
|
1677
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
1678
|
+
const target = defs[name];
|
|
1679
|
+
if (!target || depth > 4) return null;
|
|
1680
|
+
return generateMock(target, rng, defs, depth + 1);
|
|
1681
|
+
}
|
|
1682
|
+
if ('const' in schema) return schema.const;
|
|
1683
|
+
if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
|
|
1684
|
+
if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
|
|
1685
|
+
if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
|
|
1686
|
+
let type = Array.isArray(schema.type)
|
|
1687
|
+
? (schema.type.filter((t) => t !== 'null')[0] || 'null')
|
|
1688
|
+
: schema.type;
|
|
1689
|
+
switch (type) {
|
|
1690
|
+
case 'string':
|
|
1691
|
+
return __fakeString(rng, schema);
|
|
1692
|
+
case 'integer':
|
|
1693
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
|
|
1694
|
+
case 'number':
|
|
1695
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
|
|
1696
|
+
case 'boolean':
|
|
1697
|
+
return rng.next() < 0.5;
|
|
1698
|
+
case 'null':
|
|
1699
|
+
return null;
|
|
1700
|
+
case 'array': {
|
|
1701
|
+
const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
|
|
1702
|
+
const items = schema.items || {};
|
|
1703
|
+
let arr = [];
|
|
1704
|
+
for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
|
|
1705
|
+
return arr;
|
|
1706
|
+
}
|
|
1707
|
+
case 'object': {
|
|
1708
|
+
const out = {};
|
|
1709
|
+
const props = schema.properties || {};
|
|
1710
|
+
for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
|
|
1711
|
+
return out;
|
|
1712
|
+
}
|
|
1713
|
+
default:
|
|
1714
|
+
return {};
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
`.trim();
|
|
1718
|
+
|
|
1719
|
+
// src/emit/emit-mocks.ts
|
|
1720
|
+
var REF_PREFIX = "#/components/schemas/";
|
|
1721
|
+
function toMswPath(path, baseUrl) {
|
|
1722
|
+
return `${baseUrl}${path}`;
|
|
1723
|
+
}
|
|
1724
|
+
function responseSchemaFor(route, defs) {
|
|
1725
|
+
const cs = route.contract.contractSource;
|
|
1726
|
+
if (cs.responseSchema) {
|
|
1727
|
+
const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
|
|
1728
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1729
|
+
if (!(name in defs)) defs[name] = node;
|
|
1730
|
+
}
|
|
1731
|
+
return root;
|
|
1732
|
+
}
|
|
1733
|
+
return {};
|
|
1734
|
+
}
|
|
1735
|
+
function buildMocksFile(routes, opts = {}) {
|
|
1736
|
+
const seed = opts.seed ?? 1;
|
|
1737
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
1738
|
+
const contracted = routes.filter((r) => r.contract);
|
|
1739
|
+
const defs = {};
|
|
1740
|
+
const handlers = [];
|
|
1741
|
+
for (const r of contracted) {
|
|
1742
|
+
const schema = responseSchemaFor(r, defs);
|
|
1743
|
+
const method = r.method.toLowerCase();
|
|
1744
|
+
const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
|
|
1745
|
+
const path = toMswPath(r.path, baseUrl);
|
|
1746
|
+
const cs = r.contract.contractSource;
|
|
1747
|
+
const schemaLiteral = JSON.stringify(schema);
|
|
1748
|
+
const pathLit = JSON.stringify(path);
|
|
1749
|
+
if (cs.stream) {
|
|
1750
|
+
handlers.push(
|
|
1751
|
+
[
|
|
1752
|
+
` // ${r.name} (stream)`,
|
|
1753
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1754
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1755
|
+
" const body = `data: ${JSON.stringify(value)}\\n\\n`;",
|
|
1756
|
+
" return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
|
|
1757
|
+
" }),"
|
|
1758
|
+
].join("\n")
|
|
1759
|
+
);
|
|
1760
|
+
} else {
|
|
1761
|
+
handlers.push(
|
|
1762
|
+
[
|
|
1763
|
+
` // ${r.name}`,
|
|
1764
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1765
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1766
|
+
" return HttpResponse.json(value);",
|
|
1767
|
+
" }),"
|
|
1768
|
+
].join("\n")
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const lines = [
|
|
1773
|
+
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1774
|
+
"// MSW handlers returning deterministic, schema-shaped mock data.",
|
|
1775
|
+
"/* eslint-disable */",
|
|
1776
|
+
"// @ts-nocheck",
|
|
1777
|
+
"",
|
|
1778
|
+
"import { http, HttpResponse } from 'msw';",
|
|
1779
|
+
"",
|
|
1780
|
+
`const SEED = ${seed};`,
|
|
1781
|
+
"",
|
|
1782
|
+
"// ---------------------------------------------------------------------------",
|
|
1783
|
+
"// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
|
|
1784
|
+
"// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
|
|
1785
|
+
"// ---------------------------------------------------------------------------",
|
|
1786
|
+
MOCK_GEN_RUNTIME,
|
|
1787
|
+
"",
|
|
1788
|
+
"// Shared component schemas referenced by $ref.",
|
|
1789
|
+
`const DEFS = ${JSON.stringify(defs, null, 2)};`,
|
|
1790
|
+
"",
|
|
1791
|
+
"/** MSW request handlers, one per contracted route. */",
|
|
1792
|
+
"export const handlers = [",
|
|
1793
|
+
...handlers,
|
|
1794
|
+
"];",
|
|
1795
|
+
""
|
|
1796
|
+
];
|
|
1797
|
+
return lines.join("\n");
|
|
1798
|
+
}
|
|
1799
|
+
async function emitMocks(routes, outDir, opts = {}) {
|
|
1434
1800
|
await (0, import_promises7.mkdir)(outDir, { recursive: true });
|
|
1801
|
+
const content = buildMocksFile(routes, opts);
|
|
1802
|
+
const fileName = opts.fileName ?? "mocks.ts";
|
|
1803
|
+
await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, fileName), content, "utf8");
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/emit/emit-openapi.ts
|
|
1807
|
+
var import_promises8 = require("fs/promises");
|
|
1808
|
+
var import_node_path9 = require("path");
|
|
1809
|
+
var REF_PREFIX2 = "#/components/schemas/";
|
|
1810
|
+
function toOpenApiPath(path) {
|
|
1811
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
1812
|
+
}
|
|
1813
|
+
function positionSchema(schema, tsType, components) {
|
|
1814
|
+
if (schema) {
|
|
1815
|
+
const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
|
|
1816
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1817
|
+
if (!(name in components)) components[name] = node;
|
|
1818
|
+
}
|
|
1819
|
+
return root;
|
|
1820
|
+
}
|
|
1821
|
+
return tsType ? { description: tsType } : {};
|
|
1822
|
+
}
|
|
1823
|
+
function buildParameters(route) {
|
|
1824
|
+
const params = [];
|
|
1825
|
+
for (const p of route.params) {
|
|
1826
|
+
if (p.source === "path") {
|
|
1827
|
+
params.push({
|
|
1828
|
+
name: p.name,
|
|
1829
|
+
in: "path",
|
|
1830
|
+
required: true,
|
|
1831
|
+
schema: { type: "string" }
|
|
1832
|
+
});
|
|
1833
|
+
} else if (p.source === "query") {
|
|
1834
|
+
params.push({
|
|
1835
|
+
name: p.name,
|
|
1836
|
+
in: "query",
|
|
1837
|
+
required: false,
|
|
1838
|
+
schema: { type: "string" }
|
|
1839
|
+
});
|
|
1840
|
+
} else if (p.source === "header") {
|
|
1841
|
+
params.push({
|
|
1842
|
+
name: p.name,
|
|
1843
|
+
in: "header",
|
|
1844
|
+
required: false,
|
|
1845
|
+
schema: { type: "string" }
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return params;
|
|
1850
|
+
}
|
|
1851
|
+
function buildResponses(cs, components) {
|
|
1852
|
+
const responses = {};
|
|
1853
|
+
const successSchema = positionSchema(
|
|
1854
|
+
// Prefer rich response IR when present; otherwise fall back to the TS type.
|
|
1855
|
+
cs.responseSchema ?? null,
|
|
1856
|
+
cs.response,
|
|
1857
|
+
components
|
|
1858
|
+
);
|
|
1859
|
+
const successContentType = cs.stream ? "text/event-stream" : "application/json";
|
|
1860
|
+
responses["200"] = {
|
|
1861
|
+
description: cs.stream ? "Server-sent event stream" : "Successful response",
|
|
1862
|
+
content: { [successContentType]: { schema: successSchema } }
|
|
1863
|
+
};
|
|
1864
|
+
const errorSchema = positionSchema(null, cs.error ?? null, components);
|
|
1865
|
+
const errorBody = {
|
|
1866
|
+
description: "Error response",
|
|
1867
|
+
content: { "application/json": { schema: errorSchema } }
|
|
1868
|
+
};
|
|
1869
|
+
if (cs.error || cs.errorRef) {
|
|
1870
|
+
responses["400"] = errorBody;
|
|
1871
|
+
responses.default = errorBody;
|
|
1872
|
+
} else {
|
|
1873
|
+
responses.default = {
|
|
1874
|
+
description: "Error response",
|
|
1875
|
+
content: { "application/json": { schema: {} } }
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
return responses;
|
|
1879
|
+
}
|
|
1880
|
+
function buildOperation(route, components) {
|
|
1881
|
+
const cs = route.contract.contractSource;
|
|
1882
|
+
const op = {
|
|
1883
|
+
operationId: route.name,
|
|
1884
|
+
parameters: buildParameters(route),
|
|
1885
|
+
responses: buildResponses(cs, components)
|
|
1886
|
+
};
|
|
1887
|
+
const method = route.method.toUpperCase();
|
|
1888
|
+
const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
|
|
1889
|
+
if (hasBody && (cs.bodySchema || cs.body)) {
|
|
1890
|
+
const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
|
|
1891
|
+
op.requestBody = {
|
|
1892
|
+
required: true,
|
|
1893
|
+
content: { "application/json": { schema: bodySchema } }
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
return op;
|
|
1897
|
+
}
|
|
1898
|
+
function buildOpenApiSpec(routes, opts = {}) {
|
|
1899
|
+
const components = {};
|
|
1900
|
+
const paths = {};
|
|
1901
|
+
for (const route of routes) {
|
|
1902
|
+
if (!route.contract) continue;
|
|
1903
|
+
const oaPath = toOpenApiPath(route.path);
|
|
1904
|
+
const method = route.method.toLowerCase();
|
|
1905
|
+
let pathItem = paths[oaPath];
|
|
1906
|
+
if (!pathItem) {
|
|
1907
|
+
pathItem = {};
|
|
1908
|
+
paths[oaPath] = pathItem;
|
|
1909
|
+
}
|
|
1910
|
+
pathItem[method] = buildOperation(route, components);
|
|
1911
|
+
}
|
|
1912
|
+
const info = opts.info ?? {};
|
|
1913
|
+
const doc = {
|
|
1914
|
+
openapi: "3.1.0",
|
|
1915
|
+
info: {
|
|
1916
|
+
title: info.title ?? "NestJS API",
|
|
1917
|
+
version: info.version ?? "1.0.0",
|
|
1918
|
+
...info.description ? { description: info.description } : {}
|
|
1919
|
+
},
|
|
1920
|
+
paths,
|
|
1921
|
+
components: { schemas: components }
|
|
1922
|
+
};
|
|
1923
|
+
return doc;
|
|
1924
|
+
}
|
|
1925
|
+
async function emitOpenApi(routes, outDir, opts = {}) {
|
|
1926
|
+
await (0, import_promises8.mkdir)(outDir, { recursive: true });
|
|
1927
|
+
const doc = buildOpenApiSpec(routes, opts);
|
|
1928
|
+
const fileName = opts.fileName ?? "openapi.json";
|
|
1929
|
+
await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, fileName), `${JSON.stringify(doc, null, 2)}
|
|
1930
|
+
`, "utf8");
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// src/emit/emit-pages.ts
|
|
1934
|
+
var import_promises9 = require("fs/promises");
|
|
1935
|
+
var import_node_path10 = require("path");
|
|
1936
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
1937
|
+
await (0, import_promises9.mkdir)(outDir, { recursive: true });
|
|
1435
1938
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
1436
1939
|
const augBody = pages.map((p) => {
|
|
1437
1940
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -1450,7 +1953,7 @@ ${augBody}
|
|
|
1450
1953
|
}
|
|
1451
1954
|
${sharedPropsBlock}}
|
|
1452
1955
|
`;
|
|
1453
|
-
await (0,
|
|
1956
|
+
await (0, import_promises9.writeFile)((0, import_node_path10.join)(outDir, "pages.d.ts"), content, "utf8");
|
|
1454
1957
|
}
|
|
1455
1958
|
function buildSharedPropsBlock(sharedProps) {
|
|
1456
1959
|
if (!sharedProps) return "";
|
|
@@ -1469,7 +1972,7 @@ ${propsBody}
|
|
|
1469
1972
|
`;
|
|
1470
1973
|
}
|
|
1471
1974
|
function buildAugmentationType(page, outDir) {
|
|
1472
|
-
let importPath = (0,
|
|
1975
|
+
let importPath = (0, import_node_path10.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
|
|
1473
1976
|
if (!importPath.startsWith(".")) {
|
|
1474
1977
|
importPath = `./${importPath}`;
|
|
1475
1978
|
}
|
|
@@ -1480,12 +1983,12 @@ function needsQuotes(name) {
|
|
|
1480
1983
|
}
|
|
1481
1984
|
|
|
1482
1985
|
// src/emit/emit-routes.ts
|
|
1483
|
-
var
|
|
1484
|
-
var
|
|
1986
|
+
var import_promises10 = require("fs/promises");
|
|
1987
|
+
var import_node_path11 = require("path");
|
|
1485
1988
|
async function emitRoutes(routes, outDir) {
|
|
1486
|
-
await (0,
|
|
1989
|
+
await (0, import_promises10.mkdir)(outDir, { recursive: true });
|
|
1487
1990
|
const content = buildRoutesFile(routes);
|
|
1488
|
-
await (0,
|
|
1991
|
+
await (0, import_promises10.writeFile)((0, import_node_path11.join)(outDir, "routes.ts"), content, "utf8");
|
|
1489
1992
|
}
|
|
1490
1993
|
function buildRoutesFile(routes) {
|
|
1491
1994
|
if (routes.length === 0) {
|
|
@@ -1633,24 +2136,41 @@ async function generate(config, inputRoutes = []) {
|
|
|
1633
2136
|
});
|
|
1634
2137
|
}
|
|
1635
2138
|
const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
|
|
2139
|
+
if (hasContracts && config.openapi.enabled) {
|
|
2140
|
+
await emitOpenApi(routes, config.codegen.outDir, {
|
|
2141
|
+
fileName: config.openapi.fileName,
|
|
2142
|
+
info: {
|
|
2143
|
+
title: config.openapi.title,
|
|
2144
|
+
version: config.openapi.version,
|
|
2145
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
if (hasContracts && config.mocks.enabled) {
|
|
2150
|
+
await emitMocks(routes, config.codegen.outDir, {
|
|
2151
|
+
fileName: config.mocks.fileName,
|
|
2152
|
+
seed: config.mocks.seed,
|
|
2153
|
+
baseUrl: config.mocks.baseUrl
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
1636
2156
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
1637
2157
|
if (extensions.length > 0) {
|
|
1638
2158
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1639
2159
|
for (const file of extraFiles) {
|
|
1640
|
-
const dest = (0,
|
|
1641
|
-
await (0,
|
|
1642
|
-
await (0,
|
|
2160
|
+
const dest = (0, import_node_path12.join)(config.codegen.outDir, file.path);
|
|
2161
|
+
await (0, import_promises11.mkdir)((0, import_node_path12.dirname)(dest), { recursive: true });
|
|
2162
|
+
await (0, import_promises11.writeFile)(dest, file.contents, "utf8");
|
|
1643
2163
|
}
|
|
1644
2164
|
}
|
|
1645
2165
|
}
|
|
1646
2166
|
|
|
1647
2167
|
// src/watch/watcher.ts
|
|
1648
|
-
var
|
|
1649
|
-
var
|
|
2168
|
+
var import_promises14 = require("fs/promises");
|
|
2169
|
+
var import_node_path16 = require("path");
|
|
1650
2170
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
1651
2171
|
|
|
1652
2172
|
// src/discovery/contracts-fast.ts
|
|
1653
|
-
var
|
|
2173
|
+
var import_node_path14 = require("path");
|
|
1654
2174
|
var import_fast_glob2 = __toESM(require("fast-glob"), 1);
|
|
1655
2175
|
var import_ts_morph9 = require("ts-morph");
|
|
1656
2176
|
|
|
@@ -1662,7 +2182,7 @@ var import_ts_morph4 = require("ts-morph");
|
|
|
1662
2182
|
|
|
1663
2183
|
// src/discovery/type-ref-resolution.ts
|
|
1664
2184
|
var import_node_fs = require("fs");
|
|
1665
|
-
var
|
|
2185
|
+
var import_node_path13 = require("path");
|
|
1666
2186
|
var import_ts_morph3 = require("ts-morph");
|
|
1667
2187
|
var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
|
|
1668
2188
|
var _ctxByProject = /* @__PURE__ */ new WeakMap();
|
|
@@ -1714,12 +2234,12 @@ function findTypeInFile(name, file) {
|
|
|
1714
2234
|
}
|
|
1715
2235
|
function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
1716
2236
|
if (moduleSpecifier.startsWith(".")) {
|
|
1717
|
-
const dir = (0,
|
|
2237
|
+
const dir = (0, import_node_path13.dirname)(sourceFile.getFilePath());
|
|
1718
2238
|
const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
|
|
1719
2239
|
return [
|
|
1720
|
-
(0,
|
|
1721
|
-
(0,
|
|
1722
|
-
(0,
|
|
2240
|
+
(0, import_node_path13.resolve)(dir, `${noExt}.ts`),
|
|
2241
|
+
(0, import_node_path13.resolve)(dir, `${moduleSpecifier}.ts`),
|
|
2242
|
+
(0, import_node_path13.resolve)(dir, moduleSpecifier, "index.ts")
|
|
1723
2243
|
];
|
|
1724
2244
|
}
|
|
1725
2245
|
const ctx = _ctxFor(project);
|
|
@@ -1740,8 +2260,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
|
1740
2260
|
const rest = moduleSpecifier.slice(prefix.length);
|
|
1741
2261
|
const candidates = [];
|
|
1742
2262
|
for (const mapping of mappings) {
|
|
1743
|
-
const resolved = (0,
|
|
1744
|
-
candidates.push(`${resolved}.ts`, (0,
|
|
2263
|
+
const resolved = (0, import_node_path13.resolve)(baseUrl, mapping.replace("*", rest));
|
|
2264
|
+
candidates.push(`${resolved}.ts`, (0, import_node_path13.resolve)(resolved, "index.ts"));
|
|
1745
2265
|
}
|
|
1746
2266
|
dbg(" resolved candidates:", candidates);
|
|
1747
2267
|
return candidates;
|
|
@@ -1822,10 +2342,85 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
|
|
|
1822
2342
|
}
|
|
1823
2343
|
return null;
|
|
1824
2344
|
}
|
|
2345
|
+
function resolveImportedVariable(name, sourceFile, project) {
|
|
2346
|
+
const local = sourceFile.getVariableDeclaration(name);
|
|
2347
|
+
if (local) return { decl: local, file: sourceFile };
|
|
2348
|
+
return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
|
|
2349
|
+
}
|
|
2350
|
+
function resolveVariableViaImports(name, sourceFile, project, seen) {
|
|
2351
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
2352
|
+
const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
|
|
2353
|
+
if (!namedImport) continue;
|
|
2354
|
+
const sourceName = namedImport.getName();
|
|
2355
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
2356
|
+
const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
|
|
2357
|
+
if (found) return found;
|
|
2358
|
+
}
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
|
|
2362
|
+
const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
|
|
2363
|
+
for (const candidate of candidates) {
|
|
2364
|
+
let importedFile = project.getSourceFile(candidate);
|
|
2365
|
+
if (!importedFile) {
|
|
2366
|
+
try {
|
|
2367
|
+
importedFile = project.addSourceFileAtPath(candidate);
|
|
2368
|
+
} catch {
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
const found = resolveVariableInFile(name, importedFile, project, seen);
|
|
2373
|
+
if (found) return found;
|
|
2374
|
+
}
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
function resolveVariableInFile(name, file, project, seen) {
|
|
2378
|
+
const filePath = file.getFilePath();
|
|
2379
|
+
if (seen.has(filePath)) return null;
|
|
2380
|
+
seen.add(filePath);
|
|
2381
|
+
const local = file.getVariableDeclaration(name);
|
|
2382
|
+
if (local) return { decl: local, file };
|
|
2383
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
2384
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
2385
|
+
const namedExports = exportDecl.getNamedExports();
|
|
2386
|
+
if (moduleSpecifier) {
|
|
2387
|
+
const hasStar = namedExports.length === 0;
|
|
2388
|
+
const reExport2 = namedExports.find(
|
|
2389
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2390
|
+
);
|
|
2391
|
+
if (!hasStar && !reExport2) continue;
|
|
2392
|
+
const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
|
|
2393
|
+
const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
|
|
2394
|
+
if (found) return found;
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
const reExport = namedExports.find(
|
|
2398
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2399
|
+
);
|
|
2400
|
+
if (!reExport) continue;
|
|
2401
|
+
const sourceName = reExport.getName();
|
|
2402
|
+
const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
|
|
2403
|
+
if (viaImports) return viaImports;
|
|
2404
|
+
}
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
2408
|
+
function clearTypeResolutionCaches(project) {
|
|
2409
|
+
_findTypeCache.delete(project);
|
|
2410
|
+
_resolveNamedRefCache.delete(project);
|
|
2411
|
+
}
|
|
1825
2412
|
function findType(name, sourceFile, project) {
|
|
2413
|
+
let byKey = _findTypeCache.get(project);
|
|
2414
|
+
if (byKey === void 0) {
|
|
2415
|
+
byKey = /* @__PURE__ */ new Map();
|
|
2416
|
+
_findTypeCache.set(project, byKey);
|
|
2417
|
+
}
|
|
2418
|
+
const key = `${sourceFile.getFilePath()}\0${name}`;
|
|
2419
|
+
if (byKey.has(key)) return byKey.get(key) ?? null;
|
|
1826
2420
|
const local = findTypeInFile(name, sourceFile);
|
|
1827
|
-
|
|
1828
|
-
|
|
2421
|
+
const result = local ?? resolveImportedType(name, sourceFile, project);
|
|
2422
|
+
byKey.set(key, result);
|
|
2423
|
+
return result;
|
|
1829
2424
|
}
|
|
1830
2425
|
var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
|
|
1831
2426
|
function _localDeclForKinds(name, file, kinds) {
|
|
@@ -1862,6 +2457,26 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
1862
2457
|
if (_NON_REF_NAMES.has(refName)) return null;
|
|
1863
2458
|
name = refName;
|
|
1864
2459
|
}
|
|
2460
|
+
return _resolveNamedRef(name, sourceFile, project, opts);
|
|
2461
|
+
}
|
|
2462
|
+
var _resolveNamedRefCache = /* @__PURE__ */ new WeakMap();
|
|
2463
|
+
function _resolveNamedRef(name, sourceFile, project, opts) {
|
|
2464
|
+
let byKey = _resolveNamedRefCache.get(project);
|
|
2465
|
+
if (byKey === void 0) {
|
|
2466
|
+
byKey = /* @__PURE__ */ new Map();
|
|
2467
|
+
_resolveNamedRefCache.set(project, byKey);
|
|
2468
|
+
}
|
|
2469
|
+
const kindsKey = [...opts.kinds].sort().join(",");
|
|
2470
|
+
const key = `${sourceFile.getFilePath()}\0${name}\0${kindsKey}\0${opts.allowBareSpecifier ? 1 : 0}`;
|
|
2471
|
+
if (byKey.has(key)) {
|
|
2472
|
+
const cached = byKey.get(key) ?? null;
|
|
2473
|
+
return cached ? { ...cached } : null;
|
|
2474
|
+
}
|
|
2475
|
+
const computed = _computeNamedRef(name, sourceFile, project, opts);
|
|
2476
|
+
byKey.set(key, computed);
|
|
2477
|
+
return computed ? { ...computed } : null;
|
|
2478
|
+
}
|
|
2479
|
+
function _computeNamedRef(name, sourceFile, project, opts) {
|
|
1865
2480
|
if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
|
|
1866
2481
|
return { name, filePath: sourceFile.getFilePath() };
|
|
1867
2482
|
}
|
|
@@ -1936,7 +2551,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
1936
2551
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
1937
2552
|
visiting: /* @__PURE__ */ new Set(),
|
|
1938
2553
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
1939
|
-
depth: 0
|
|
2554
|
+
depth: 0,
|
|
2555
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
1940
2556
|
};
|
|
1941
2557
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
1942
2558
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -1960,11 +2576,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
1960
2576
|
const typeNode = prop.getTypeNode();
|
|
1961
2577
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
1962
2578
|
const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
|
|
2579
|
+
const discriminator = resolveDiscriminator(dec("Type"));
|
|
2580
|
+
if (discriminator) {
|
|
2581
|
+
const options = discriminator.subTypes.map(
|
|
2582
|
+
(name) => buildNestedReference(name, classFile, ctx)
|
|
2583
|
+
);
|
|
2584
|
+
const unionNode = {
|
|
2585
|
+
kind: "union",
|
|
2586
|
+
options,
|
|
2587
|
+
discriminator: discriminator.property
|
|
2588
|
+
};
|
|
2589
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2590
|
+
const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
|
|
2591
|
+
return applyPresence(node2, decorators);
|
|
2592
|
+
}
|
|
2593
|
+
const propTypeParam = singularClassName(typeText);
|
|
2594
|
+
if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
|
|
2595
|
+
const bound = ctx.typeBindings.get(propTypeParam);
|
|
2596
|
+
const childNode = buildNestedReference(bound, classFile, ctx);
|
|
2597
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2598
|
+
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
2599
|
+
return applyPresence(node2, decorators);
|
|
2600
|
+
}
|
|
1963
2601
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
1964
2602
|
if (has("ValidateNested") || typeRefName) {
|
|
2603
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
1965
2604
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
1966
2605
|
if (childName) {
|
|
1967
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
2606
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
1968
2607
|
const wrapArray = has("IsArray") || isArrayType;
|
|
1969
2608
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
1970
2609
|
return applyPresence(node2, decorators);
|
|
@@ -2089,10 +2728,12 @@ function baseFromType(typeText, isArrayType) {
|
|
|
2089
2728
|
return { kind: "unknown" };
|
|
2090
2729
|
}
|
|
2091
2730
|
}
|
|
2092
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2731
|
+
function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
|
|
2732
|
+
const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
|
|
2733
|
+
const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
|
|
2734
|
+
if (ctx.visiting.has(cacheKey)) {
|
|
2735
|
+
const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
|
|
2736
|
+
ctx.emittedClasses.set(cacheKey, reserved);
|
|
2096
2737
|
ctx.recursiveSchemas.add(reserved);
|
|
2097
2738
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2098
2739
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -2111,19 +2752,27 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
2111
2752
|
}
|
|
2112
2753
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
2113
2754
|
}
|
|
2114
|
-
const existing = ctx.emittedClasses.get(
|
|
2755
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
2115
2756
|
if (existing) return { kind: "ref", name: existing };
|
|
2116
|
-
const schemaName = aliasFor(
|
|
2757
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
2117
2758
|
const resolved = findType(className, fromFile, ctx.project);
|
|
2118
2759
|
if (!resolved || resolved.kind !== "class") {
|
|
2119
2760
|
return { kind: "object", fields: [], passthrough: true };
|
|
2120
2761
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2762
|
+
const params = resolved.decl.getTypeParameters().map((p) => p.getName());
|
|
2763
|
+
const newBindings = [];
|
|
2764
|
+
params.forEach((param, i) => {
|
|
2765
|
+
const arg = typeArgs[i];
|
|
2766
|
+
if (arg) newBindings.push([param, arg]);
|
|
2767
|
+
});
|
|
2768
|
+
for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
|
|
2769
|
+
ctx.emittedClasses.set(cacheKey, schemaName);
|
|
2770
|
+
ctx.visiting.add(cacheKey);
|
|
2123
2771
|
ctx.depth += 1;
|
|
2124
2772
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
2125
2773
|
ctx.depth -= 1;
|
|
2126
|
-
ctx.visiting.delete(
|
|
2774
|
+
ctx.visiting.delete(cacheKey);
|
|
2775
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
2127
2776
|
ctx.named.set(schemaName, childNode);
|
|
2128
2777
|
return { kind: "ref", name: schemaName };
|
|
2129
2778
|
}
|
|
@@ -2170,6 +2819,39 @@ function messageRaw(decorator) {
|
|
|
2170
2819
|
}
|
|
2171
2820
|
return void 0;
|
|
2172
2821
|
}
|
|
2822
|
+
function resolveDiscriminator(decorator) {
|
|
2823
|
+
const optsArg = decorator?.getArguments()[1];
|
|
2824
|
+
if (!optsArg || !import_ts_morph4.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
2825
|
+
let discProp;
|
|
2826
|
+
for (const prop of optsArg.getProperties()) {
|
|
2827
|
+
if (import_ts_morph4.Node.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
|
|
2828
|
+
discProp = prop.getInitializer();
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
if (!discProp || !import_ts_morph4.Node.isObjectLiteralExpression(discProp)) return null;
|
|
2832
|
+
let property = null;
|
|
2833
|
+
const subTypes = [];
|
|
2834
|
+
for (const prop of discProp.getProperties()) {
|
|
2835
|
+
if (!import_ts_morph4.Node.isPropertyAssignment(prop)) continue;
|
|
2836
|
+
const name = prop.getName();
|
|
2837
|
+
const init = prop.getInitializer();
|
|
2838
|
+
if (!init) continue;
|
|
2839
|
+
if (name === "property" && import_ts_morph4.Node.isStringLiteral(init)) {
|
|
2840
|
+
property = init.getLiteralValue();
|
|
2841
|
+
} else if (name === "subTypes" && import_ts_morph4.Node.isArrayLiteralExpression(init)) {
|
|
2842
|
+
for (const el of init.getElements()) {
|
|
2843
|
+
if (!import_ts_morph4.Node.isObjectLiteralExpression(el)) continue;
|
|
2844
|
+
for (const p of el.getProperties()) {
|
|
2845
|
+
if (!import_ts_morph4.Node.isPropertyAssignment(p) || p.getName() !== "name") continue;
|
|
2846
|
+
const nameInit = p.getInitializer();
|
|
2847
|
+
if (nameInit && import_ts_morph4.Node.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
if (!property || subTypes.length === 0) return null;
|
|
2853
|
+
return { property, subTypes };
|
|
2854
|
+
}
|
|
2173
2855
|
function resolveTypeFactoryName(decorator) {
|
|
2174
2856
|
const arg = firstArg(decorator);
|
|
2175
2857
|
if (!arg) return null;
|
|
@@ -2183,6 +2865,17 @@ function singularClassName(typeText) {
|
|
|
2183
2865
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2184
2866
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2185
2867
|
}
|
|
2868
|
+
function genericTypeArgNames(typeNode) {
|
|
2869
|
+
if (!typeNode || !import_ts_morph4.Node.isTypeReference(typeNode)) return [];
|
|
2870
|
+
const names = [];
|
|
2871
|
+
for (const arg of typeNode.getTypeArguments()) {
|
|
2872
|
+
if (!import_ts_morph4.Node.isTypeReference(arg)) return [];
|
|
2873
|
+
const tn = arg.getTypeName();
|
|
2874
|
+
if (!import_ts_morph4.Node.isIdentifier(tn)) return [];
|
|
2875
|
+
names.push(tn.getText());
|
|
2876
|
+
}
|
|
2877
|
+
return names;
|
|
2878
|
+
}
|
|
2186
2879
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
2187
2880
|
const arg = firstArg(decorator);
|
|
2188
2881
|
if (!arg) return null;
|
|
@@ -2236,17 +2929,34 @@ var import_ts_morph6 = require("ts-morph");
|
|
|
2236
2929
|
var import_ts_morph5 = require("ts-morph");
|
|
2237
2930
|
|
|
2238
2931
|
// src/discovery/enum-resolution.ts
|
|
2932
|
+
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
2933
|
+
function clearEnumCache(project) {
|
|
2934
|
+
_enumCache.delete(project);
|
|
2935
|
+
}
|
|
2239
2936
|
function resolveEnumValues(name, sourceFile, project) {
|
|
2937
|
+
let byKey = _enumCache.get(project);
|
|
2938
|
+
if (byKey === void 0) {
|
|
2939
|
+
byKey = /* @__PURE__ */ new Map();
|
|
2940
|
+
_enumCache.set(project, byKey);
|
|
2941
|
+
}
|
|
2942
|
+
const key = `${sourceFile.getFilePath()}\0${name}`;
|
|
2943
|
+
if (byKey.has(key)) {
|
|
2944
|
+
const cached = byKey.get(key) ?? null;
|
|
2945
|
+
return cached ? { values: [...cached.values], numeric: cached.numeric } : null;
|
|
2946
|
+
}
|
|
2240
2947
|
const resolved = findType(name, sourceFile, project);
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
const
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2948
|
+
let result = null;
|
|
2949
|
+
if (resolved && resolved.kind === "enum") {
|
|
2950
|
+
let numeric = true;
|
|
2951
|
+
const values = resolved.members.map((m) => {
|
|
2952
|
+
const parsed = JSON.parse(m);
|
|
2953
|
+
if (typeof parsed === "string") numeric = false;
|
|
2954
|
+
return String(parsed);
|
|
2955
|
+
});
|
|
2956
|
+
if (values.length > 0) result = { values, numeric };
|
|
2957
|
+
}
|
|
2958
|
+
byKey.set(key, result);
|
|
2959
|
+
return result ? { values: [...result.values], numeric: result.numeric } : null;
|
|
2250
2960
|
}
|
|
2251
2961
|
|
|
2252
2962
|
// src/discovery/filter-field-types.ts
|
|
@@ -2691,24 +3401,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
2691
3401
|
"Map",
|
|
2692
3402
|
"Set"
|
|
2693
3403
|
]);
|
|
2694
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3404
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2695
3405
|
if (depth <= 0) return "unknown";
|
|
2696
3406
|
if (import_ts_morph7.Node.isArrayTypeNode(typeNode)) {
|
|
2697
3407
|
const elementType = typeNode.getElementTypeNode();
|
|
2698
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3408
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
2699
3409
|
}
|
|
2700
3410
|
if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
|
|
2701
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3411
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
2702
3412
|
}
|
|
2703
3413
|
if (import_ts_morph7.Node.isIntersectionTypeNode(typeNode)) {
|
|
2704
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3414
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
2705
3415
|
}
|
|
2706
3416
|
if (import_ts_morph7.Node.isParenthesizedTypeNode(typeNode)) {
|
|
2707
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3417
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
2708
3418
|
}
|
|
2709
3419
|
if (import_ts_morph7.Node.isTypeReference(typeNode)) {
|
|
2710
3420
|
const typeName = typeNode.getTypeName();
|
|
2711
3421
|
const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3422
|
+
const bound = subst.get(name);
|
|
3423
|
+
if (bound !== void 0) return bound;
|
|
2712
3424
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
2713
3425
|
if (name === "Date") return "string";
|
|
2714
3426
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -2716,14 +3428,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2716
3428
|
return "unknown";
|
|
2717
3429
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
2718
3430
|
if (wrapperMode) {
|
|
2719
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3431
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
2720
3432
|
}
|
|
2721
3433
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
2722
3434
|
return typeNode.getText();
|
|
2723
3435
|
}
|
|
2724
3436
|
const resolved = findType(name, sourceFile, project);
|
|
2725
3437
|
if (resolved) {
|
|
2726
|
-
|
|
3438
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
3439
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
2727
3440
|
}
|
|
2728
3441
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
2729
3442
|
return "unknown";
|
|
@@ -2736,32 +3449,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2736
3449
|
if (kind === import_ts_morph7.SyntaxKind.AnyKeyword) return "unknown";
|
|
2737
3450
|
return typeNode.getText();
|
|
2738
3451
|
}
|
|
2739
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
3452
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
2740
3453
|
const typeArgs = typeNode.getTypeArguments();
|
|
2741
3454
|
const firstTypeArg = typeArgs[0];
|
|
2742
3455
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2743
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3456
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
2744
3457
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2745
3458
|
}
|
|
2746
3459
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2747
3460
|
}
|
|
2748
|
-
function
|
|
3461
|
+
function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
|
|
3462
|
+
if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
|
|
3463
|
+
const params = result.decl.getTypeParameters().map((p) => p.getName());
|
|
3464
|
+
if (params.length === 0) return /* @__PURE__ */ new Map();
|
|
3465
|
+
const args = typeNode.getTypeArguments();
|
|
3466
|
+
const subst = /* @__PURE__ */ new Map();
|
|
3467
|
+
params.forEach((param, i) => {
|
|
3468
|
+
const arg = args[i];
|
|
3469
|
+
if (arg)
|
|
3470
|
+
subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
|
|
3471
|
+
});
|
|
3472
|
+
return subst;
|
|
3473
|
+
}
|
|
3474
|
+
function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2749
3475
|
if (depth < 0) return "unknown";
|
|
2750
3476
|
switch (result.kind) {
|
|
2751
3477
|
case "class":
|
|
2752
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3478
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2753
3479
|
case "interface":
|
|
2754
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3480
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2755
3481
|
case "typeAlias":
|
|
2756
3482
|
if (result.typeNode) {
|
|
2757
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
3483
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
2758
3484
|
}
|
|
2759
3485
|
return result.text;
|
|
2760
3486
|
case "enum":
|
|
2761
3487
|
return result.members.join(" | ");
|
|
2762
3488
|
}
|
|
2763
3489
|
}
|
|
2764
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
3490
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2765
3491
|
if (depth < 0) return "unknown";
|
|
2766
3492
|
const lines = [];
|
|
2767
3493
|
for (const prop of decl.getProperties()) {
|
|
@@ -2770,7 +3496,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
2770
3496
|
const propTypeNode = prop.getTypeNode();
|
|
2771
3497
|
let propType = "unknown";
|
|
2772
3498
|
if (propTypeNode) {
|
|
2773
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
3499
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
2774
3500
|
}
|
|
2775
3501
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
2776
3502
|
}
|
|
@@ -2819,7 +3545,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
2819
3545
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
2820
3546
|
}
|
|
2821
3547
|
function extractResponseType(method, sourceFile, project) {
|
|
2822
|
-
const apiResponseDecorator = method.
|
|
3548
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2823
3549
|
if (apiResponseDecorator) {
|
|
2824
3550
|
const args = apiResponseDecorator.getArguments();
|
|
2825
3551
|
const optsArg = args[0];
|
|
@@ -2848,6 +3574,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
2848
3574
|
}
|
|
2849
3575
|
return "unknown";
|
|
2850
3576
|
}
|
|
3577
|
+
function apiResponseStatus(decorator) {
|
|
3578
|
+
const optsArg = decorator.getArguments()[0];
|
|
3579
|
+
if (!optsArg || !import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
3580
|
+
for (const prop of optsArg.getProperties()) {
|
|
3581
|
+
if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
|
|
3582
|
+
if (prop.getName() !== "status") continue;
|
|
3583
|
+
const val = prop.getInitializer();
|
|
3584
|
+
if (val && import_ts_morph7.Node.isNumericLiteral(val)) return Number(val.getLiteralValue());
|
|
3585
|
+
}
|
|
3586
|
+
return null;
|
|
3587
|
+
}
|
|
3588
|
+
function apiResponseTypeNode(decorator) {
|
|
3589
|
+
const optsArg = decorator.getArguments()[0];
|
|
3590
|
+
if (!optsArg || !import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
3591
|
+
for (const prop of optsArg.getProperties()) {
|
|
3592
|
+
if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
|
|
3593
|
+
if (prop.getName() !== "type") continue;
|
|
3594
|
+
const val = prop.getInitializer();
|
|
3595
|
+
if (!val) return null;
|
|
3596
|
+
if (import_ts_morph7.Node.isArrayLiteralExpression(val)) {
|
|
3597
|
+
const first = val.getElements()[0];
|
|
3598
|
+
return first ? { node: first, isArray: true } : null;
|
|
3599
|
+
}
|
|
3600
|
+
return { node: val, isArray: false };
|
|
3601
|
+
}
|
|
3602
|
+
return null;
|
|
3603
|
+
}
|
|
3604
|
+
function extractErrorType(method, sourceFile, project) {
|
|
3605
|
+
for (const decorator of method.getDecorators()) {
|
|
3606
|
+
if (decorator.getName() !== "ApiResponse") continue;
|
|
3607
|
+
const status = apiResponseStatus(decorator);
|
|
3608
|
+
if (status === null || status < 400) continue;
|
|
3609
|
+
const typeInfo = apiResponseTypeNode(decorator);
|
|
3610
|
+
if (!typeInfo) continue;
|
|
3611
|
+
const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
|
|
3612
|
+
const type = typeInfo.isArray ? `Array<${inner}>` : inner;
|
|
3613
|
+
let ref = null;
|
|
3614
|
+
if (import_ts_morph7.Node.isIdentifier(typeInfo.node)) {
|
|
3615
|
+
const name = typeInfo.node.getText();
|
|
3616
|
+
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3617
|
+
if (localDecl?.isExported()) {
|
|
3618
|
+
ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
|
|
3619
|
+
} else {
|
|
3620
|
+
const resolved = resolveImportedType(name, sourceFile, project);
|
|
3621
|
+
if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
|
|
3622
|
+
ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
return { type, ref };
|
|
3627
|
+
}
|
|
3628
|
+
return null;
|
|
3629
|
+
}
|
|
2851
3630
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
2852
3631
|
if (!import_ts_morph7.Node.isIdentifier(node)) return "unknown";
|
|
2853
3632
|
const name = node.getText();
|
|
@@ -2863,17 +3642,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
2863
3642
|
unwrapContainers: true
|
|
2864
3643
|
});
|
|
2865
3644
|
}
|
|
3645
|
+
var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
|
|
3646
|
+
var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
|
|
3647
|
+
var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
|
|
3648
|
+
function detectStreamElement(method) {
|
|
3649
|
+
const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
|
|
3650
|
+
let node = method.getReturnTypeNode();
|
|
3651
|
+
node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
|
|
3652
|
+
const containerEl = streamContainerElement(node);
|
|
3653
|
+
if (containerEl) {
|
|
3654
|
+
return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
|
|
3655
|
+
}
|
|
3656
|
+
if (hasSse) return node ?? null;
|
|
3657
|
+
return null;
|
|
3658
|
+
}
|
|
3659
|
+
function streamContainerElement(node) {
|
|
3660
|
+
if (!node || !import_ts_morph7.Node.isTypeReference(node)) return null;
|
|
3661
|
+
const typeName = node.getTypeName();
|
|
3662
|
+
const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3663
|
+
if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
|
|
3664
|
+
return node.getTypeArguments()[0] ?? null;
|
|
3665
|
+
}
|
|
3666
|
+
return null;
|
|
3667
|
+
}
|
|
3668
|
+
function unwrapNamedContainer(node, names) {
|
|
3669
|
+
if (!node || !import_ts_morph7.Node.isTypeReference(node)) return node;
|
|
3670
|
+
const typeName = node.getTypeName();
|
|
3671
|
+
const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3672
|
+
if (names.has(name)) {
|
|
3673
|
+
return node.getTypeArguments()[0] ?? node;
|
|
3674
|
+
}
|
|
3675
|
+
return node;
|
|
3676
|
+
}
|
|
2866
3677
|
function extractDtoContract(method, sourceFile, project) {
|
|
2867
3678
|
let body = extractBodyType(method, sourceFile, project);
|
|
2868
3679
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
2869
3680
|
const query = extractQueryType(method, sourceFile, project);
|
|
3681
|
+
const streamElement = detectStreamElement(method);
|
|
3682
|
+
const isStream = streamElement !== null;
|
|
2870
3683
|
if (filterInfo && filterInfo.source === "body") {
|
|
2871
3684
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
2872
3685
|
body = body ?? bodyType;
|
|
2873
3686
|
}
|
|
2874
3687
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
2875
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
2876
|
-
|
|
3688
|
+
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
3689
|
+
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
3690
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
2877
3691
|
return null;
|
|
2878
3692
|
}
|
|
2879
3693
|
let bodyRef = null;
|
|
@@ -2887,12 +3701,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2887
3701
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
2888
3702
|
}
|
|
2889
3703
|
}
|
|
2890
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
3704
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
2891
3705
|
if (returnTypeNode) {
|
|
2892
3706
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
2893
3707
|
}
|
|
2894
|
-
if (!responseRef) {
|
|
2895
|
-
const apiResp = method.
|
|
3708
|
+
if (!responseRef && !isStream) {
|
|
3709
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2896
3710
|
if (apiResp) {
|
|
2897
3711
|
const args = apiResp.getArguments();
|
|
2898
3712
|
const optsArg = args[0];
|
|
@@ -2934,16 +3748,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2934
3748
|
query,
|
|
2935
3749
|
body,
|
|
2936
3750
|
response,
|
|
3751
|
+
error: errorInfo?.type ?? null,
|
|
2937
3752
|
params: paramsType,
|
|
2938
3753
|
queryRef,
|
|
2939
3754
|
bodyRef,
|
|
2940
3755
|
responseRef,
|
|
3756
|
+
errorRef: errorInfo?.ref ?? null,
|
|
2941
3757
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2942
3758
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2943
3759
|
filterSource: filterInfo?.source ?? null,
|
|
2944
3760
|
formWarnings,
|
|
2945
3761
|
bodySchema,
|
|
2946
|
-
querySchema
|
|
3762
|
+
querySchema,
|
|
3763
|
+
stream: isStream
|
|
2947
3764
|
};
|
|
2948
3765
|
}
|
|
2949
3766
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -3061,6 +3878,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
3061
3878
|
let query = null;
|
|
3062
3879
|
let body = null;
|
|
3063
3880
|
let response = "unknown";
|
|
3881
|
+
let error = null;
|
|
3064
3882
|
let bodyZodText = null;
|
|
3065
3883
|
let queryZodText = null;
|
|
3066
3884
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -3076,25 +3894,38 @@ function parseDefineContractCall(callExpr) {
|
|
|
3076
3894
|
bodyZodText = val.getText();
|
|
3077
3895
|
} else if (propName === "response") {
|
|
3078
3896
|
response = zodAstToTs(val);
|
|
3897
|
+
} else if (propName === "error") {
|
|
3898
|
+
error = zodAstToTs(val);
|
|
3079
3899
|
}
|
|
3080
3900
|
}
|
|
3081
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3901
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
3082
3902
|
}
|
|
3083
3903
|
|
|
3084
3904
|
// src/discovery/contracts-fast.ts
|
|
3085
3905
|
async function discoverContractsFast(opts) {
|
|
3086
3906
|
const { cwd, glob, tsconfig } = opts;
|
|
3087
|
-
const tsconfigPath =
|
|
3088
|
-
|
|
3907
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3908
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3909
|
+
const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3910
|
+
for (const f of files) {
|
|
3911
|
+
project.addSourceFileAtPath(f);
|
|
3912
|
+
}
|
|
3913
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3914
|
+
return extractAllRoutes(project);
|
|
3915
|
+
}
|
|
3916
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
3917
|
+
return tsconfig ? (0, import_node_path14.resolve)(tsconfig) : (0, import_node_path14.join)(cwd, "tsconfig.json");
|
|
3918
|
+
}
|
|
3919
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
3089
3920
|
try {
|
|
3090
|
-
|
|
3921
|
+
return new import_ts_morph9.Project({
|
|
3091
3922
|
tsConfigFilePath: tsconfigPath,
|
|
3092
3923
|
skipAddingFilesFromTsConfig: true,
|
|
3093
3924
|
skipLoadingLibFiles: true,
|
|
3094
3925
|
skipFileDependencyResolution: true
|
|
3095
3926
|
});
|
|
3096
3927
|
} catch {
|
|
3097
|
-
|
|
3928
|
+
return new import_ts_morph9.Project({
|
|
3098
3929
|
skipAddingFilesFromTsConfig: true,
|
|
3099
3930
|
skipLoadingLibFiles: true,
|
|
3100
3931
|
skipFileDependencyResolution: true,
|
|
@@ -3105,20 +3936,105 @@ async function discoverContractsFast(opts) {
|
|
|
3105
3936
|
}
|
|
3106
3937
|
});
|
|
3107
3938
|
}
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
project.addSourceFileAtPath(f);
|
|
3111
|
-
}
|
|
3112
|
-
const routes = [];
|
|
3939
|
+
}
|
|
3940
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
3113
3941
|
setDiscoveryContext(project, {
|
|
3114
3942
|
projectRoot: cwd,
|
|
3115
3943
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3116
3944
|
});
|
|
3945
|
+
}
|
|
3946
|
+
function extractRoutesFrom(project, controllerPaths) {
|
|
3947
|
+
const routes = [];
|
|
3948
|
+
for (const path of controllerPaths) {
|
|
3949
|
+
const sourceFile = project.getSourceFile(path);
|
|
3950
|
+
if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3951
|
+
}
|
|
3952
|
+
return routes;
|
|
3953
|
+
}
|
|
3954
|
+
function extractAllRoutes(project) {
|
|
3955
|
+
const routes = [];
|
|
3117
3956
|
for (const sourceFile of project.getSourceFiles()) {
|
|
3118
3957
|
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3119
3958
|
}
|
|
3120
3959
|
return routes;
|
|
3121
3960
|
}
|
|
3961
|
+
var PersistentDiscovery = class _PersistentDiscovery {
|
|
3962
|
+
project;
|
|
3963
|
+
cwd;
|
|
3964
|
+
glob;
|
|
3965
|
+
/** Absolute paths of the controllers currently loaded as extraction roots. */
|
|
3966
|
+
controllerPaths = /* @__PURE__ */ new Set();
|
|
3967
|
+
constructor(project, cwd, glob) {
|
|
3968
|
+
this.project = project;
|
|
3969
|
+
this.cwd = cwd;
|
|
3970
|
+
this.glob = glob;
|
|
3971
|
+
}
|
|
3972
|
+
/**
|
|
3973
|
+
* Build the initial persistent Project: create it, glob + add all controllers,
|
|
3974
|
+
* bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
|
|
3975
|
+
*/
|
|
3976
|
+
static async create(opts) {
|
|
3977
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3978
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3979
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3980
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3981
|
+
const instance = new _PersistentDiscovery(project, cwd, glob);
|
|
3982
|
+
const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3983
|
+
for (const f of files) {
|
|
3984
|
+
project.addSourceFileAtPath(f);
|
|
3985
|
+
instance.controllerPaths.add(f);
|
|
3986
|
+
}
|
|
3987
|
+
return instance;
|
|
3988
|
+
}
|
|
3989
|
+
/** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
|
|
3990
|
+
discover() {
|
|
3991
|
+
return this.runExtraction();
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Re-discover after one or more files changed. Refreshes the changed file(s)
|
|
3995
|
+
* from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
|
|
3996
|
+
* to pick up added/removed controllers, clears the per-Project caches, then
|
|
3997
|
+
* re-extracts. `changedPaths` is a hint; correctness does not depend on it
|
|
3998
|
+
* being exhaustive because re-globbing + refresh-on-presence covers the set.
|
|
3999
|
+
*/
|
|
4000
|
+
async rediscover(changedPaths) {
|
|
4001
|
+
if (changedPaths) {
|
|
4002
|
+
for (const p of changedPaths) {
|
|
4003
|
+
const abs = (0, import_node_path14.resolve)(p);
|
|
4004
|
+
const sf = this.project.getSourceFile(abs);
|
|
4005
|
+
if (sf) {
|
|
4006
|
+
await sf.refreshFromFileSystem();
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
const globbed = new Set(
|
|
4011
|
+
await (0, import_fast_glob2.default)(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
|
|
4012
|
+
);
|
|
4013
|
+
for (const f of globbed) {
|
|
4014
|
+
if (!this.controllerPaths.has(f)) {
|
|
4015
|
+
try {
|
|
4016
|
+
this.project.addSourceFileAtPath(f);
|
|
4017
|
+
this.controllerPaths.add(f);
|
|
4018
|
+
} catch {
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
for (const f of this.controllerPaths) {
|
|
4023
|
+
if (!globbed.has(f)) {
|
|
4024
|
+
const sf = this.project.getSourceFile(f);
|
|
4025
|
+
if (sf) this.project.removeSourceFile(sf);
|
|
4026
|
+
this.controllerPaths.delete(f);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
return this.runExtraction();
|
|
4030
|
+
}
|
|
4031
|
+
/** Clear stale per-Project caches, then extract over the controller set. */
|
|
4032
|
+
runExtraction() {
|
|
4033
|
+
clearTypeResolutionCaches(this.project);
|
|
4034
|
+
clearEnumCache(this.project);
|
|
4035
|
+
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
3122
4038
|
function decoratorStringArg(decoratorExpr) {
|
|
3123
4039
|
if (!decoratorExpr) return void 0;
|
|
3124
4040
|
if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -3174,6 +4090,11 @@ function resolveVerb(method) {
|
|
|
3174
4090
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3175
4091
|
}
|
|
3176
4092
|
}
|
|
4093
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
4094
|
+
if (sseDecorator) {
|
|
4095
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
4096
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
4097
|
+
}
|
|
3177
4098
|
return null;
|
|
3178
4099
|
}
|
|
3179
4100
|
function readAsDecorator(node, label) {
|
|
@@ -3216,7 +4137,17 @@ function buildRoute(args) {
|
|
|
3216
4137
|
};
|
|
3217
4138
|
}
|
|
3218
4139
|
function extractContractRoute(args) {
|
|
3219
|
-
const {
|
|
4140
|
+
const {
|
|
4141
|
+
cls,
|
|
4142
|
+
method,
|
|
4143
|
+
applyContractDecorator,
|
|
4144
|
+
verb,
|
|
4145
|
+
prefix,
|
|
4146
|
+
className,
|
|
4147
|
+
sourceFile,
|
|
4148
|
+
project,
|
|
4149
|
+
seenNames
|
|
4150
|
+
} = args;
|
|
3220
4151
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3221
4152
|
if (!firstDecoratorArg) return null;
|
|
3222
4153
|
let contractDef = null;
|
|
@@ -3226,18 +4157,19 @@ function extractContractRoute(args) {
|
|
|
3226
4157
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3227
4158
|
} else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
|
|
3228
4159
|
const identName = firstDecoratorArg.getText();
|
|
3229
|
-
const
|
|
3230
|
-
if (!
|
|
4160
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
4161
|
+
if (!resolvedVar) {
|
|
3231
4162
|
console.warn(
|
|
3232
|
-
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()}
|
|
4163
|
+
`[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
|
|
3233
4164
|
);
|
|
3234
4165
|
return null;
|
|
3235
4166
|
}
|
|
4167
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
3236
4168
|
const initializer = varDecl.getInitializer();
|
|
3237
4169
|
if (!initializer) return null;
|
|
3238
4170
|
contractDef = parseDefineContractCall(initializer);
|
|
3239
4171
|
if (contractDef && varDecl.isExported()) {
|
|
3240
|
-
const filePath =
|
|
4172
|
+
const filePath = declFile.getFilePath();
|
|
3241
4173
|
if (contractDef.body !== null) {
|
|
3242
4174
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3243
4175
|
}
|
|
@@ -3270,6 +4202,7 @@ function extractContractRoute(args) {
|
|
|
3270
4202
|
query: contractDef.query,
|
|
3271
4203
|
body: contractDef.body,
|
|
3272
4204
|
response: contractDef.response,
|
|
4205
|
+
error: contractDef.error,
|
|
3273
4206
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3274
4207
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3275
4208
|
// would drag server-only deps into the client bundle).
|
|
@@ -3301,15 +4234,18 @@ function extractDtoRoute(args) {
|
|
|
3301
4234
|
query: dtoContract?.query ?? null,
|
|
3302
4235
|
body: dtoContract?.body ?? null,
|
|
3303
4236
|
response: dtoContract?.response ?? "unknown",
|
|
4237
|
+
error: dtoContract?.error ?? null,
|
|
3304
4238
|
queryRef: dtoContract?.queryRef ?? null,
|
|
3305
4239
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3306
4240
|
responseRef: dtoContract?.responseRef ?? null,
|
|
4241
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
3307
4242
|
filterFields: dtoContract?.filterFields ?? null,
|
|
3308
4243
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3309
4244
|
filterSource: dtoContract?.filterSource ?? null,
|
|
3310
4245
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3311
4246
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3312
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
4247
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
4248
|
+
stream: dtoContract?.stream ?? false
|
|
3313
4249
|
}
|
|
3314
4250
|
});
|
|
3315
4251
|
}
|
|
@@ -3333,6 +4269,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3333
4269
|
prefix,
|
|
3334
4270
|
className,
|
|
3335
4271
|
sourceFile,
|
|
4272
|
+
project,
|
|
3336
4273
|
seenNames
|
|
3337
4274
|
}) : extractDtoRoute({
|
|
3338
4275
|
cls,
|
|
@@ -3351,9 +4288,9 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3351
4288
|
}
|
|
3352
4289
|
|
|
3353
4290
|
// src/watch/lock-file.ts
|
|
3354
|
-
var
|
|
3355
|
-
var
|
|
3356
|
-
var
|
|
4291
|
+
var import_promises12 = require("fs/promises");
|
|
4292
|
+
var import_promises13 = require("fs/promises");
|
|
4293
|
+
var import_node_path15 = require("path");
|
|
3357
4294
|
var LOCK_FILE = ".watcher.lock";
|
|
3358
4295
|
function isProcessAlive(pid) {
|
|
3359
4296
|
try {
|
|
@@ -3364,21 +4301,21 @@ function isProcessAlive(pid) {
|
|
|
3364
4301
|
}
|
|
3365
4302
|
}
|
|
3366
4303
|
async function acquireLock(outDir) {
|
|
3367
|
-
await (0,
|
|
3368
|
-
const lockPath = (0,
|
|
4304
|
+
await (0, import_promises13.mkdir)(outDir, { recursive: true });
|
|
4305
|
+
const lockPath = (0, import_node_path15.join)(outDir, LOCK_FILE);
|
|
3369
4306
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3370
4307
|
try {
|
|
3371
|
-
const fd = await (0,
|
|
4308
|
+
const fd = await (0, import_promises12.open)(lockPath, "wx");
|
|
3372
4309
|
await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
|
|
3373
4310
|
`, "utf8");
|
|
3374
4311
|
await fd.close();
|
|
3375
4312
|
} catch (err) {
|
|
3376
4313
|
if (err.code === "EEXIST") {
|
|
3377
4314
|
try {
|
|
3378
|
-
const raw = await (0,
|
|
4315
|
+
const raw = await (0, import_promises13.readFile)(lockPath, "utf8");
|
|
3379
4316
|
const existing = JSON.parse(raw);
|
|
3380
4317
|
if (isProcessAlive(existing.pid)) return null;
|
|
3381
|
-
await (0,
|
|
4318
|
+
await (0, import_promises13.unlink)(lockPath);
|
|
3382
4319
|
return acquireLock(outDir);
|
|
3383
4320
|
} catch {
|
|
3384
4321
|
return null;
|
|
@@ -3389,7 +4326,7 @@ async function acquireLock(outDir) {
|
|
|
3389
4326
|
return {
|
|
3390
4327
|
release: async () => {
|
|
3391
4328
|
try {
|
|
3392
|
-
await (0,
|
|
4329
|
+
await (0, import_promises13.unlink)(lockPath);
|
|
3393
4330
|
} catch {
|
|
3394
4331
|
}
|
|
3395
4332
|
}
|
|
@@ -3405,7 +4342,7 @@ async function watch(config, onChange) {
|
|
|
3405
4342
|
if (lock === null) {
|
|
3406
4343
|
let holderPid = "unknown";
|
|
3407
4344
|
try {
|
|
3408
|
-
const raw = await (0,
|
|
4345
|
+
const raw = await (0, import_promises14.readFile)((0, import_node_path16.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3409
4346
|
const data = JSON.parse(raw);
|
|
3410
4347
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3411
4348
|
} catch {
|
|
@@ -3415,12 +4352,20 @@ async function watch(config, onChange) {
|
|
|
3415
4352
|
);
|
|
3416
4353
|
return NO_OP_WATCHER;
|
|
3417
4354
|
}
|
|
4355
|
+
let discovery = null;
|
|
4356
|
+
async function getDiscovery() {
|
|
4357
|
+
if (discovery === null) {
|
|
4358
|
+
discovery = await PersistentDiscovery.create({
|
|
4359
|
+
cwd: config.codegen.cwd,
|
|
4360
|
+
glob: config.contracts.glob,
|
|
4361
|
+
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
4362
|
+
});
|
|
4363
|
+
return discovery;
|
|
4364
|
+
}
|
|
4365
|
+
return discovery;
|
|
4366
|
+
}
|
|
3418
4367
|
try {
|
|
3419
|
-
const initialRoutes = await
|
|
3420
|
-
cwd: config.codegen.cwd,
|
|
3421
|
-
glob: config.contracts.glob,
|
|
3422
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3423
|
-
});
|
|
4368
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3424
4369
|
await generate(config, initialRoutes);
|
|
3425
4370
|
} catch (err) {
|
|
3426
4371
|
console.warn(
|
|
@@ -3433,7 +4378,7 @@ async function watch(config, onChange) {
|
|
|
3433
4378
|
}
|
|
3434
4379
|
let pagesDebounceTimer;
|
|
3435
4380
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3436
|
-
const pagesWatcher = import_chokidar.default.watch((0,
|
|
4381
|
+
const pagesWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, pagesGlob), {
|
|
3437
4382
|
ignoreInitial: true,
|
|
3438
4383
|
persistent: true,
|
|
3439
4384
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3459,23 +4404,23 @@ async function watch(config, onChange) {
|
|
|
3459
4404
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3460
4405
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3461
4406
|
let contractsDebounceTimer;
|
|
3462
|
-
const
|
|
4407
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4408
|
+
const contractsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.contracts.glob), {
|
|
3463
4409
|
ignoreInitial: true,
|
|
3464
4410
|
persistent: true,
|
|
3465
4411
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3466
4412
|
});
|
|
3467
|
-
function scheduleContractsRegenerate() {
|
|
4413
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4414
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3468
4415
|
if (contractsDebounceTimer !== void 0) {
|
|
3469
4416
|
clearTimeout(contractsDebounceTimer);
|
|
3470
4417
|
}
|
|
3471
4418
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3472
4419
|
contractsDebounceTimer = void 0;
|
|
4420
|
+
const changed = [...pendingChangedPaths];
|
|
4421
|
+
pendingChangedPaths.clear();
|
|
3473
4422
|
try {
|
|
3474
|
-
const routes = await
|
|
3475
|
-
cwd: config.codegen.cwd,
|
|
3476
|
-
glob: config.contracts.glob,
|
|
3477
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3478
|
-
});
|
|
4423
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3479
4424
|
await generate(config, routes);
|
|
3480
4425
|
} catch (err) {
|
|
3481
4426
|
console.error(
|
|
@@ -3486,17 +4431,17 @@ async function watch(config, onChange) {
|
|
|
3486
4431
|
onChange?.();
|
|
3487
4432
|
}, config.contracts.debounceMs);
|
|
3488
4433
|
}
|
|
3489
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3490
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3491
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3492
|
-
const formsWatcher = import_chokidar.default.watch((0,
|
|
4434
|
+
contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4435
|
+
contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4436
|
+
contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
4437
|
+
const formsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.forms.watch), {
|
|
3493
4438
|
ignoreInitial: true,
|
|
3494
4439
|
persistent: true,
|
|
3495
4440
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3496
4441
|
});
|
|
3497
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3498
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3499
|
-
formsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
4442
|
+
formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4443
|
+
formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4444
|
+
formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
3500
4445
|
return {
|
|
3501
4446
|
close: async () => {
|
|
3502
4447
|
if (pagesDebounceTimer !== void 0) {
|
|
@@ -3565,17 +4510,21 @@ function renderTsType(node, ctx) {
|
|
|
3565
4510
|
}
|
|
3566
4511
|
|
|
3567
4512
|
// src/index.ts
|
|
3568
|
-
var VERSION = "0.
|
|
4513
|
+
var VERSION = "0.5.0";
|
|
3569
4514
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3570
4515
|
0 && (module.exports = {
|
|
3571
4516
|
CodegenError,
|
|
3572
4517
|
ConfigError,
|
|
3573
4518
|
VERSION,
|
|
3574
4519
|
acquireLock,
|
|
4520
|
+
buildMocksFile,
|
|
4521
|
+
buildOpenApiSpec,
|
|
3575
4522
|
defineConfig,
|
|
3576
4523
|
discoverContractsFast,
|
|
3577
4524
|
emitApi,
|
|
3578
4525
|
emitForms,
|
|
4526
|
+
emitMocks,
|
|
4527
|
+
emitOpenApi,
|
|
3579
4528
|
emitRoutes,
|
|
3580
4529
|
extractSchemaFromDto,
|
|
3581
4530
|
generate,
|
|
@@ -3583,6 +4532,8 @@ var VERSION = "0.4.0";
|
|
|
3583
4532
|
renderTsType,
|
|
3584
4533
|
resolveAdapter,
|
|
3585
4534
|
resolveConfig,
|
|
4535
|
+
schemaModuleToJsonSchema,
|
|
4536
|
+
schemaNodeToJsonSchema,
|
|
3586
4537
|
watch
|
|
3587
4538
|
});
|
|
3588
4539
|
//# sourceMappingURL=index.cjs.map
|