@dudousxd/nestjs-codegen 0.4.1 → 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 +31 -0
- package/dist/cli/main.cjs +1043 -150
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1026 -133
- 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 +1016 -108
- 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 +997 -95
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +978 -103
- 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 +972 -97
- package/dist/nest/index.js.map +1 -1
- package/package.json +30 -11
package/dist/cli/main.js
CHANGED
|
@@ -126,6 +126,19 @@ function applyDefaults(userConfig, cwd) {
|
|
|
126
126
|
enabled: userConfig.forms?.enabled ?? true,
|
|
127
127
|
watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
|
|
128
128
|
zodImport: userConfig.forms?.zodImport ?? "zod"
|
|
129
|
+
},
|
|
130
|
+
openapi: {
|
|
131
|
+
enabled: userConfig.openapi?.enabled ?? false,
|
|
132
|
+
fileName: userConfig.openapi?.fileName ?? "openapi.json",
|
|
133
|
+
title: userConfig.openapi?.title ?? "NestJS API",
|
|
134
|
+
version: userConfig.openapi?.version ?? "1.0.0",
|
|
135
|
+
description: userConfig.openapi?.description ?? null
|
|
136
|
+
},
|
|
137
|
+
mocks: {
|
|
138
|
+
enabled: userConfig.mocks?.enabled ?? false,
|
|
139
|
+
fileName: userConfig.mocks?.fileName ?? "mocks.ts",
|
|
140
|
+
seed: userConfig.mocks?.seed ?? 1,
|
|
141
|
+
baseUrl: userConfig.mocks?.baseUrl ?? ""
|
|
129
142
|
}
|
|
130
143
|
};
|
|
131
144
|
}
|
|
@@ -163,8 +176,8 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
// src/generate.ts
|
|
166
|
-
import { mkdir as
|
|
167
|
-
import { dirname as dirname2, join as
|
|
179
|
+
import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
|
|
180
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
168
181
|
|
|
169
182
|
// src/discovery/pages.ts
|
|
170
183
|
import { readFile } from "fs/promises";
|
|
@@ -693,17 +706,28 @@ function emitFilterQueryType(c) {
|
|
|
693
706
|
return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
|
|
694
707
|
}
|
|
695
708
|
function buildResponseType(c, outDir) {
|
|
709
|
+
const respRef = c.contractSource.responseRef;
|
|
710
|
+
if (c.contractSource.stream) {
|
|
711
|
+
if (respRef) return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
712
|
+
return c.contractSource.response;
|
|
713
|
+
}
|
|
696
714
|
if (c.controllerRef) {
|
|
697
715
|
let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
698
716
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
699
717
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
700
718
|
}
|
|
701
|
-
const respRef = c.contractSource.responseRef;
|
|
702
719
|
if (respRef) {
|
|
703
720
|
return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
|
|
704
721
|
}
|
|
705
722
|
return c.contractSource.response;
|
|
706
723
|
}
|
|
724
|
+
function buildErrorType(c) {
|
|
725
|
+
const errRef = c.contractSource.errorRef;
|
|
726
|
+
if (errRef) {
|
|
727
|
+
return errRef.isArray ? `Array<${errRef.name}>` : errRef.name;
|
|
728
|
+
}
|
|
729
|
+
return c.contractSource.error ?? "unknown";
|
|
730
|
+
}
|
|
707
731
|
function emitRouterTypeBlock(tree, indent, outDir) {
|
|
708
732
|
const pad = " ".repeat(indent);
|
|
709
733
|
const lines = [];
|
|
@@ -718,12 +742,14 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
718
742
|
const bodyRef = c.contractSource.bodyRef;
|
|
719
743
|
const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
720
744
|
const response = buildResponseType(c, outDir);
|
|
745
|
+
const error = buildErrorType(c);
|
|
721
746
|
const params = buildParamsType(c.params);
|
|
722
747
|
const safeMethod = JSON.stringify(method);
|
|
723
748
|
const safeUrl = JSON.stringify(c.path);
|
|
724
749
|
const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
|
|
750
|
+
const stream = c.contractSource.stream ? "true" : "false";
|
|
725
751
|
lines.push(
|
|
726
|
-
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
|
|
752
|
+
`${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
|
|
727
753
|
);
|
|
728
754
|
} else {
|
|
729
755
|
lines.push(`${pad}${objKey}: {`);
|
|
@@ -795,15 +821,21 @@ function emitReqHelper() {
|
|
|
795
821
|
""
|
|
796
822
|
];
|
|
797
823
|
}
|
|
798
|
-
function renderLeaf(pad, objKey, req, requestExpr, members) {
|
|
824
|
+
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
799
825
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
800
826
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
827
|
+
if (streamExpr) {
|
|
828
|
+
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
829
|
+
}
|
|
801
830
|
for (const [name, value] of Object.entries(members)) {
|
|
802
831
|
lines.push(`${pad} ${name}: ${value},`);
|
|
803
832
|
}
|
|
804
833
|
lines.push(`${pad}}),`);
|
|
805
834
|
return lines;
|
|
806
835
|
}
|
|
836
|
+
function renderStreamExpr(req) {
|
|
837
|
+
return `fetcher.sse<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
|
|
838
|
+
}
|
|
807
839
|
function emitApiObjectBlock(tree, indent, p) {
|
|
808
840
|
const pad = " ".repeat(indent);
|
|
809
841
|
const lines = [];
|
|
@@ -838,7 +870,8 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
838
870
|
}
|
|
839
871
|
const members = {};
|
|
840
872
|
for (const [name, { value }] of owned) members[name] = value;
|
|
841
|
-
|
|
873
|
+
const streamExpr = node.contractSource.stream ? renderStreamExpr(req) : void 0;
|
|
874
|
+
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members, streamExpr));
|
|
842
875
|
}
|
|
843
876
|
return lines;
|
|
844
877
|
}
|
|
@@ -876,6 +909,8 @@ var ROUTE_NAMESPACE = [
|
|
|
876
909
|
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
877
910
|
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
878
911
|
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
912
|
+
" /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
|
|
913
|
+
' export type Stream<K extends string> = ResolveByName<K, "response">;',
|
|
879
914
|
" export type Request<K extends string> = {",
|
|
880
915
|
" body: Body<K>;",
|
|
881
916
|
" query: Query<K>;",
|
|
@@ -892,6 +927,7 @@ var PATH_NAMESPACE = [
|
|
|
892
927
|
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
893
928
|
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
894
929
|
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
930
|
+
' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
895
931
|
"}",
|
|
896
932
|
""
|
|
897
933
|
];
|
|
@@ -903,6 +939,7 @@ var EMPTY_ROUTE_NAMESPACE = [
|
|
|
903
939
|
" export type Params<K extends string> = never;",
|
|
904
940
|
" export type Error<K extends string> = never;",
|
|
905
941
|
" export type FilterFields<K extends string> = never;",
|
|
942
|
+
" export type Stream<K extends string> = never;",
|
|
906
943
|
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
907
944
|
"}",
|
|
908
945
|
""
|
|
@@ -915,6 +952,7 @@ var EMPTY_PATH_NAMESPACE = [
|
|
|
915
952
|
" export type Params<M extends string, U extends string> = never;",
|
|
916
953
|
" export type Error<M extends string, U extends string> = never;",
|
|
917
954
|
" export type FilterFields<M extends string, U extends string> = never;",
|
|
955
|
+
" export type Stream<M extends string, U extends string> = never;",
|
|
918
956
|
"}",
|
|
919
957
|
""
|
|
920
958
|
];
|
|
@@ -938,7 +976,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
938
976
|
for (const r of contracted) {
|
|
939
977
|
const cs = r.contract?.contractSource;
|
|
940
978
|
if (!cs) continue;
|
|
941
|
-
const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
|
|
979
|
+
const refs = r.controllerRef && !cs.stream ? [cs.queryRef, cs.bodyRef, cs.errorRef] : [cs.queryRef, cs.bodyRef, cs.responseRef, cs.errorRef];
|
|
942
980
|
for (const ref of refs) {
|
|
943
981
|
if (!ref) continue;
|
|
944
982
|
let names = importsByFile.get(ref.filePath);
|
|
@@ -1371,11 +1409,467 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
1371
1409
|
await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
|
|
1372
1410
|
}
|
|
1373
1411
|
|
|
1374
|
-
// src/emit/emit-
|
|
1412
|
+
// src/emit/emit-mocks.ts
|
|
1375
1413
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1376
|
-
import { join as join8
|
|
1377
|
-
|
|
1414
|
+
import { join as join8 } from "path";
|
|
1415
|
+
|
|
1416
|
+
// src/ir/schema-node-to-json-schema.ts
|
|
1417
|
+
var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
|
|
1418
|
+
function parseLiteral(raw) {
|
|
1419
|
+
const t = raw.trim();
|
|
1420
|
+
if (t === "true") return true;
|
|
1421
|
+
if (t === "false") return false;
|
|
1422
|
+
if (t === "null") return null;
|
|
1423
|
+
const q = t[0];
|
|
1424
|
+
if ((q === "'" || q === '"' || q === "`") && t[t.length - 1] === q) {
|
|
1425
|
+
return t.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\`/g, "`").replace(/\\\\/g, "\\");
|
|
1426
|
+
}
|
|
1427
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
|
|
1428
|
+
return Number(t);
|
|
1429
|
+
}
|
|
1430
|
+
return t;
|
|
1431
|
+
}
|
|
1432
|
+
function literalsType(values) {
|
|
1433
|
+
const types = new Set(values.map((v) => v === null ? "null" : typeof v));
|
|
1434
|
+
if (types.size === 1) {
|
|
1435
|
+
const only = [...types][0];
|
|
1436
|
+
if (only === "string") return "string";
|
|
1437
|
+
if (only === "number") return "number";
|
|
1438
|
+
if (only === "boolean") return "boolean";
|
|
1439
|
+
}
|
|
1440
|
+
return void 0;
|
|
1441
|
+
}
|
|
1442
|
+
function convert(node, ctx) {
|
|
1443
|
+
switch (node.kind) {
|
|
1444
|
+
case "string": {
|
|
1445
|
+
const out = { type: "string" };
|
|
1446
|
+
for (const c of node.checks) {
|
|
1447
|
+
if (c.check === "email") out.format = "email";
|
|
1448
|
+
else if (c.check === "url") out.format = "uri";
|
|
1449
|
+
else if (c.check === "uuid") out.format = "uuid";
|
|
1450
|
+
else if (c.check === "min") out.minLength = Number(c.value);
|
|
1451
|
+
else if (c.check === "max") out.maxLength = Number(c.value);
|
|
1452
|
+
else if (c.check === "regex") {
|
|
1453
|
+
const m = /^\/(.*)\/[a-z]*$/.exec(c.pattern);
|
|
1454
|
+
out.pattern = m ? m[1] : c.pattern;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return out;
|
|
1458
|
+
}
|
|
1459
|
+
case "number": {
|
|
1460
|
+
const out = { type: "number" };
|
|
1461
|
+
for (const c of node.checks) {
|
|
1462
|
+
if (c.check === "int") out.type = "integer";
|
|
1463
|
+
else if (c.check === "min") out.minimum = Number(c.value);
|
|
1464
|
+
else if (c.check === "max") out.maximum = Number(c.value);
|
|
1465
|
+
else if (c.check === "positive") out.exclusiveMinimum = 0;
|
|
1466
|
+
else if (c.check === "negative") out.exclusiveMaximum = 0;
|
|
1467
|
+
}
|
|
1468
|
+
return out;
|
|
1469
|
+
}
|
|
1470
|
+
case "boolean":
|
|
1471
|
+
return { type: "boolean" };
|
|
1472
|
+
case "date":
|
|
1473
|
+
return { type: "string", format: "date-time" };
|
|
1474
|
+
case "unknown":
|
|
1475
|
+
return node.note ? { description: node.note } : {};
|
|
1476
|
+
case "instanceof":
|
|
1477
|
+
return { type: "object", description: `instanceof ${node.ctor}` };
|
|
1478
|
+
case "enum": {
|
|
1479
|
+
const values = node.literals.map(parseLiteral);
|
|
1480
|
+
const t = literalsType(values);
|
|
1481
|
+
const out = { enum: values };
|
|
1482
|
+
if (t) out.type = t;
|
|
1483
|
+
return out;
|
|
1484
|
+
}
|
|
1485
|
+
case "literal": {
|
|
1486
|
+
const value = parseLiteral(node.raw);
|
|
1487
|
+
const out = { const: value };
|
|
1488
|
+
const t = literalsType([value]);
|
|
1489
|
+
if (t) out.type = t;
|
|
1490
|
+
return out;
|
|
1491
|
+
}
|
|
1492
|
+
case "union": {
|
|
1493
|
+
const options = node.options.map((o) => convert(o, ctx));
|
|
1494
|
+
const out = { oneOf: options };
|
|
1495
|
+
if (node.discriminator) {
|
|
1496
|
+
out.discriminator = { propertyName: node.discriminator };
|
|
1497
|
+
}
|
|
1498
|
+
return out;
|
|
1499
|
+
}
|
|
1500
|
+
case "object": {
|
|
1501
|
+
const properties = {};
|
|
1502
|
+
const required = [];
|
|
1503
|
+
for (const f of node.fields) {
|
|
1504
|
+
if (f.value.kind === "optional") {
|
|
1505
|
+
properties[f.key] = convert(f.value.inner, ctx);
|
|
1506
|
+
} else {
|
|
1507
|
+
properties[f.key] = convert(f.value, ctx);
|
|
1508
|
+
required.push(f.key);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
const out = { type: "object", properties };
|
|
1512
|
+
if (required.length > 0) out.required = required;
|
|
1513
|
+
out.additionalProperties = node.passthrough;
|
|
1514
|
+
return out;
|
|
1515
|
+
}
|
|
1516
|
+
case "array":
|
|
1517
|
+
return { type: "array", items: convert(node.element, ctx) };
|
|
1518
|
+
case "optional":
|
|
1519
|
+
return widenNullable(convert(node.inner, ctx));
|
|
1520
|
+
case "ref":
|
|
1521
|
+
case "lazyRef":
|
|
1522
|
+
return { $ref: `${ctx.refPrefix}${node.name}` };
|
|
1523
|
+
case "annotated":
|
|
1524
|
+
return convert(node.inner, ctx);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function widenNullable(schema) {
|
|
1528
|
+
if (schema.$ref) {
|
|
1529
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1530
|
+
}
|
|
1531
|
+
if (typeof schema.type === "string") {
|
|
1532
|
+
return { ...schema, type: [schema.type, "null"] };
|
|
1533
|
+
}
|
|
1534
|
+
if (Array.isArray(schema.type)) {
|
|
1535
|
+
return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
|
|
1536
|
+
}
|
|
1537
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
1538
|
+
}
|
|
1539
|
+
function schemaModuleToJsonSchema(mod, ctx = DEFAULT_CTX) {
|
|
1540
|
+
const named = {};
|
|
1541
|
+
for (const [name, node] of mod.named) {
|
|
1542
|
+
named[name] = convert(node, ctx);
|
|
1543
|
+
}
|
|
1544
|
+
return { root: convert(mod.root, ctx), named };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/emit/mock-gen-runtime.ts
|
|
1548
|
+
var MOCK_GEN_RUNTIME = `
|
|
1549
|
+
/** mulberry32 \u2014 a tiny, fast, seedable PRNG. \`next()\` returns a float in [0, 1). */
|
|
1550
|
+
function makeRng(seed) {
|
|
1551
|
+
let a = seed >>> 0;
|
|
1552
|
+
return {
|
|
1553
|
+
next() {
|
|
1554
|
+
a |= 0;
|
|
1555
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
1556
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
1557
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
1558
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1559
|
+
},
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function __pick(rng, items) {
|
|
1564
|
+
return items[Math.floor(rng.next() * items.length)];
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function __intBetween(rng, min, max) {
|
|
1568
|
+
return Math.floor(rng.next() * (max - min + 1)) + min;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const __WORDS = ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'tempor'];
|
|
1572
|
+
const __FIRST_NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis'];
|
|
1573
|
+
const __LAST_NAMES = ['Lovelace', 'Turing', 'Hopper', 'Torvalds', 'Hamilton', 'Ritchie'];
|
|
1574
|
+
|
|
1575
|
+
function __fakeWords(rng, count) {
|
|
1576
|
+
let out = [];
|
|
1577
|
+
for (let i = 0; i < count; i++) out.push(__pick(rng, __WORDS));
|
|
1578
|
+
return out.join(' ');
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function __hex(rng, len) {
|
|
1582
|
+
let s = '';
|
|
1583
|
+
for (let i = 0; i < len; i++) s += Math.floor(rng.next() * 16).toString(16);
|
|
1584
|
+
return s;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function __fakeUuid(rng) {
|
|
1588
|
+
return __hex(rng, 8) + '-' + __hex(rng, 4) + '-4' + __hex(rng, 3) + '-' + __pick(rng, ['8', '9', 'a', 'b']) + __hex(rng, 3) + '-' + __hex(rng, 12);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function __fakeString(rng, schema) {
|
|
1592
|
+
switch (schema.format) {
|
|
1593
|
+
case 'email':
|
|
1594
|
+
return __pick(rng, __FIRST_NAMES).toLowerCase() + '.' + __pick(rng, __LAST_NAMES).toLowerCase() + '@example.com';
|
|
1595
|
+
case 'uri':
|
|
1596
|
+
case 'url':
|
|
1597
|
+
return 'https://example.com/' + __pick(rng, __WORDS);
|
|
1598
|
+
case 'uuid':
|
|
1599
|
+
return __fakeUuid(rng);
|
|
1600
|
+
case 'date-time':
|
|
1601
|
+
return new Date(Date.UTC(2020, __intBetween(rng, 0, 11), __intBetween(rng, 1, 28))).toISOString();
|
|
1602
|
+
default:
|
|
1603
|
+
return __fakeWords(rng, __intBetween(rng, 1, 3));
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/** Generate a mock value for a JSON Schema node (depth-capped recursion via $ref). */
|
|
1608
|
+
function generateMock(schema, rng, defs, depth) {
|
|
1609
|
+
defs = defs || {};
|
|
1610
|
+
depth = depth || 0;
|
|
1611
|
+
if (schema.$ref) {
|
|
1612
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
1613
|
+
const target = defs[name];
|
|
1614
|
+
if (!target || depth > 4) return null;
|
|
1615
|
+
return generateMock(target, rng, defs, depth + 1);
|
|
1616
|
+
}
|
|
1617
|
+
if ('const' in schema) return schema.const;
|
|
1618
|
+
if (schema.enum && schema.enum.length > 0) return __pick(rng, schema.enum);
|
|
1619
|
+
if (schema.oneOf && schema.oneOf.length > 0) return generateMock(__pick(rng, schema.oneOf), rng, defs, depth);
|
|
1620
|
+
if (schema.anyOf && schema.anyOf.length > 0) return generateMock(__pick(rng, schema.anyOf), rng, defs, depth);
|
|
1621
|
+
let type = Array.isArray(schema.type)
|
|
1622
|
+
? (schema.type.filter((t) => t !== 'null')[0] || 'null')
|
|
1623
|
+
: schema.type;
|
|
1624
|
+
switch (type) {
|
|
1625
|
+
case 'string':
|
|
1626
|
+
return __fakeString(rng, schema);
|
|
1627
|
+
case 'integer':
|
|
1628
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000);
|
|
1629
|
+
case 'number':
|
|
1630
|
+
return __intBetween(rng, typeof schema.minimum === 'number' ? schema.minimum : 0, typeof schema.maximum === 'number' ? schema.maximum : 1000) + Math.round(rng.next() * 100) / 100;
|
|
1631
|
+
case 'boolean':
|
|
1632
|
+
return rng.next() < 0.5;
|
|
1633
|
+
case 'null':
|
|
1634
|
+
return null;
|
|
1635
|
+
case 'array': {
|
|
1636
|
+
const count = depth > 2 ? 0 : __intBetween(rng, 1, 2);
|
|
1637
|
+
const items = schema.items || {};
|
|
1638
|
+
let arr = [];
|
|
1639
|
+
for (let i = 0; i < count; i++) arr.push(generateMock(items, rng, defs, depth + 1));
|
|
1640
|
+
return arr;
|
|
1641
|
+
}
|
|
1642
|
+
case 'object': {
|
|
1643
|
+
const out = {};
|
|
1644
|
+
const props = schema.properties || {};
|
|
1645
|
+
for (const key of Object.keys(props)) out[key] = generateMock(props[key], rng, defs, depth + 1);
|
|
1646
|
+
return out;
|
|
1647
|
+
}
|
|
1648
|
+
default:
|
|
1649
|
+
return {};
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
`.trim();
|
|
1653
|
+
|
|
1654
|
+
// src/emit/emit-mocks.ts
|
|
1655
|
+
var REF_PREFIX = "#/components/schemas/";
|
|
1656
|
+
function toMswPath(path, baseUrl) {
|
|
1657
|
+
return `${baseUrl}${path}`;
|
|
1658
|
+
}
|
|
1659
|
+
function responseSchemaFor(route, defs) {
|
|
1660
|
+
const cs = route.contract.contractSource;
|
|
1661
|
+
if (cs.responseSchema) {
|
|
1662
|
+
const { root, named } = schemaModuleToJsonSchema(cs.responseSchema, { refPrefix: REF_PREFIX });
|
|
1663
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1664
|
+
if (!(name in defs)) defs[name] = node;
|
|
1665
|
+
}
|
|
1666
|
+
return root;
|
|
1667
|
+
}
|
|
1668
|
+
return {};
|
|
1669
|
+
}
|
|
1670
|
+
function buildMocksFile(routes, opts = {}) {
|
|
1671
|
+
const seed = opts.seed ?? 1;
|
|
1672
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
1673
|
+
const contracted = routes.filter((r) => r.contract);
|
|
1674
|
+
const defs = {};
|
|
1675
|
+
const handlers = [];
|
|
1676
|
+
for (const r of contracted) {
|
|
1677
|
+
const schema = responseSchemaFor(r, defs);
|
|
1678
|
+
const method = r.method.toLowerCase();
|
|
1679
|
+
const mswMethod = method === "get" || method === "post" || method === "put" || method === "patch" || method === "delete" ? method : "all";
|
|
1680
|
+
const path = toMswPath(r.path, baseUrl);
|
|
1681
|
+
const cs = r.contract.contractSource;
|
|
1682
|
+
const schemaLiteral = JSON.stringify(schema);
|
|
1683
|
+
const pathLit = JSON.stringify(path);
|
|
1684
|
+
if (cs.stream) {
|
|
1685
|
+
handlers.push(
|
|
1686
|
+
[
|
|
1687
|
+
` // ${r.name} (stream)`,
|
|
1688
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1689
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1690
|
+
" const body = `data: ${JSON.stringify(value)}\\n\\n`;",
|
|
1691
|
+
" return new HttpResponse(body, { headers: { 'Content-Type': 'text/event-stream' } });",
|
|
1692
|
+
" }),"
|
|
1693
|
+
].join("\n")
|
|
1694
|
+
);
|
|
1695
|
+
} else {
|
|
1696
|
+
handlers.push(
|
|
1697
|
+
[
|
|
1698
|
+
` // ${r.name}`,
|
|
1699
|
+
` http.${mswMethod}(${pathLit}, () => {`,
|
|
1700
|
+
` const value = generateMock(${schemaLiteral}, makeRng(SEED), DEFS);`,
|
|
1701
|
+
" return HttpResponse.json(value);",
|
|
1702
|
+
" }),"
|
|
1703
|
+
].join("\n")
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
const lines = [
|
|
1708
|
+
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1709
|
+
"// MSW handlers returning deterministic, schema-shaped mock data.",
|
|
1710
|
+
"/* eslint-disable */",
|
|
1711
|
+
"// @ts-nocheck",
|
|
1712
|
+
"",
|
|
1713
|
+
"import { http, HttpResponse } from 'msw';",
|
|
1714
|
+
"",
|
|
1715
|
+
`const SEED = ${seed};`,
|
|
1716
|
+
"",
|
|
1717
|
+
"// ---------------------------------------------------------------------------",
|
|
1718
|
+
"// Embedded mock-data runtime (mulberry32 PRNG + JSON-Schema value generator).",
|
|
1719
|
+
"// Dependency-free: no @faker-js/faker. Deterministic for a given SEED.",
|
|
1720
|
+
"// ---------------------------------------------------------------------------",
|
|
1721
|
+
MOCK_GEN_RUNTIME,
|
|
1722
|
+
"",
|
|
1723
|
+
"// Shared component schemas referenced by $ref.",
|
|
1724
|
+
`const DEFS = ${JSON.stringify(defs, null, 2)};`,
|
|
1725
|
+
"",
|
|
1726
|
+
"/** MSW request handlers, one per contracted route. */",
|
|
1727
|
+
"export const handlers = [",
|
|
1728
|
+
...handlers,
|
|
1729
|
+
"];",
|
|
1730
|
+
""
|
|
1731
|
+
];
|
|
1732
|
+
return lines.join("\n");
|
|
1733
|
+
}
|
|
1734
|
+
async function emitMocks(routes, outDir, opts = {}) {
|
|
1378
1735
|
await mkdir5(outDir, { recursive: true });
|
|
1736
|
+
const content = buildMocksFile(routes, opts);
|
|
1737
|
+
const fileName = opts.fileName ?? "mocks.ts";
|
|
1738
|
+
await writeFile5(join8(outDir, fileName), content, "utf8");
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/emit/emit-openapi.ts
|
|
1742
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1743
|
+
import { join as join9 } from "path";
|
|
1744
|
+
var REF_PREFIX2 = "#/components/schemas/";
|
|
1745
|
+
function toOpenApiPath(path) {
|
|
1746
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
1747
|
+
}
|
|
1748
|
+
function positionSchema(schema, tsType, components) {
|
|
1749
|
+
if (schema) {
|
|
1750
|
+
const { root, named } = schemaModuleToJsonSchema(schema, { refPrefix: REF_PREFIX2 });
|
|
1751
|
+
for (const [name, node] of Object.entries(named)) {
|
|
1752
|
+
if (!(name in components)) components[name] = node;
|
|
1753
|
+
}
|
|
1754
|
+
return root;
|
|
1755
|
+
}
|
|
1756
|
+
return tsType ? { description: tsType } : {};
|
|
1757
|
+
}
|
|
1758
|
+
function buildParameters(route) {
|
|
1759
|
+
const params = [];
|
|
1760
|
+
for (const p of route.params) {
|
|
1761
|
+
if (p.source === "path") {
|
|
1762
|
+
params.push({
|
|
1763
|
+
name: p.name,
|
|
1764
|
+
in: "path",
|
|
1765
|
+
required: true,
|
|
1766
|
+
schema: { type: "string" }
|
|
1767
|
+
});
|
|
1768
|
+
} else if (p.source === "query") {
|
|
1769
|
+
params.push({
|
|
1770
|
+
name: p.name,
|
|
1771
|
+
in: "query",
|
|
1772
|
+
required: false,
|
|
1773
|
+
schema: { type: "string" }
|
|
1774
|
+
});
|
|
1775
|
+
} else if (p.source === "header") {
|
|
1776
|
+
params.push({
|
|
1777
|
+
name: p.name,
|
|
1778
|
+
in: "header",
|
|
1779
|
+
required: false,
|
|
1780
|
+
schema: { type: "string" }
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return params;
|
|
1785
|
+
}
|
|
1786
|
+
function buildResponses(cs, components) {
|
|
1787
|
+
const responses = {};
|
|
1788
|
+
const successSchema = positionSchema(
|
|
1789
|
+
// Prefer rich response IR when present; otherwise fall back to the TS type.
|
|
1790
|
+
cs.responseSchema ?? null,
|
|
1791
|
+
cs.response,
|
|
1792
|
+
components
|
|
1793
|
+
);
|
|
1794
|
+
const successContentType = cs.stream ? "text/event-stream" : "application/json";
|
|
1795
|
+
responses["200"] = {
|
|
1796
|
+
description: cs.stream ? "Server-sent event stream" : "Successful response",
|
|
1797
|
+
content: { [successContentType]: { schema: successSchema } }
|
|
1798
|
+
};
|
|
1799
|
+
const errorSchema = positionSchema(null, cs.error ?? null, components);
|
|
1800
|
+
const errorBody = {
|
|
1801
|
+
description: "Error response",
|
|
1802
|
+
content: { "application/json": { schema: errorSchema } }
|
|
1803
|
+
};
|
|
1804
|
+
if (cs.error || cs.errorRef) {
|
|
1805
|
+
responses["400"] = errorBody;
|
|
1806
|
+
responses.default = errorBody;
|
|
1807
|
+
} else {
|
|
1808
|
+
responses.default = {
|
|
1809
|
+
description: "Error response",
|
|
1810
|
+
content: { "application/json": { schema: {} } }
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
return responses;
|
|
1814
|
+
}
|
|
1815
|
+
function buildOperation(route, components) {
|
|
1816
|
+
const cs = route.contract.contractSource;
|
|
1817
|
+
const op = {
|
|
1818
|
+
operationId: route.name,
|
|
1819
|
+
parameters: buildParameters(route),
|
|
1820
|
+
responses: buildResponses(cs, components)
|
|
1821
|
+
};
|
|
1822
|
+
const method = route.method.toUpperCase();
|
|
1823
|
+
const hasBody = method !== "GET" && method !== "HEAD" && method !== "DELETE";
|
|
1824
|
+
if (hasBody && (cs.bodySchema || cs.body)) {
|
|
1825
|
+
const bodySchema = positionSchema(cs.bodySchema, cs.body, components);
|
|
1826
|
+
op.requestBody = {
|
|
1827
|
+
required: true,
|
|
1828
|
+
content: { "application/json": { schema: bodySchema } }
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
return op;
|
|
1832
|
+
}
|
|
1833
|
+
function buildOpenApiSpec(routes, opts = {}) {
|
|
1834
|
+
const components = {};
|
|
1835
|
+
const paths = {};
|
|
1836
|
+
for (const route of routes) {
|
|
1837
|
+
if (!route.contract) continue;
|
|
1838
|
+
const oaPath = toOpenApiPath(route.path);
|
|
1839
|
+
const method = route.method.toLowerCase();
|
|
1840
|
+
let pathItem = paths[oaPath];
|
|
1841
|
+
if (!pathItem) {
|
|
1842
|
+
pathItem = {};
|
|
1843
|
+
paths[oaPath] = pathItem;
|
|
1844
|
+
}
|
|
1845
|
+
pathItem[method] = buildOperation(route, components);
|
|
1846
|
+
}
|
|
1847
|
+
const info = opts.info ?? {};
|
|
1848
|
+
const doc = {
|
|
1849
|
+
openapi: "3.1.0",
|
|
1850
|
+
info: {
|
|
1851
|
+
title: info.title ?? "NestJS API",
|
|
1852
|
+
version: info.version ?? "1.0.0",
|
|
1853
|
+
...info.description ? { description: info.description } : {}
|
|
1854
|
+
},
|
|
1855
|
+
paths,
|
|
1856
|
+
components: { schemas: components }
|
|
1857
|
+
};
|
|
1858
|
+
return doc;
|
|
1859
|
+
}
|
|
1860
|
+
async function emitOpenApi(routes, outDir, opts = {}) {
|
|
1861
|
+
await mkdir6(outDir, { recursive: true });
|
|
1862
|
+
const doc = buildOpenApiSpec(routes, opts);
|
|
1863
|
+
const fileName = opts.fileName ?? "openapi.json";
|
|
1864
|
+
await writeFile6(join9(outDir, fileName), `${JSON.stringify(doc, null, 2)}
|
|
1865
|
+
`, "utf8");
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/emit/emit-pages.ts
|
|
1869
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
1870
|
+
import { join as join10, relative as relative5 } from "path";
|
|
1871
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
1872
|
+
await mkdir7(outDir, { recursive: true });
|
|
1379
1873
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
1380
1874
|
const augBody = pages.map((p) => {
|
|
1381
1875
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
@@ -1394,7 +1888,7 @@ ${augBody}
|
|
|
1394
1888
|
}
|
|
1395
1889
|
${sharedPropsBlock}}
|
|
1396
1890
|
`;
|
|
1397
|
-
await
|
|
1891
|
+
await writeFile7(join10(outDir, "pages.d.ts"), content, "utf8");
|
|
1398
1892
|
}
|
|
1399
1893
|
function buildSharedPropsBlock(sharedProps) {
|
|
1400
1894
|
if (!sharedProps) return "";
|
|
@@ -1424,12 +1918,12 @@ function needsQuotes(name) {
|
|
|
1424
1918
|
}
|
|
1425
1919
|
|
|
1426
1920
|
// src/emit/emit-routes.ts
|
|
1427
|
-
import { mkdir as
|
|
1428
|
-
import { join as
|
|
1921
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
1922
|
+
import { join as join11 } from "path";
|
|
1429
1923
|
async function emitRoutes(routes, outDir) {
|
|
1430
|
-
await
|
|
1924
|
+
await mkdir8(outDir, { recursive: true });
|
|
1431
1925
|
const content = buildRoutesFile(routes);
|
|
1432
|
-
await
|
|
1926
|
+
await writeFile8(join11(outDir, "routes.ts"), content, "utf8");
|
|
1433
1927
|
}
|
|
1434
1928
|
function buildRoutesFile(routes) {
|
|
1435
1929
|
if (routes.length === 0) {
|
|
@@ -1577,24 +2071,41 @@ async function generate(config, inputRoutes = []) {
|
|
|
1577
2071
|
});
|
|
1578
2072
|
}
|
|
1579
2073
|
const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
|
|
2074
|
+
if (hasContracts && config.openapi.enabled) {
|
|
2075
|
+
await emitOpenApi(routes, config.codegen.outDir, {
|
|
2076
|
+
fileName: config.openapi.fileName,
|
|
2077
|
+
info: {
|
|
2078
|
+
title: config.openapi.title,
|
|
2079
|
+
version: config.openapi.version,
|
|
2080
|
+
...config.openapi.description ? { description: config.openapi.description } : {}
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
if (hasContracts && config.mocks.enabled) {
|
|
2085
|
+
await emitMocks(routes, config.codegen.outDir, {
|
|
2086
|
+
fileName: config.mocks.fileName,
|
|
2087
|
+
seed: config.mocks.seed,
|
|
2088
|
+
baseUrl: config.mocks.baseUrl
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
1580
2091
|
await emitIndex(config.codegen.outDir, hasContracts, hasForms);
|
|
1581
2092
|
if (extensions.length > 0) {
|
|
1582
2093
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1583
2094
|
for (const file of extraFiles) {
|
|
1584
|
-
const dest =
|
|
1585
|
-
await
|
|
1586
|
-
await
|
|
2095
|
+
const dest = join12(config.codegen.outDir, file.path);
|
|
2096
|
+
await mkdir9(dirname2(dest), { recursive: true });
|
|
2097
|
+
await writeFile9(dest, file.contents, "utf8");
|
|
1587
2098
|
}
|
|
1588
2099
|
}
|
|
1589
2100
|
}
|
|
1590
2101
|
|
|
1591
2102
|
// src/watch/watcher.ts
|
|
1592
2103
|
import { readFile as readFile3 } from "fs/promises";
|
|
1593
|
-
import { join as
|
|
2104
|
+
import { join as join15 } from "path";
|
|
1594
2105
|
import chokidar from "chokidar";
|
|
1595
2106
|
|
|
1596
2107
|
// src/discovery/contracts-fast.ts
|
|
1597
|
-
import { join as
|
|
2108
|
+
import { join as join13, resolve as resolve3 } from "path";
|
|
1598
2109
|
import fg2 from "fast-glob";
|
|
1599
2110
|
import {
|
|
1600
2111
|
Node as Node8,
|
|
@@ -1776,7 +2287,73 @@ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
|
|
|
1776
2287
|
}
|
|
1777
2288
|
return null;
|
|
1778
2289
|
}
|
|
2290
|
+
function resolveImportedVariable(name, sourceFile, project) {
|
|
2291
|
+
const local = sourceFile.getVariableDeclaration(name);
|
|
2292
|
+
if (local) return { decl: local, file: sourceFile };
|
|
2293
|
+
return resolveVariableViaImports(name, sourceFile, project, /* @__PURE__ */ new Set());
|
|
2294
|
+
}
|
|
2295
|
+
function resolveVariableViaImports(name, sourceFile, project, seen) {
|
|
2296
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
2297
|
+
const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
|
|
2298
|
+
if (!namedImport) continue;
|
|
2299
|
+
const sourceName = namedImport.getName();
|
|
2300
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
2301
|
+
const found = followModuleForVariable(sourceName, moduleSpecifier, sourceFile, project, seen);
|
|
2302
|
+
if (found) return found;
|
|
2303
|
+
}
|
|
2304
|
+
return null;
|
|
2305
|
+
}
|
|
2306
|
+
function followModuleForVariable(name, moduleSpecifier, fromFile, project, seen) {
|
|
2307
|
+
const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
|
|
2308
|
+
for (const candidate of candidates) {
|
|
2309
|
+
let importedFile = project.getSourceFile(candidate);
|
|
2310
|
+
if (!importedFile) {
|
|
2311
|
+
try {
|
|
2312
|
+
importedFile = project.addSourceFileAtPath(candidate);
|
|
2313
|
+
} catch {
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
const found = resolveVariableInFile(name, importedFile, project, seen);
|
|
2318
|
+
if (found) return found;
|
|
2319
|
+
}
|
|
2320
|
+
return null;
|
|
2321
|
+
}
|
|
2322
|
+
function resolveVariableInFile(name, file, project, seen) {
|
|
2323
|
+
const filePath = file.getFilePath();
|
|
2324
|
+
if (seen.has(filePath)) return null;
|
|
2325
|
+
seen.add(filePath);
|
|
2326
|
+
const local = file.getVariableDeclaration(name);
|
|
2327
|
+
if (local) return { decl: local, file };
|
|
2328
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
2329
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
2330
|
+
const namedExports = exportDecl.getNamedExports();
|
|
2331
|
+
if (moduleSpecifier) {
|
|
2332
|
+
const hasStar = namedExports.length === 0;
|
|
2333
|
+
const reExport2 = namedExports.find(
|
|
2334
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2335
|
+
);
|
|
2336
|
+
if (!hasStar && !reExport2) continue;
|
|
2337
|
+
const sourceName2 = hasStar ? name : reExport2?.getName() ?? name;
|
|
2338
|
+
const found = followModuleForVariable(sourceName2, moduleSpecifier, file, project, seen);
|
|
2339
|
+
if (found) return found;
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
const reExport = namedExports.find(
|
|
2343
|
+
(n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
|
|
2344
|
+
);
|
|
2345
|
+
if (!reExport) continue;
|
|
2346
|
+
const sourceName = reExport.getName();
|
|
2347
|
+
const viaImports = resolveVariableViaImports(sourceName, file, project, seen);
|
|
2348
|
+
if (viaImports) return viaImports;
|
|
2349
|
+
}
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
1779
2352
|
var _findTypeCache = /* @__PURE__ */ new WeakMap();
|
|
2353
|
+
function clearTypeResolutionCaches(project) {
|
|
2354
|
+
_findTypeCache.delete(project);
|
|
2355
|
+
_resolveNamedRefCache.delete(project);
|
|
2356
|
+
}
|
|
1780
2357
|
function findType(name, sourceFile, project) {
|
|
1781
2358
|
let byKey = _findTypeCache.get(project);
|
|
1782
2359
|
if (byKey === void 0) {
|
|
@@ -1919,7 +2496,8 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
1919
2496
|
emittedClasses: /* @__PURE__ */ new Map(),
|
|
1920
2497
|
visiting: /* @__PURE__ */ new Set(),
|
|
1921
2498
|
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
1922
|
-
depth: 0
|
|
2499
|
+
depth: 0,
|
|
2500
|
+
typeBindings: /* @__PURE__ */ new Map()
|
|
1923
2501
|
};
|
|
1924
2502
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
1925
2503
|
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
@@ -1943,11 +2521,34 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
1943
2521
|
const typeNode = prop.getTypeNode();
|
|
1944
2522
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
1945
2523
|
const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
|
|
2524
|
+
const discriminator = resolveDiscriminator(dec("Type"));
|
|
2525
|
+
if (discriminator) {
|
|
2526
|
+
const options = discriminator.subTypes.map(
|
|
2527
|
+
(name) => buildNestedReference(name, classFile, ctx)
|
|
2528
|
+
);
|
|
2529
|
+
const unionNode = {
|
|
2530
|
+
kind: "union",
|
|
2531
|
+
options,
|
|
2532
|
+
discriminator: discriminator.property
|
|
2533
|
+
};
|
|
2534
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2535
|
+
const node2 = wrapArray ? { kind: "array", element: unionNode } : unionNode;
|
|
2536
|
+
return applyPresence(node2, decorators);
|
|
2537
|
+
}
|
|
2538
|
+
const propTypeParam = singularClassName(typeText);
|
|
2539
|
+
if (propTypeParam && ctx.typeBindings.has(propTypeParam)) {
|
|
2540
|
+
const bound = ctx.typeBindings.get(propTypeParam);
|
|
2541
|
+
const childNode = buildNestedReference(bound, classFile, ctx);
|
|
2542
|
+
const wrapArray = has("IsArray") || isArrayType;
|
|
2543
|
+
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
2544
|
+
return applyPresence(node2, decorators);
|
|
2545
|
+
}
|
|
1946
2546
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
1947
2547
|
if (has("ValidateNested") || typeRefName) {
|
|
2548
|
+
const typeArgs = genericTypeArgNames(typeNode);
|
|
1948
2549
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
1949
2550
|
if (childName) {
|
|
1950
|
-
const childNode = buildNestedReference(childName, classFile, ctx);
|
|
2551
|
+
const childNode = buildNestedReference(childName, classFile, ctx, typeArgs);
|
|
1951
2552
|
const wrapArray = has("IsArray") || isArrayType;
|
|
1952
2553
|
const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
|
|
1953
2554
|
return applyPresence(node2, decorators);
|
|
@@ -2072,10 +2673,12 @@ function baseFromType(typeText, isArrayType) {
|
|
|
2072
2673
|
return { kind: "unknown" };
|
|
2073
2674
|
}
|
|
2074
2675
|
}
|
|
2075
|
-
function buildNestedReference(className, fromFile, ctx) {
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2676
|
+
function buildNestedReference(className, fromFile, ctx, typeArgs = []) {
|
|
2677
|
+
const cacheKey = typeArgs.length > 0 ? `${className}<${typeArgs.join(",")}>` : className;
|
|
2678
|
+
const schemaBase = typeArgs.length > 0 ? `${className}Of${typeArgs.join("")}` : className;
|
|
2679
|
+
if (ctx.visiting.has(cacheKey)) {
|
|
2680
|
+
const reserved = ctx.emittedClasses.get(cacheKey) ?? aliasFor(schemaBase, ctx);
|
|
2681
|
+
ctx.emittedClasses.set(cacheKey, reserved);
|
|
2079
2682
|
ctx.recursiveSchemas.add(reserved);
|
|
2080
2683
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2081
2684
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
@@ -2094,19 +2697,27 @@ function buildNestedReference(className, fromFile, ctx) {
|
|
|
2094
2697
|
}
|
|
2095
2698
|
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
2096
2699
|
}
|
|
2097
|
-
const existing = ctx.emittedClasses.get(
|
|
2700
|
+
const existing = ctx.emittedClasses.get(cacheKey);
|
|
2098
2701
|
if (existing) return { kind: "ref", name: existing };
|
|
2099
|
-
const schemaName = aliasFor(
|
|
2702
|
+
const schemaName = aliasFor(schemaBase, ctx);
|
|
2100
2703
|
const resolved = findType(className, fromFile, ctx.project);
|
|
2101
2704
|
if (!resolved || resolved.kind !== "class") {
|
|
2102
2705
|
return { kind: "object", fields: [], passthrough: true };
|
|
2103
2706
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2707
|
+
const params = resolved.decl.getTypeParameters().map((p) => p.getName());
|
|
2708
|
+
const newBindings = [];
|
|
2709
|
+
params.forEach((param, i) => {
|
|
2710
|
+
const arg = typeArgs[i];
|
|
2711
|
+
if (arg) newBindings.push([param, arg]);
|
|
2712
|
+
});
|
|
2713
|
+
for (const [k, v] of newBindings) ctx.typeBindings.set(k, v);
|
|
2714
|
+
ctx.emittedClasses.set(cacheKey, schemaName);
|
|
2715
|
+
ctx.visiting.add(cacheKey);
|
|
2106
2716
|
ctx.depth += 1;
|
|
2107
2717
|
const childNode = buildObject(resolved.decl, resolved.file, ctx);
|
|
2108
2718
|
ctx.depth -= 1;
|
|
2109
|
-
ctx.visiting.delete(
|
|
2719
|
+
ctx.visiting.delete(cacheKey);
|
|
2720
|
+
for (const [k] of newBindings) ctx.typeBindings.delete(k);
|
|
2110
2721
|
ctx.named.set(schemaName, childNode);
|
|
2111
2722
|
return { kind: "ref", name: schemaName };
|
|
2112
2723
|
}
|
|
@@ -2153,6 +2764,39 @@ function messageRaw(decorator) {
|
|
|
2153
2764
|
}
|
|
2154
2765
|
return void 0;
|
|
2155
2766
|
}
|
|
2767
|
+
function resolveDiscriminator(decorator) {
|
|
2768
|
+
const optsArg = decorator?.getArguments()[1];
|
|
2769
|
+
if (!optsArg || !Node3.isObjectLiteralExpression(optsArg)) return null;
|
|
2770
|
+
let discProp;
|
|
2771
|
+
for (const prop of optsArg.getProperties()) {
|
|
2772
|
+
if (Node3.isPropertyAssignment(prop) && prop.getName() === "discriminator") {
|
|
2773
|
+
discProp = prop.getInitializer();
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
if (!discProp || !Node3.isObjectLiteralExpression(discProp)) return null;
|
|
2777
|
+
let property = null;
|
|
2778
|
+
const subTypes = [];
|
|
2779
|
+
for (const prop of discProp.getProperties()) {
|
|
2780
|
+
if (!Node3.isPropertyAssignment(prop)) continue;
|
|
2781
|
+
const name = prop.getName();
|
|
2782
|
+
const init = prop.getInitializer();
|
|
2783
|
+
if (!init) continue;
|
|
2784
|
+
if (name === "property" && Node3.isStringLiteral(init)) {
|
|
2785
|
+
property = init.getLiteralValue();
|
|
2786
|
+
} else if (name === "subTypes" && Node3.isArrayLiteralExpression(init)) {
|
|
2787
|
+
for (const el of init.getElements()) {
|
|
2788
|
+
if (!Node3.isObjectLiteralExpression(el)) continue;
|
|
2789
|
+
for (const p of el.getProperties()) {
|
|
2790
|
+
if (!Node3.isPropertyAssignment(p) || p.getName() !== "name") continue;
|
|
2791
|
+
const nameInit = p.getInitializer();
|
|
2792
|
+
if (nameInit && Node3.isIdentifier(nameInit)) subTypes.push(nameInit.getText());
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
if (!property || subTypes.length === 0) return null;
|
|
2798
|
+
return { property, subTypes };
|
|
2799
|
+
}
|
|
2156
2800
|
function resolveTypeFactoryName(decorator) {
|
|
2157
2801
|
const arg = firstArg(decorator);
|
|
2158
2802
|
if (!arg) return null;
|
|
@@ -2166,6 +2810,17 @@ function singularClassName(typeText) {
|
|
|
2166
2810
|
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2167
2811
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2168
2812
|
}
|
|
2813
|
+
function genericTypeArgNames(typeNode) {
|
|
2814
|
+
if (!typeNode || !Node3.isTypeReference(typeNode)) return [];
|
|
2815
|
+
const names = [];
|
|
2816
|
+
for (const arg of typeNode.getTypeArguments()) {
|
|
2817
|
+
if (!Node3.isTypeReference(arg)) return [];
|
|
2818
|
+
const tn = arg.getTypeName();
|
|
2819
|
+
if (!Node3.isIdentifier(tn)) return [];
|
|
2820
|
+
names.push(tn.getText());
|
|
2821
|
+
}
|
|
2822
|
+
return names;
|
|
2823
|
+
}
|
|
2169
2824
|
function enumSchemaFromDecorator(decorator, classFile, ctx) {
|
|
2170
2825
|
const arg = firstArg(decorator);
|
|
2171
2826
|
if (!arg) return null;
|
|
@@ -2225,6 +2880,9 @@ import {
|
|
|
2225
2880
|
|
|
2226
2881
|
// src/discovery/enum-resolution.ts
|
|
2227
2882
|
var _enumCache = /* @__PURE__ */ new WeakMap();
|
|
2883
|
+
function clearEnumCache(project) {
|
|
2884
|
+
_enumCache.delete(project);
|
|
2885
|
+
}
|
|
2228
2886
|
function resolveEnumValues(name, sourceFile, project) {
|
|
2229
2887
|
let byKey = _enumCache.get(project);
|
|
2230
2888
|
if (byKey === void 0) {
|
|
@@ -2693,24 +3351,26 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
|
2693
3351
|
"Map",
|
|
2694
3352
|
"Set"
|
|
2695
3353
|
]);
|
|
2696
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3354
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2697
3355
|
if (depth <= 0) return "unknown";
|
|
2698
3356
|
if (Node6.isArrayTypeNode(typeNode)) {
|
|
2699
3357
|
const elementType = typeNode.getElementTypeNode();
|
|
2700
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3358
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
|
|
2701
3359
|
}
|
|
2702
3360
|
if (Node6.isUnionTypeNode(typeNode)) {
|
|
2703
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3361
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
|
|
2704
3362
|
}
|
|
2705
3363
|
if (Node6.isIntersectionTypeNode(typeNode)) {
|
|
2706
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3364
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
|
|
2707
3365
|
}
|
|
2708
3366
|
if (Node6.isParenthesizedTypeNode(typeNode)) {
|
|
2709
|
-
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3367
|
+
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
|
|
2710
3368
|
}
|
|
2711
3369
|
if (Node6.isTypeReference(typeNode)) {
|
|
2712
3370
|
const typeName = typeNode.getTypeName();
|
|
2713
3371
|
const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3372
|
+
const bound = subst.get(name);
|
|
3373
|
+
if (bound !== void 0) return bound;
|
|
2714
3374
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
2715
3375
|
if (name === "Date") return "string";
|
|
2716
3376
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
@@ -2718,14 +3378,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2718
3378
|
return "unknown";
|
|
2719
3379
|
const wrapperMode = WRAPPER_TYPES[name];
|
|
2720
3380
|
if (wrapperMode) {
|
|
2721
|
-
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3381
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode, subst);
|
|
2722
3382
|
}
|
|
2723
3383
|
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
2724
3384
|
return typeNode.getText();
|
|
2725
3385
|
}
|
|
2726
3386
|
const resolved = findType(name, sourceFile, project);
|
|
2727
3387
|
if (resolved) {
|
|
2728
|
-
|
|
3388
|
+
const childSubst = buildSubst(resolved, typeNode, sourceFile, project, depth, subst);
|
|
3389
|
+
return expandTypeDecl(resolved, project, depth - 1, childSubst);
|
|
2729
3390
|
}
|
|
2730
3391
|
dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
|
|
2731
3392
|
return "unknown";
|
|
@@ -2738,32 +3399,45 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
2738
3399
|
if (kind === SyntaxKind3.AnyKeyword) return "unknown";
|
|
2739
3400
|
return typeNode.getText();
|
|
2740
3401
|
}
|
|
2741
|
-
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
3402
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
|
|
2742
3403
|
const typeArgs = typeNode.getTypeArguments();
|
|
2743
3404
|
const firstTypeArg = typeArgs[0];
|
|
2744
3405
|
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2745
|
-
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3406
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth, subst);
|
|
2746
3407
|
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2747
3408
|
}
|
|
2748
3409
|
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2749
3410
|
}
|
|
2750
|
-
function
|
|
3411
|
+
function buildSubst(result, typeNode, sourceFile, project, depth, parentSubst) {
|
|
3412
|
+
if (result.kind !== "class" && result.kind !== "interface") return /* @__PURE__ */ new Map();
|
|
3413
|
+
const params = result.decl.getTypeParameters().map((p) => p.getName());
|
|
3414
|
+
if (params.length === 0) return /* @__PURE__ */ new Map();
|
|
3415
|
+
const args = typeNode.getTypeArguments();
|
|
3416
|
+
const subst = /* @__PURE__ */ new Map();
|
|
3417
|
+
params.forEach((param, i) => {
|
|
3418
|
+
const arg = args[i];
|
|
3419
|
+
if (arg)
|
|
3420
|
+
subst.set(param, resolveTypeNodeToString(arg, sourceFile, project, depth, parentSubst));
|
|
3421
|
+
});
|
|
3422
|
+
return subst;
|
|
3423
|
+
}
|
|
3424
|
+
function expandTypeDecl(result, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2751
3425
|
if (depth < 0) return "unknown";
|
|
2752
3426
|
switch (result.kind) {
|
|
2753
3427
|
case "class":
|
|
2754
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3428
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2755
3429
|
case "interface":
|
|
2756
|
-
return resolvePropertied(result.decl, result.file, project, depth);
|
|
3430
|
+
return resolvePropertied(result.decl, result.file, project, depth, subst);
|
|
2757
3431
|
case "typeAlias":
|
|
2758
3432
|
if (result.typeNode) {
|
|
2759
|
-
return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
|
|
3433
|
+
return resolveTypeNodeToString(result.typeNode, result.file, project, depth, subst);
|
|
2760
3434
|
}
|
|
2761
3435
|
return result.text;
|
|
2762
3436
|
case "enum":
|
|
2763
3437
|
return result.members.join(" | ");
|
|
2764
3438
|
}
|
|
2765
3439
|
}
|
|
2766
|
-
function resolvePropertied(decl, sourceFile, project, depth) {
|
|
3440
|
+
function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
|
|
2767
3441
|
if (depth < 0) return "unknown";
|
|
2768
3442
|
const lines = [];
|
|
2769
3443
|
for (const prop of decl.getProperties()) {
|
|
@@ -2772,7 +3446,7 @@ function resolvePropertied(decl, sourceFile, project, depth) {
|
|
|
2772
3446
|
const propTypeNode = prop.getTypeNode();
|
|
2773
3447
|
let propType = "unknown";
|
|
2774
3448
|
if (propTypeNode) {
|
|
2775
|
-
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
|
|
3449
|
+
propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
|
|
2776
3450
|
}
|
|
2777
3451
|
lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
|
|
2778
3452
|
}
|
|
@@ -2821,7 +3495,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
2821
3495
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
2822
3496
|
}
|
|
2823
3497
|
function extractResponseType(method, sourceFile, project) {
|
|
2824
|
-
const apiResponseDecorator = method.
|
|
3498
|
+
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2825
3499
|
if (apiResponseDecorator) {
|
|
2826
3500
|
const args = apiResponseDecorator.getArguments();
|
|
2827
3501
|
const optsArg = args[0];
|
|
@@ -2850,6 +3524,59 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
2850
3524
|
}
|
|
2851
3525
|
return "unknown";
|
|
2852
3526
|
}
|
|
3527
|
+
function apiResponseStatus(decorator) {
|
|
3528
|
+
const optsArg = decorator.getArguments()[0];
|
|
3529
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
3530
|
+
for (const prop of optsArg.getProperties()) {
|
|
3531
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3532
|
+
if (prop.getName() !== "status") continue;
|
|
3533
|
+
const val = prop.getInitializer();
|
|
3534
|
+
if (val && Node6.isNumericLiteral(val)) return Number(val.getLiteralValue());
|
|
3535
|
+
}
|
|
3536
|
+
return null;
|
|
3537
|
+
}
|
|
3538
|
+
function apiResponseTypeNode(decorator) {
|
|
3539
|
+
const optsArg = decorator.getArguments()[0];
|
|
3540
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
3541
|
+
for (const prop of optsArg.getProperties()) {
|
|
3542
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3543
|
+
if (prop.getName() !== "type") continue;
|
|
3544
|
+
const val = prop.getInitializer();
|
|
3545
|
+
if (!val) return null;
|
|
3546
|
+
if (Node6.isArrayLiteralExpression(val)) {
|
|
3547
|
+
const first = val.getElements()[0];
|
|
3548
|
+
return first ? { node: first, isArray: true } : null;
|
|
3549
|
+
}
|
|
3550
|
+
return { node: val, isArray: false };
|
|
3551
|
+
}
|
|
3552
|
+
return null;
|
|
3553
|
+
}
|
|
3554
|
+
function extractErrorType(method, sourceFile, project) {
|
|
3555
|
+
for (const decorator of method.getDecorators()) {
|
|
3556
|
+
if (decorator.getName() !== "ApiResponse") continue;
|
|
3557
|
+
const status = apiResponseStatus(decorator);
|
|
3558
|
+
if (status === null || status < 400) continue;
|
|
3559
|
+
const typeInfo = apiResponseTypeNode(decorator);
|
|
3560
|
+
if (!typeInfo) continue;
|
|
3561
|
+
const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
|
|
3562
|
+
const type = typeInfo.isArray ? `Array<${inner}>` : inner;
|
|
3563
|
+
let ref = null;
|
|
3564
|
+
if (Node6.isIdentifier(typeInfo.node)) {
|
|
3565
|
+
const name = typeInfo.node.getText();
|
|
3566
|
+
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3567
|
+
if (localDecl?.isExported()) {
|
|
3568
|
+
ref = { name, filePath: sourceFile.getFilePath(), isArray: typeInfo.isArray };
|
|
3569
|
+
} else {
|
|
3570
|
+
const resolved = resolveImportedType(name, sourceFile, project);
|
|
3571
|
+
if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
|
|
3572
|
+
ref = { name, filePath: resolved.file.getFilePath(), isArray: typeInfo.isArray };
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
return { type, ref };
|
|
3577
|
+
}
|
|
3578
|
+
return null;
|
|
3579
|
+
}
|
|
2853
3580
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
2854
3581
|
if (!Node6.isIdentifier(node)) return "unknown";
|
|
2855
3582
|
const name = node.getText();
|
|
@@ -2865,17 +3592,52 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
|
|
|
2865
3592
|
unwrapContainers: true
|
|
2866
3593
|
});
|
|
2867
3594
|
}
|
|
3595
|
+
var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
|
|
3596
|
+
var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
|
|
3597
|
+
var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
|
|
3598
|
+
function detectStreamElement(method) {
|
|
3599
|
+
const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
|
|
3600
|
+
let node = method.getReturnTypeNode();
|
|
3601
|
+
node = unwrapNamedContainer(node, /* @__PURE__ */ new Set(["Promise"]));
|
|
3602
|
+
const containerEl = streamContainerElement(node);
|
|
3603
|
+
if (containerEl) {
|
|
3604
|
+
return unwrapNamedContainer(containerEl, STREAM_ENVELOPES) ?? containerEl;
|
|
3605
|
+
}
|
|
3606
|
+
if (hasSse) return node ?? null;
|
|
3607
|
+
return null;
|
|
3608
|
+
}
|
|
3609
|
+
function streamContainerElement(node) {
|
|
3610
|
+
if (!node || !Node6.isTypeReference(node)) return null;
|
|
3611
|
+
const typeName = node.getTypeName();
|
|
3612
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3613
|
+
if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
|
|
3614
|
+
return node.getTypeArguments()[0] ?? null;
|
|
3615
|
+
}
|
|
3616
|
+
return null;
|
|
3617
|
+
}
|
|
3618
|
+
function unwrapNamedContainer(node, names) {
|
|
3619
|
+
if (!node || !Node6.isTypeReference(node)) return node;
|
|
3620
|
+
const typeName = node.getTypeName();
|
|
3621
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
|
|
3622
|
+
if (names.has(name)) {
|
|
3623
|
+
return node.getTypeArguments()[0] ?? node;
|
|
3624
|
+
}
|
|
3625
|
+
return node;
|
|
3626
|
+
}
|
|
2868
3627
|
function extractDtoContract(method, sourceFile, project) {
|
|
2869
3628
|
let body = extractBodyType(method, sourceFile, project);
|
|
2870
3629
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
2871
3630
|
const query = extractQueryType(method, sourceFile, project);
|
|
3631
|
+
const streamElement = detectStreamElement(method);
|
|
3632
|
+
const isStream = streamElement !== null;
|
|
2872
3633
|
if (filterInfo && filterInfo.source === "body") {
|
|
2873
3634
|
const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
|
|
2874
3635
|
body = body ?? bodyType;
|
|
2875
3636
|
}
|
|
2876
3637
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
2877
|
-
const response = extractResponseType(method, sourceFile, project);
|
|
2878
|
-
|
|
3638
|
+
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
3639
|
+
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
3640
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
2879
3641
|
return null;
|
|
2880
3642
|
}
|
|
2881
3643
|
let bodyRef = null;
|
|
@@ -2889,12 +3651,12 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2889
3651
|
queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
|
|
2890
3652
|
}
|
|
2891
3653
|
}
|
|
2892
|
-
const returnTypeNode = method.getReturnTypeNode();
|
|
3654
|
+
const returnTypeNode = isStream ? streamElement : method.getReturnTypeNode();
|
|
2893
3655
|
if (returnTypeNode) {
|
|
2894
3656
|
responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
|
|
2895
3657
|
}
|
|
2896
|
-
if (!responseRef) {
|
|
2897
|
-
const apiResp = method.
|
|
3658
|
+
if (!responseRef && !isStream) {
|
|
3659
|
+
const apiResp = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
2898
3660
|
if (apiResp) {
|
|
2899
3661
|
const args = apiResp.getArguments();
|
|
2900
3662
|
const optsArg = args[0];
|
|
@@ -2936,16 +3698,19 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2936
3698
|
query,
|
|
2937
3699
|
body,
|
|
2938
3700
|
response,
|
|
3701
|
+
error: errorInfo?.type ?? null,
|
|
2939
3702
|
params: paramsType,
|
|
2940
3703
|
queryRef,
|
|
2941
3704
|
bodyRef,
|
|
2942
3705
|
responseRef,
|
|
3706
|
+
errorRef: errorInfo?.ref ?? null,
|
|
2943
3707
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2944
3708
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2945
3709
|
filterSource: filterInfo?.source ?? null,
|
|
2946
3710
|
formWarnings,
|
|
2947
3711
|
bodySchema,
|
|
2948
|
-
querySchema
|
|
3712
|
+
querySchema,
|
|
3713
|
+
stream: isStream
|
|
2949
3714
|
};
|
|
2950
3715
|
}
|
|
2951
3716
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -3063,6 +3828,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
3063
3828
|
let query = null;
|
|
3064
3829
|
let body = null;
|
|
3065
3830
|
let response = "unknown";
|
|
3831
|
+
let error = null;
|
|
3066
3832
|
let bodyZodText = null;
|
|
3067
3833
|
let queryZodText = null;
|
|
3068
3834
|
for (const prop of optsArg.getProperties()) {
|
|
@@ -3078,25 +3844,38 @@ function parseDefineContractCall(callExpr) {
|
|
|
3078
3844
|
bodyZodText = val.getText();
|
|
3079
3845
|
} else if (propName === "response") {
|
|
3080
3846
|
response = zodAstToTs(val);
|
|
3847
|
+
} else if (propName === "error") {
|
|
3848
|
+
error = zodAstToTs(val);
|
|
3081
3849
|
}
|
|
3082
3850
|
}
|
|
3083
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3851
|
+
return { query, body, response, error, bodyZodText, queryZodText };
|
|
3084
3852
|
}
|
|
3085
3853
|
|
|
3086
3854
|
// src/discovery/contracts-fast.ts
|
|
3087
3855
|
async function discoverContractsFast(opts) {
|
|
3088
3856
|
const { cwd, glob, tsconfig } = opts;
|
|
3089
|
-
const tsconfigPath =
|
|
3090
|
-
|
|
3857
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3858
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3859
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3860
|
+
for (const f of files) {
|
|
3861
|
+
project.addSourceFileAtPath(f);
|
|
3862
|
+
}
|
|
3863
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3864
|
+
return extractAllRoutes(project);
|
|
3865
|
+
}
|
|
3866
|
+
function resolveTsconfigPath(cwd, tsconfig) {
|
|
3867
|
+
return tsconfig ? resolve3(tsconfig) : join13(cwd, "tsconfig.json");
|
|
3868
|
+
}
|
|
3869
|
+
function createDiscoveryProject(tsconfigPath) {
|
|
3091
3870
|
try {
|
|
3092
|
-
|
|
3871
|
+
return new Project3({
|
|
3093
3872
|
tsConfigFilePath: tsconfigPath,
|
|
3094
3873
|
skipAddingFilesFromTsConfig: true,
|
|
3095
3874
|
skipLoadingLibFiles: true,
|
|
3096
3875
|
skipFileDependencyResolution: true
|
|
3097
3876
|
});
|
|
3098
3877
|
} catch {
|
|
3099
|
-
|
|
3878
|
+
return new Project3({
|
|
3100
3879
|
skipAddingFilesFromTsConfig: true,
|
|
3101
3880
|
skipLoadingLibFiles: true,
|
|
3102
3881
|
skipFileDependencyResolution: true,
|
|
@@ -3107,20 +3886,105 @@ async function discoverContractsFast(opts) {
|
|
|
3107
3886
|
}
|
|
3108
3887
|
});
|
|
3109
3888
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
project.addSourceFileAtPath(f);
|
|
3113
|
-
}
|
|
3114
|
-
const routes = [];
|
|
3889
|
+
}
|
|
3890
|
+
function bindDiscoveryContext(project, cwd, tsconfigPath) {
|
|
3115
3891
|
setDiscoveryContext(project, {
|
|
3116
3892
|
projectRoot: cwd,
|
|
3117
3893
|
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3118
3894
|
});
|
|
3895
|
+
}
|
|
3896
|
+
function extractRoutesFrom(project, controllerPaths) {
|
|
3897
|
+
const routes = [];
|
|
3898
|
+
for (const path of controllerPaths) {
|
|
3899
|
+
const sourceFile = project.getSourceFile(path);
|
|
3900
|
+
if (sourceFile) routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3901
|
+
}
|
|
3902
|
+
return routes;
|
|
3903
|
+
}
|
|
3904
|
+
function extractAllRoutes(project) {
|
|
3905
|
+
const routes = [];
|
|
3119
3906
|
for (const sourceFile of project.getSourceFiles()) {
|
|
3120
3907
|
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3121
3908
|
}
|
|
3122
3909
|
return routes;
|
|
3123
3910
|
}
|
|
3911
|
+
var PersistentDiscovery = class _PersistentDiscovery {
|
|
3912
|
+
project;
|
|
3913
|
+
cwd;
|
|
3914
|
+
glob;
|
|
3915
|
+
/** Absolute paths of the controllers currently loaded as extraction roots. */
|
|
3916
|
+
controllerPaths = /* @__PURE__ */ new Set();
|
|
3917
|
+
constructor(project, cwd, glob) {
|
|
3918
|
+
this.project = project;
|
|
3919
|
+
this.cwd = cwd;
|
|
3920
|
+
this.glob = glob;
|
|
3921
|
+
}
|
|
3922
|
+
/**
|
|
3923
|
+
* Build the initial persistent Project: create it, glob + add all controllers,
|
|
3924
|
+
* bind the discovery context. Mirrors {@link discoverContractsFast}'s setup.
|
|
3925
|
+
*/
|
|
3926
|
+
static async create(opts) {
|
|
3927
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3928
|
+
const tsconfigPath = resolveTsconfigPath(cwd, tsconfig);
|
|
3929
|
+
const project = createDiscoveryProject(tsconfigPath);
|
|
3930
|
+
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
3931
|
+
const instance = new _PersistentDiscovery(project, cwd, glob);
|
|
3932
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3933
|
+
for (const f of files) {
|
|
3934
|
+
project.addSourceFileAtPath(f);
|
|
3935
|
+
instance.controllerPaths.add(f);
|
|
3936
|
+
}
|
|
3937
|
+
return instance;
|
|
3938
|
+
}
|
|
3939
|
+
/** Run the initial extraction (equivalent to a first `discoverContractsFast`). */
|
|
3940
|
+
discover() {
|
|
3941
|
+
return this.runExtraction();
|
|
3942
|
+
}
|
|
3943
|
+
/**
|
|
3944
|
+
* Re-discover after one or more files changed. Refreshes the changed file(s)
|
|
3945
|
+
* from disk (controllers AND any lazily-loaded DTO/imported files), re-globs
|
|
3946
|
+
* to pick up added/removed controllers, clears the per-Project caches, then
|
|
3947
|
+
* re-extracts. `changedPaths` is a hint; correctness does not depend on it
|
|
3948
|
+
* being exhaustive because re-globbing + refresh-on-presence covers the set.
|
|
3949
|
+
*/
|
|
3950
|
+
async rediscover(changedPaths) {
|
|
3951
|
+
if (changedPaths) {
|
|
3952
|
+
for (const p of changedPaths) {
|
|
3953
|
+
const abs = resolve3(p);
|
|
3954
|
+
const sf = this.project.getSourceFile(abs);
|
|
3955
|
+
if (sf) {
|
|
3956
|
+
await sf.refreshFromFileSystem();
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
const globbed = new Set(
|
|
3961
|
+
await fg2(this.glob, { cwd: this.cwd, absolute: true, onlyFiles: true })
|
|
3962
|
+
);
|
|
3963
|
+
for (const f of globbed) {
|
|
3964
|
+
if (!this.controllerPaths.has(f)) {
|
|
3965
|
+
try {
|
|
3966
|
+
this.project.addSourceFileAtPath(f);
|
|
3967
|
+
this.controllerPaths.add(f);
|
|
3968
|
+
} catch {
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
for (const f of this.controllerPaths) {
|
|
3973
|
+
if (!globbed.has(f)) {
|
|
3974
|
+
const sf = this.project.getSourceFile(f);
|
|
3975
|
+
if (sf) this.project.removeSourceFile(sf);
|
|
3976
|
+
this.controllerPaths.delete(f);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
return this.runExtraction();
|
|
3980
|
+
}
|
|
3981
|
+
/** Clear stale per-Project caches, then extract over the controller set. */
|
|
3982
|
+
runExtraction() {
|
|
3983
|
+
clearTypeResolutionCaches(this.project);
|
|
3984
|
+
clearEnumCache(this.project);
|
|
3985
|
+
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
3986
|
+
}
|
|
3987
|
+
};
|
|
3124
3988
|
function decoratorStringArg(decoratorExpr) {
|
|
3125
3989
|
if (!decoratorExpr) return void 0;
|
|
3126
3990
|
if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
@@ -3176,6 +4040,11 @@ function resolveVerb(method) {
|
|
|
3176
4040
|
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3177
4041
|
}
|
|
3178
4042
|
}
|
|
4043
|
+
const sseDecorator = method.getDecorator("Sse");
|
|
4044
|
+
if (sseDecorator) {
|
|
4045
|
+
const pathArg = sseDecorator.getArguments()[0];
|
|
4046
|
+
return { httpMethod: "GET", handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
4047
|
+
}
|
|
3179
4048
|
return null;
|
|
3180
4049
|
}
|
|
3181
4050
|
function readAsDecorator(node, label) {
|
|
@@ -3218,7 +4087,17 @@ function buildRoute(args) {
|
|
|
3218
4087
|
};
|
|
3219
4088
|
}
|
|
3220
4089
|
function extractContractRoute(args) {
|
|
3221
|
-
const {
|
|
4090
|
+
const {
|
|
4091
|
+
cls,
|
|
4092
|
+
method,
|
|
4093
|
+
applyContractDecorator,
|
|
4094
|
+
verb,
|
|
4095
|
+
prefix,
|
|
4096
|
+
className,
|
|
4097
|
+
sourceFile,
|
|
4098
|
+
project,
|
|
4099
|
+
seenNames
|
|
4100
|
+
} = args;
|
|
3222
4101
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3223
4102
|
if (!firstDecoratorArg) return null;
|
|
3224
4103
|
let contractDef = null;
|
|
@@ -3228,18 +4107,19 @@ function extractContractRoute(args) {
|
|
|
3228
4107
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3229
4108
|
} else if (Node8.isIdentifier(firstDecoratorArg)) {
|
|
3230
4109
|
const identName = firstDecoratorArg.getText();
|
|
3231
|
-
const
|
|
3232
|
-
if (!
|
|
4110
|
+
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
4111
|
+
if (!resolvedVar) {
|
|
3233
4112
|
console.warn(
|
|
3234
|
-
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()}
|
|
4113
|
+
`[nestjs-codegen/fast] Cannot resolve contract identifier '${identName}' applied in ${sourceFile.getFilePath()} \u2014 the import could not be followed to a declaration; skipping`
|
|
3235
4114
|
);
|
|
3236
4115
|
return null;
|
|
3237
4116
|
}
|
|
4117
|
+
const { decl: varDecl, file: declFile } = resolvedVar;
|
|
3238
4118
|
const initializer = varDecl.getInitializer();
|
|
3239
4119
|
if (!initializer) return null;
|
|
3240
4120
|
contractDef = parseDefineContractCall(initializer);
|
|
3241
4121
|
if (contractDef && varDecl.isExported()) {
|
|
3242
|
-
const filePath =
|
|
4122
|
+
const filePath = declFile.getFilePath();
|
|
3243
4123
|
if (contractDef.body !== null) {
|
|
3244
4124
|
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3245
4125
|
}
|
|
@@ -3272,6 +4152,7 @@ function extractContractRoute(args) {
|
|
|
3272
4152
|
query: contractDef.query,
|
|
3273
4153
|
body: contractDef.body,
|
|
3274
4154
|
response: contractDef.response,
|
|
4155
|
+
error: contractDef.error,
|
|
3275
4156
|
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3276
4157
|
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3277
4158
|
// would drag server-only deps into the client bundle).
|
|
@@ -3303,15 +4184,18 @@ function extractDtoRoute(args) {
|
|
|
3303
4184
|
query: dtoContract?.query ?? null,
|
|
3304
4185
|
body: dtoContract?.body ?? null,
|
|
3305
4186
|
response: dtoContract?.response ?? "unknown",
|
|
4187
|
+
error: dtoContract?.error ?? null,
|
|
3306
4188
|
queryRef: dtoContract?.queryRef ?? null,
|
|
3307
4189
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3308
4190
|
responseRef: dtoContract?.responseRef ?? null,
|
|
4191
|
+
errorRef: dtoContract?.errorRef ?? null,
|
|
3309
4192
|
filterFields: dtoContract?.filterFields ?? null,
|
|
3310
4193
|
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3311
4194
|
filterSource: dtoContract?.filterSource ?? null,
|
|
3312
4195
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3313
4196
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3314
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
4197
|
+
querySchema: dtoContract?.querySchema ?? null,
|
|
4198
|
+
stream: dtoContract?.stream ?? false
|
|
3315
4199
|
}
|
|
3316
4200
|
});
|
|
3317
4201
|
}
|
|
@@ -3335,6 +4219,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3335
4219
|
prefix,
|
|
3336
4220
|
className,
|
|
3337
4221
|
sourceFile,
|
|
4222
|
+
project,
|
|
3338
4223
|
seenNames
|
|
3339
4224
|
}) : extractDtoRoute({
|
|
3340
4225
|
cls,
|
|
@@ -3354,8 +4239,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3354
4239
|
|
|
3355
4240
|
// src/watch/lock-file.ts
|
|
3356
4241
|
import { open } from "fs/promises";
|
|
3357
|
-
import { mkdir as
|
|
3358
|
-
import { join as
|
|
4242
|
+
import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
|
|
4243
|
+
import { join as join14 } from "path";
|
|
3359
4244
|
var LOCK_FILE = ".watcher.lock";
|
|
3360
4245
|
function isProcessAlive(pid) {
|
|
3361
4246
|
try {
|
|
@@ -3366,8 +4251,8 @@ function isProcessAlive(pid) {
|
|
|
3366
4251
|
}
|
|
3367
4252
|
}
|
|
3368
4253
|
async function acquireLock(outDir) {
|
|
3369
|
-
await
|
|
3370
|
-
const lockPath =
|
|
4254
|
+
await mkdir10(outDir, { recursive: true });
|
|
4255
|
+
const lockPath = join14(outDir, LOCK_FILE);
|
|
3371
4256
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3372
4257
|
try {
|
|
3373
4258
|
const fd = await open(lockPath, "wx");
|
|
@@ -3407,7 +4292,7 @@ async function watch(config, onChange) {
|
|
|
3407
4292
|
if (lock === null) {
|
|
3408
4293
|
let holderPid = "unknown";
|
|
3409
4294
|
try {
|
|
3410
|
-
const raw = await readFile3(
|
|
4295
|
+
const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3411
4296
|
const data = JSON.parse(raw);
|
|
3412
4297
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3413
4298
|
} catch {
|
|
@@ -3417,12 +4302,20 @@ async function watch(config, onChange) {
|
|
|
3417
4302
|
);
|
|
3418
4303
|
return NO_OP_WATCHER;
|
|
3419
4304
|
}
|
|
4305
|
+
let discovery = null;
|
|
4306
|
+
async function getDiscovery() {
|
|
4307
|
+
if (discovery === null) {
|
|
4308
|
+
discovery = await PersistentDiscovery.create({
|
|
4309
|
+
cwd: config.codegen.cwd,
|
|
4310
|
+
glob: config.contracts.glob,
|
|
4311
|
+
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
4312
|
+
});
|
|
4313
|
+
return discovery;
|
|
4314
|
+
}
|
|
4315
|
+
return discovery;
|
|
4316
|
+
}
|
|
3420
4317
|
try {
|
|
3421
|
-
const initialRoutes = await
|
|
3422
|
-
cwd: config.codegen.cwd,
|
|
3423
|
-
glob: config.contracts.glob,
|
|
3424
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3425
|
-
});
|
|
4318
|
+
const initialRoutes = (await getDiscovery()).discover();
|
|
3426
4319
|
await generate(config, initialRoutes);
|
|
3427
4320
|
} catch (err) {
|
|
3428
4321
|
console.warn(
|
|
@@ -3435,7 +4328,7 @@ async function watch(config, onChange) {
|
|
|
3435
4328
|
}
|
|
3436
4329
|
let pagesDebounceTimer;
|
|
3437
4330
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3438
|
-
const pagesWatcher = chokidar.watch(
|
|
4331
|
+
const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
|
|
3439
4332
|
ignoreInitial: true,
|
|
3440
4333
|
persistent: true,
|
|
3441
4334
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3461,23 +4354,23 @@ async function watch(config, onChange) {
|
|
|
3461
4354
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3462
4355
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3463
4356
|
let contractsDebounceTimer;
|
|
3464
|
-
const
|
|
4357
|
+
const pendingChangedPaths = /* @__PURE__ */ new Set();
|
|
4358
|
+
const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
|
|
3465
4359
|
ignoreInitial: true,
|
|
3466
4360
|
persistent: true,
|
|
3467
4361
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3468
4362
|
});
|
|
3469
|
-
function scheduleContractsRegenerate() {
|
|
4363
|
+
function scheduleContractsRegenerate(changedPath) {
|
|
4364
|
+
if (typeof changedPath === "string") pendingChangedPaths.add(changedPath);
|
|
3470
4365
|
if (contractsDebounceTimer !== void 0) {
|
|
3471
4366
|
clearTimeout(contractsDebounceTimer);
|
|
3472
4367
|
}
|
|
3473
4368
|
contractsDebounceTimer = setTimeout(async () => {
|
|
3474
4369
|
contractsDebounceTimer = void 0;
|
|
4370
|
+
const changed = [...pendingChangedPaths];
|
|
4371
|
+
pendingChangedPaths.clear();
|
|
3475
4372
|
try {
|
|
3476
|
-
const routes = await
|
|
3477
|
-
cwd: config.codegen.cwd,
|
|
3478
|
-
glob: config.contracts.glob,
|
|
3479
|
-
...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
|
|
3480
|
-
});
|
|
4373
|
+
const routes = await (await getDiscovery()).rediscover(changed);
|
|
3481
4374
|
await generate(config, routes);
|
|
3482
4375
|
} catch (err) {
|
|
3483
4376
|
console.error(
|
|
@@ -3488,17 +4381,17 @@ async function watch(config, onChange) {
|
|
|
3488
4381
|
onChange?.();
|
|
3489
4382
|
}, config.contracts.debounceMs);
|
|
3490
4383
|
}
|
|
3491
|
-
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3492
|
-
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3493
|
-
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3494
|
-
const formsWatcher = chokidar.watch(
|
|
4384
|
+
contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4385
|
+
contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4386
|
+
contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
4387
|
+
const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
|
|
3495
4388
|
ignoreInitial: true,
|
|
3496
4389
|
persistent: true,
|
|
3497
4390
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
3498
4391
|
});
|
|
3499
|
-
formsWatcher.on("add", scheduleContractsRegenerate);
|
|
3500
|
-
formsWatcher.on("change", scheduleContractsRegenerate);
|
|
3501
|
-
formsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
4392
|
+
formsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
|
|
4393
|
+
formsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
|
|
4394
|
+
formsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
|
|
3502
4395
|
return {
|
|
3503
4396
|
close: async () => {
|
|
3504
4397
|
if (pagesDebounceTimer !== void 0) {
|
|
@@ -3518,7 +4411,7 @@ async function watch(config, onChange) {
|
|
|
3518
4411
|
}
|
|
3519
4412
|
|
|
3520
4413
|
// src/index.ts
|
|
3521
|
-
var VERSION = "0.
|
|
4414
|
+
var VERSION = "0.5.0";
|
|
3522
4415
|
|
|
3523
4416
|
// src/cli/codegen.ts
|
|
3524
4417
|
async function runCodegen(opts = {}) {
|
|
@@ -3547,13 +4440,13 @@ async function runCodegen(opts = {}) {
|
|
|
3547
4440
|
// src/cli/doctor.ts
|
|
3548
4441
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3549
4442
|
import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
3550
|
-
import { join as
|
|
4443
|
+
import { join as join17 } from "path";
|
|
3551
4444
|
|
|
3552
4445
|
// src/cli/init.ts
|
|
3553
4446
|
import { execFileSync } from "child_process";
|
|
3554
4447
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
3555
|
-
import { access as access2, mkdir as
|
|
3556
|
-
import { join as
|
|
4448
|
+
import { access as access2, mkdir as mkdir11, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
|
|
4449
|
+
import { join as join16 } from "path";
|
|
3557
4450
|
import { createInterface } from "readline";
|
|
3558
4451
|
|
|
3559
4452
|
// src/cli/patch-utils.ts
|
|
@@ -3615,7 +4508,7 @@ ${bold(title)}`);
|
|
|
3615
4508
|
}
|
|
3616
4509
|
async function readPackageJson(cwd) {
|
|
3617
4510
|
try {
|
|
3618
|
-
const raw = await readFile4(
|
|
4511
|
+
const raw = await readFile4(join16(cwd, "package.json"), "utf8");
|
|
3619
4512
|
return JSON.parse(raw);
|
|
3620
4513
|
} catch {
|
|
3621
4514
|
return {};
|
|
@@ -3646,7 +4539,7 @@ async function detectTemplateEngine(cwd) {
|
|
|
3646
4539
|
async function detectPackageManager(cwd) {
|
|
3647
4540
|
async function exists(file) {
|
|
3648
4541
|
try {
|
|
3649
|
-
await access2(
|
|
4542
|
+
await access2(join16(cwd, file));
|
|
3650
4543
|
return true;
|
|
3651
4544
|
} catch {
|
|
3652
4545
|
return false;
|
|
@@ -3687,13 +4580,13 @@ async function writeIfNotExists(filePath, content, label) {
|
|
|
3687
4580
|
}
|
|
3688
4581
|
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
3689
4582
|
if (dir) {
|
|
3690
|
-
await
|
|
4583
|
+
await mkdir11(dir, { recursive: true });
|
|
3691
4584
|
}
|
|
3692
|
-
await
|
|
4585
|
+
await writeFile10(filePath, content, "utf8");
|
|
3693
4586
|
logCreated(label);
|
|
3694
4587
|
}
|
|
3695
4588
|
async function handleViteConfig(cwd, framework) {
|
|
3696
|
-
const filePath =
|
|
4589
|
+
const filePath = join16(cwd, "vite.config.ts");
|
|
3697
4590
|
if (await fileExists2(filePath)) {
|
|
3698
4591
|
const existing = await readFile4(filePath, "utf8");
|
|
3699
4592
|
const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
|
|
@@ -3711,9 +4604,9 @@ async function handleViteConfig(cwd, framework) {
|
|
|
3711
4604
|
}
|
|
3712
4605
|
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
3713
4606
|
if (dir) {
|
|
3714
|
-
await
|
|
4607
|
+
await mkdir11(dir, { recursive: true });
|
|
3715
4608
|
}
|
|
3716
|
-
await
|
|
4609
|
+
await writeFile10(filePath, viteConfigTemplate(framework), "utf8");
|
|
3717
4610
|
logCreated("vite.config.ts");
|
|
3718
4611
|
}
|
|
3719
4612
|
async function patchGitignore(gitignorePath) {
|
|
@@ -3729,7 +4622,7 @@ async function patchGitignore(gitignorePath) {
|
|
|
3729
4622
|
` : `${existing}
|
|
3730
4623
|
${GITIGNORE_ENTRY}
|
|
3731
4624
|
`;
|
|
3732
|
-
await
|
|
4625
|
+
await writeFile10(gitignorePath, newContent, "utf8");
|
|
3733
4626
|
logPatched(".gitignore", "added .nestjs-inertia/");
|
|
3734
4627
|
}
|
|
3735
4628
|
function installDeps(pkgManager, deps, dev) {
|
|
@@ -3751,7 +4644,7 @@ function installDeps(pkgManager, deps, dev) {
|
|
|
3751
4644
|
}
|
|
3752
4645
|
}
|
|
3753
4646
|
async function patchPackageJsonScripts(cwd, scripts) {
|
|
3754
|
-
const pkgPath =
|
|
4647
|
+
const pkgPath = join16(cwd, "package.json");
|
|
3755
4648
|
let pkg = {};
|
|
3756
4649
|
try {
|
|
3757
4650
|
pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
@@ -3773,7 +4666,7 @@ async function patchPackageJsonScripts(cwd, scripts) {
|
|
|
3773
4666
|
return;
|
|
3774
4667
|
}
|
|
3775
4668
|
pkg.scripts = existing;
|
|
3776
|
-
await
|
|
4669
|
+
await writeFile10(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
3777
4670
|
`, "utf8");
|
|
3778
4671
|
}
|
|
3779
4672
|
function patchAppModule(filePath, rootView) {
|
|
@@ -4068,7 +4961,7 @@ export class HomeController {
|
|
|
4068
4961
|
}
|
|
4069
4962
|
`;
|
|
4070
4963
|
function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
|
|
4071
|
-
const filePath =
|
|
4964
|
+
const filePath = join16(cwd, filename);
|
|
4072
4965
|
return patchJsonFile(
|
|
4073
4966
|
filePath,
|
|
4074
4967
|
(json) => {
|
|
@@ -4083,7 +4976,7 @@ function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
|
|
|
4083
4976
|
);
|
|
4084
4977
|
}
|
|
4085
4978
|
function patchNestCliJson(cwd, shellDir) {
|
|
4086
|
-
const filePath =
|
|
4979
|
+
const filePath = join16(cwd, "nest-cli.json");
|
|
4087
4980
|
return patchJsonFile(filePath, (json) => {
|
|
4088
4981
|
const compiler = json.compilerOptions ?? {};
|
|
4089
4982
|
const assets = compiler.assets ?? [];
|
|
@@ -4106,46 +4999,46 @@ async function scaffoldFiles(ctx) {
|
|
|
4106
4999
|
const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
|
|
4107
5000
|
logSection("Scaffold files");
|
|
4108
5001
|
await writeIfNotExists(
|
|
4109
|
-
|
|
5002
|
+
join16(cwd, "nestjs-inertia.config.ts"),
|
|
4110
5003
|
configTemplate(framework),
|
|
4111
5004
|
"nestjs-inertia.config.ts"
|
|
4112
5005
|
);
|
|
4113
|
-
await writeIfNotExists(
|
|
5006
|
+
await writeIfNotExists(join16(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
|
|
4114
5007
|
await writeIfNotExists(
|
|
4115
|
-
|
|
5008
|
+
join16(cwd, "tsconfig.inertia.json"),
|
|
4116
5009
|
TSCONFIG_INERTIA_TEMPLATE,
|
|
4117
5010
|
"tsconfig.inertia.json"
|
|
4118
5011
|
);
|
|
4119
5012
|
await writeIfNotExists(
|
|
4120
|
-
|
|
5013
|
+
join16(cwd, "inertia", "tsconfig.json"),
|
|
4121
5014
|
INERTIA_TSCONFIG_TEMPLATE,
|
|
4122
5015
|
"inertia/tsconfig.json"
|
|
4123
5016
|
);
|
|
4124
5017
|
await writeIfNotExists(
|
|
4125
|
-
|
|
5018
|
+
join16(cwd, "inertia", shellFileName),
|
|
4126
5019
|
htmlShellTemplate(framework, engine),
|
|
4127
5020
|
`inertia/${shellFileName}`
|
|
4128
5021
|
);
|
|
4129
5022
|
await handleViteConfig(cwd, framework);
|
|
4130
5023
|
await writeIfNotExists(
|
|
4131
|
-
|
|
5024
|
+
join16(cwd, "inertia", "app", `client.${entryExt}`),
|
|
4132
5025
|
entryPointTemplate(framework),
|
|
4133
5026
|
`inertia/app/client.${entryExt}`
|
|
4134
5027
|
);
|
|
4135
5028
|
await writeIfNotExists(
|
|
4136
|
-
|
|
5029
|
+
join16(cwd, "inertia", "pages", `Home.${pageExt}`),
|
|
4137
5030
|
samplePageTemplate(framework),
|
|
4138
5031
|
`inertia/pages/Home.${pageExt}`
|
|
4139
5032
|
);
|
|
4140
5033
|
await writeIfNotExists(
|
|
4141
|
-
|
|
5034
|
+
join16(cwd, "src", "home.controller.ts"),
|
|
4142
5035
|
SAMPLE_CONTROLLER,
|
|
4143
5036
|
"src/home.controller.ts"
|
|
4144
5037
|
);
|
|
4145
5038
|
}
|
|
4146
5039
|
function patchServerAppModule(ctx) {
|
|
4147
5040
|
const { cwd, rootView } = ctx;
|
|
4148
|
-
const appModulePath =
|
|
5041
|
+
const appModulePath = join16(cwd, "src", "app.module.ts");
|
|
4149
5042
|
const appModuleResult = patchAppModule(appModulePath, rootView);
|
|
4150
5043
|
if (appModuleResult === "patched") {
|
|
4151
5044
|
logPatched("src/app.module.ts", "added InertiaModule.forRoot");
|
|
@@ -4159,7 +5052,7 @@ function patchServerAppModule(ctx) {
|
|
|
4159
5052
|
}
|
|
4160
5053
|
}
|
|
4161
5054
|
function patchServerMainTs(ctx) {
|
|
4162
|
-
const mainTsPath =
|
|
5055
|
+
const mainTsPath = join16(ctx.cwd, "src", "main.ts");
|
|
4163
5056
|
const mainTsResult = patchMainTs(mainTsPath);
|
|
4164
5057
|
if (mainTsResult === "patched") {
|
|
4165
5058
|
logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
|
|
@@ -4194,7 +5087,7 @@ function patchBuildConfigs(ctx) {
|
|
|
4194
5087
|
}
|
|
4195
5088
|
async function patchGitignoreAndDist(ctx) {
|
|
4196
5089
|
const { cwd } = ctx;
|
|
4197
|
-
await patchGitignore(
|
|
5090
|
+
await patchGitignore(join16(cwd, ".gitignore"));
|
|
4198
5091
|
const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
|
|
4199
5092
|
if (tsconfigDistResult === "patched") {
|
|
4200
5093
|
logPatched("tsconfig.json", "excluded dist/ from server compilation");
|
|
@@ -4300,7 +5193,7 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
|
|
|
4300
5193
|
|
|
4301
5194
|
// src/cli/doctor.ts
|
|
4302
5195
|
function checkFileExists(cwd, file) {
|
|
4303
|
-
return existsSync(
|
|
5196
|
+
return existsSync(join17(cwd, file));
|
|
4304
5197
|
}
|
|
4305
5198
|
function readJson(path) {
|
|
4306
5199
|
try {
|
|
@@ -4336,15 +5229,15 @@ function writeJsonField(filePath, dotPath, value) {
|
|
|
4336
5229
|
}
|
|
4337
5230
|
function getPackageVersion(cwd, pkg) {
|
|
4338
5231
|
try {
|
|
4339
|
-
const pkgJson = readJson(
|
|
5232
|
+
const pkgJson = readJson(join17(cwd, "node_modules", pkg, "package.json"));
|
|
4340
5233
|
return pkgJson?.version ?? null;
|
|
4341
5234
|
} catch {
|
|
4342
5235
|
return null;
|
|
4343
5236
|
}
|
|
4344
5237
|
}
|
|
4345
5238
|
function detectPkgManager(cwd) {
|
|
4346
|
-
if (existsSync(
|
|
4347
|
-
if (existsSync(
|
|
5239
|
+
if (existsSync(join17(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
5240
|
+
if (existsSync(join17(cwd, "yarn.lock"))) return "yarn";
|
|
4348
5241
|
return "npm";
|
|
4349
5242
|
}
|
|
4350
5243
|
async function runDoctor(opts) {
|
|
@@ -4382,7 +5275,7 @@ async function runDoctor(opts) {
|
|
|
4382
5275
|
autoFix: () => runInit({ cwd })
|
|
4383
5276
|
});
|
|
4384
5277
|
if (foundShellDir) {
|
|
4385
|
-
const nestCliPath =
|
|
5278
|
+
const nestCliPath = join17(cwd, "nest-cli.json");
|
|
4386
5279
|
const nestCli = readJson(nestCliPath);
|
|
4387
5280
|
const compiler = nestCli?.compilerOptions ?? {};
|
|
4388
5281
|
const assets = compiler.assets ?? [];
|
|
@@ -4421,7 +5314,7 @@ async function runDoctor(opts) {
|
|
|
4421
5314
|
fix: "Run: nestjs-codegen codegen",
|
|
4422
5315
|
autoFix: () => runCodegen({ cwd })
|
|
4423
5316
|
});
|
|
4424
|
-
const tsconfigPath =
|
|
5317
|
+
const tsconfigPath = join17(cwd, "tsconfig.json");
|
|
4425
5318
|
const tsconfig = readJson(tsconfigPath);
|
|
4426
5319
|
const paths = tsconfig?.compilerOptions?.paths;
|
|
4427
5320
|
checks.push({
|
|
@@ -4432,7 +5325,7 @@ async function runDoctor(opts) {
|
|
|
4432
5325
|
});
|
|
4433
5326
|
const inertiaDir = foundShellDir ?? "inertia";
|
|
4434
5327
|
for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
|
|
4435
|
-
const tsc = readJson(
|
|
5328
|
+
const tsc = readJson(join17(cwd, tsconfigFile));
|
|
4436
5329
|
if (!tsc) continue;
|
|
4437
5330
|
const excl = tsc.exclude ?? [];
|
|
4438
5331
|
const excludesIt = excl.includes(inertiaDir);
|
|
@@ -4458,7 +5351,7 @@ async function runDoctor(opts) {
|
|
|
4458
5351
|
}
|
|
4459
5352
|
});
|
|
4460
5353
|
}
|
|
4461
|
-
const inertiaTsconfigPath =
|
|
5354
|
+
const inertiaTsconfigPath = join17(cwd, "tsconfig.inertia.json");
|
|
4462
5355
|
const inertiaTsconfig = readJson(inertiaTsconfigPath);
|
|
4463
5356
|
checks.push({
|
|
4464
5357
|
name: "tsconfig.inertia.json exists",
|
|
@@ -4510,7 +5403,7 @@ async function runDoctor(opts) {
|
|
|
4510
5403
|
fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
|
|
4511
5404
|
});
|
|
4512
5405
|
}
|
|
4513
|
-
const innerTsconfigPath =
|
|
5406
|
+
const innerTsconfigPath = join17(cwd, "inertia", "tsconfig.json");
|
|
4514
5407
|
checks.push({
|
|
4515
5408
|
name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
|
|
4516
5409
|
pass: existsSync(innerTsconfigPath),
|
|
@@ -4520,7 +5413,7 @@ async function runDoctor(opts) {
|
|
|
4520
5413
|
}
|
|
4521
5414
|
});
|
|
4522
5415
|
if (checkFileExists(cwd, "vite.config.ts")) {
|
|
4523
|
-
const viteContent = readFileSync4(
|
|
5416
|
+
const viteContent = readFileSync4(join17(cwd, "vite.config.ts"), "utf8");
|
|
4524
5417
|
checks.push({
|
|
4525
5418
|
name: "vite.config.ts has resolve.alias",
|
|
4526
5419
|
pass: viteContent.includes("resolve") && viteContent.includes("alias"),
|
|
@@ -4585,7 +5478,7 @@ async function runDoctor(opts) {
|
|
|
4585
5478
|
});
|
|
4586
5479
|
}
|
|
4587
5480
|
if (checkFileExists(cwd, ".gitignore")) {
|
|
4588
|
-
const gitignorePath =
|
|
5481
|
+
const gitignorePath = join17(cwd, ".gitignore");
|
|
4589
5482
|
const gitignore = readFileSync4(gitignorePath, "utf8");
|
|
4590
5483
|
checks.push({
|
|
4591
5484
|
name: ".gitignore includes .nestjs-inertia/",
|
|
@@ -4594,7 +5487,7 @@ async function runDoctor(opts) {
|
|
|
4594
5487
|
autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
|
|
4595
5488
|
});
|
|
4596
5489
|
}
|
|
4597
|
-
const pkgJsonPath =
|
|
5490
|
+
const pkgJsonPath = join17(cwd, "package.json");
|
|
4598
5491
|
const pkgJson = readJson(pkgJsonPath);
|
|
4599
5492
|
const scripts = pkgJson?.scripts ?? {};
|
|
4600
5493
|
checks.push({
|