@dudousxd/nestjs-codegen 0.2.1 → 0.3.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 +30 -0
- package/dist/cli/main.cjs +1214 -1613
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1188 -1587
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.cjs +12 -2
- package/dist/extension/index.cjs.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js +10 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/{index-BwIRjOQA.d.cts → index-oH5t7x4G.d.cts} +56 -41
- package/dist/{index-BwIRjOQA.d.ts → index-oH5t7x4G.d.ts} +56 -41
- package/dist/index.cjs +1003 -1457
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -16
- package/dist/index.d.ts +32 -16
- package/dist/index.js +977 -1430
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +908 -1355
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +9 -2
- package/dist/nest/index.d.ts +9 -2
- package/dist/nest/index.js +893 -1340
- package/dist/nest/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -22,112 +22,9 @@ var CodegenError = class extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
// src/adapters/zod.ts
|
|
26
|
-
function toObjectKey(name) {
|
|
27
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
28
|
-
}
|
|
29
|
-
function messageSuffix(messageRaw2) {
|
|
30
|
-
return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
|
|
31
|
-
}
|
|
32
|
-
function renderStringChecks(checks) {
|
|
33
|
-
let out = "";
|
|
34
|
-
for (const c of checks) {
|
|
35
|
-
switch (c.check) {
|
|
36
|
-
case "email":
|
|
37
|
-
out += `.email(${messageSuffix(c.messageRaw)})`;
|
|
38
|
-
break;
|
|
39
|
-
case "url":
|
|
40
|
-
out += `.url(${messageSuffix(c.messageRaw)})`;
|
|
41
|
-
break;
|
|
42
|
-
case "uuid":
|
|
43
|
-
out += `.uuid(${messageSuffix(c.messageRaw)})`;
|
|
44
|
-
break;
|
|
45
|
-
case "regex":
|
|
46
|
-
out += `.regex(${c.pattern})`;
|
|
47
|
-
break;
|
|
48
|
-
case "min":
|
|
49
|
-
out += `.min(${c.value})`;
|
|
50
|
-
break;
|
|
51
|
-
case "max":
|
|
52
|
-
out += `.max(${c.value})`;
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
function render(node, ctx) {
|
|
59
|
-
switch (node.kind) {
|
|
60
|
-
case "string":
|
|
61
|
-
return `z.string()${renderStringChecks(node.checks)}`;
|
|
62
|
-
case "number": {
|
|
63
|
-
let out = "z.number()";
|
|
64
|
-
for (const c of node.checks) {
|
|
65
|
-
if (c.check === "int") out += ".int()";
|
|
66
|
-
else if (c.check === "positive") out += ".positive()";
|
|
67
|
-
else if (c.check === "negative") out += ".negative()";
|
|
68
|
-
else if (c.check === "min") out += `.min(${c.value})`;
|
|
69
|
-
else if (c.check === "max") out += `.max(${c.value})`;
|
|
70
|
-
}
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
case "boolean":
|
|
74
|
-
return "z.boolean()";
|
|
75
|
-
case "date":
|
|
76
|
-
return "z.coerce.date()";
|
|
77
|
-
case "unknown":
|
|
78
|
-
return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
|
|
79
|
-
case "instanceof":
|
|
80
|
-
return `z.instanceof(${node.ctor})`;
|
|
81
|
-
case "enum":
|
|
82
|
-
return `z.enum([${node.literals.join(", ")}])`;
|
|
83
|
-
case "literal":
|
|
84
|
-
return `z.literal(${node.raw})`;
|
|
85
|
-
case "union":
|
|
86
|
-
return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
|
|
87
|
-
case "object": {
|
|
88
|
-
if (node.fields.length === 0) {
|
|
89
|
-
return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
|
|
90
|
-
}
|
|
91
|
-
const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
|
|
92
|
-
return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
|
|
93
|
-
}
|
|
94
|
-
case "array":
|
|
95
|
-
return `z.array(${render(node.element, ctx)})`;
|
|
96
|
-
case "optional":
|
|
97
|
-
return `${render(node.inner, ctx)}.optional()`;
|
|
98
|
-
case "ref":
|
|
99
|
-
return node.name;
|
|
100
|
-
case "lazyRef":
|
|
101
|
-
return `z.lazy(() => ${node.name})`;
|
|
102
|
-
case "annotated": {
|
|
103
|
-
const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
|
|
104
|
-
return `${render(node.inner, ctx)} ${comments}`;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
var zodAdapter = {
|
|
109
|
-
name: "zod",
|
|
110
|
-
importStatements(usage) {
|
|
111
|
-
return usage.used ? ["import { z } from 'zod';"] : [];
|
|
112
|
-
},
|
|
113
|
-
render,
|
|
114
|
-
inferType(schemaConst) {
|
|
115
|
-
return `z.infer<typeof ${schemaConst}>`;
|
|
116
|
-
},
|
|
117
|
-
renderModule(mod) {
|
|
118
|
-
const ctx = { named: mod.named };
|
|
119
|
-
const namedNestedSchemas = /* @__PURE__ */ new Map();
|
|
120
|
-
for (const [name, node] of mod.named) {
|
|
121
|
-
namedNestedSchemas.set(name, render(node, ctx));
|
|
122
|
-
}
|
|
123
|
-
return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
25
|
// src/adapters/registry.ts
|
|
128
26
|
function resolveAdapter(option) {
|
|
129
27
|
if (typeof option !== "string") return option;
|
|
130
|
-
if (option === "zod") return zodAdapter;
|
|
131
28
|
const pkg = `@dudousxd/nestjs-codegen-${option}`;
|
|
132
29
|
const named = `${option}Adapter`;
|
|
133
30
|
throw new ConfigError(
|
|
@@ -180,8 +77,21 @@ If this is intentional, move the file inside your project directory.`
|
|
|
180
77
|
function resolveConfig(userConfig, cwd) {
|
|
181
78
|
return applyDefaults(userConfig, cwd ?? process.cwd());
|
|
182
79
|
}
|
|
80
|
+
function validateUserConfig(userConfig) {
|
|
81
|
+
if (userConfig.validation == null) {
|
|
82
|
+
throw new ConfigError(
|
|
83
|
+
"validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
|
|
87
|
+
throw new ConfigError(
|
|
88
|
+
"Config validation failed: `pages.glob` must be a string when `pages` is set"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
183
92
|
function applyDefaults(userConfig, cwd) {
|
|
184
|
-
|
|
93
|
+
validateUserConfig(userConfig);
|
|
94
|
+
const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
|
|
185
95
|
const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
|
|
186
96
|
let app = null;
|
|
187
97
|
if (userConfig.app) {
|
|
@@ -199,7 +109,8 @@ function applyDefaults(userConfig, cwd) {
|
|
|
199
109
|
}
|
|
200
110
|
return {
|
|
201
111
|
extensions: userConfig.extensions ?? [],
|
|
202
|
-
|
|
112
|
+
// Non-null: validateUserConfig() above throws when `validation` is absent.
|
|
113
|
+
validation: resolveAdapter(userConfig.validation),
|
|
203
114
|
pages: userConfig.pages ? {
|
|
204
115
|
glob: userConfig.pages.glob,
|
|
205
116
|
propsExport: userConfig.pages.propsExport ?? "ComponentProps",
|
|
@@ -253,18 +164,12 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
253
164
|
`Config file must have a default export. Did you forget \`export default defineConfig({...})\`? (${configPath})`
|
|
254
165
|
);
|
|
255
166
|
}
|
|
256
|
-
if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
|
|
257
|
-
throw new ConfigError(
|
|
258
|
-
`Config validation failed: \`pages.glob\` must be a string when \`pages\` is set (${configPath})`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
167
|
return applyDefaults(userConfig, resolvedCwd);
|
|
262
168
|
}
|
|
263
169
|
|
|
264
170
|
// src/generate.ts
|
|
265
171
|
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
266
|
-
import { dirname as dirname2, join as
|
|
267
|
-
import { Project as Project2 } from "ts-morph";
|
|
172
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
268
173
|
|
|
269
174
|
// src/discovery/pages.ts
|
|
270
175
|
import { readFile } from "fs/promises";
|
|
@@ -319,7 +224,8 @@ function extractPropsSource(source, exportName) {
|
|
|
319
224
|
}
|
|
320
225
|
|
|
321
226
|
// src/discovery/shared-props.ts
|
|
322
|
-
import {
|
|
227
|
+
import { join as join3 } from "path";
|
|
228
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
323
229
|
function discoverSharedProps(project, moduleEntry) {
|
|
324
230
|
try {
|
|
325
231
|
let sourceFile = project.getSourceFile(moduleEntry);
|
|
@@ -339,6 +245,31 @@ function discoverSharedProps(project, moduleEntry) {
|
|
|
339
245
|
return null;
|
|
340
246
|
}
|
|
341
247
|
}
|
|
248
|
+
function discoverSharedPropsFromConfig(config) {
|
|
249
|
+
if (!config.app?.moduleEntry) return null;
|
|
250
|
+
try {
|
|
251
|
+
const tsconfigPath = config.app.tsconfig ?? join3(config.codegen.cwd, "tsconfig.json");
|
|
252
|
+
let project;
|
|
253
|
+
try {
|
|
254
|
+
project = new Project({
|
|
255
|
+
tsConfigFilePath: tsconfigPath,
|
|
256
|
+
skipAddingFilesFromTsConfig: true,
|
|
257
|
+
skipLoadingLibFiles: true,
|
|
258
|
+
skipFileDependencyResolution: true
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
project = new Project({
|
|
262
|
+
skipAddingFilesFromTsConfig: true,
|
|
263
|
+
skipLoadingLibFiles: true,
|
|
264
|
+
skipFileDependencyResolution: true,
|
|
265
|
+
compilerOptions: { allowJs: true, strict: false }
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return discoverSharedProps(project, config.app.moduleEntry);
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
342
273
|
function findForRootCall(sourceFile) {
|
|
343
274
|
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
344
275
|
for (const call of callExpressions) {
|
|
@@ -358,9 +289,9 @@ function findForRootCall(sourceFile) {
|
|
|
358
289
|
function findShareInitializer(forRootCall) {
|
|
359
290
|
if (!Node.isCallExpression(forRootCall)) return null;
|
|
360
291
|
const args = forRootCall.getArguments();
|
|
361
|
-
const
|
|
362
|
-
if (!
|
|
363
|
-
for (const prop of
|
|
292
|
+
const firstArg2 = args[0];
|
|
293
|
+
if (!firstArg2 || !Node.isObjectLiteralExpression(firstArg2)) return null;
|
|
294
|
+
for (const prop of firstArg2.getProperties()) {
|
|
364
295
|
if (Node.isPropertyAssignment(prop) && prop.getName() === "share") {
|
|
365
296
|
return prop.getInitializer() ?? null;
|
|
366
297
|
}
|
|
@@ -459,9 +390,9 @@ function extractFromReturnTypeAnnotation(typeNode) {
|
|
|
459
390
|
const typeName = typeNode.getTypeName();
|
|
460
391
|
if (Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
|
|
461
392
|
const typeArgs = typeNode.getTypeArguments();
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
464
|
-
return extractFromReturnTypeAnnotation(
|
|
393
|
+
const firstArg2 = typeArgs[0];
|
|
394
|
+
if (firstArg2) {
|
|
395
|
+
return extractFromReturnTypeAnnotation(firstArg2);
|
|
465
396
|
}
|
|
466
397
|
return null;
|
|
467
398
|
}
|
|
@@ -567,25 +498,14 @@ function inferExpressionType(node) {
|
|
|
567
498
|
|
|
568
499
|
// src/emit/emit-api.ts
|
|
569
500
|
import { mkdir, writeFile } from "fs/promises";
|
|
570
|
-
import { isAbsolute as isAbsolute2, join as
|
|
501
|
+
import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
|
|
571
502
|
|
|
572
503
|
// src/extension/registry.ts
|
|
573
|
-
import { Project } from "ts-morph";
|
|
504
|
+
import { Project as Project2 } from "ts-morph";
|
|
574
505
|
function resolveApiSlots(extensions) {
|
|
575
|
-
let transport;
|
|
576
|
-
let transportOwner;
|
|
577
506
|
let layer;
|
|
578
507
|
let layerOwner;
|
|
579
508
|
for (const ext of extensions) {
|
|
580
|
-
if (ext.apiTransport) {
|
|
581
|
-
if (transport) {
|
|
582
|
-
throw new CodegenError(
|
|
583
|
-
`api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
transport = ext.apiTransport;
|
|
587
|
-
transportOwner = ext.name;
|
|
588
|
-
}
|
|
589
509
|
if (ext.apiClientLayer) {
|
|
590
510
|
if (layer) {
|
|
591
511
|
throw new CodegenError(
|
|
@@ -597,11 +517,22 @@ function resolveApiSlots(extensions) {
|
|
|
597
517
|
}
|
|
598
518
|
}
|
|
599
519
|
return {
|
|
600
|
-
...transport ? { transport } : {},
|
|
601
520
|
...layer ? { layer } : {}
|
|
602
521
|
};
|
|
603
522
|
}
|
|
604
523
|
var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
|
|
524
|
+
function mergeExclusive(target, incoming, {
|
|
525
|
+
owner,
|
|
526
|
+
describe
|
|
527
|
+
}) {
|
|
528
|
+
for (const [key, value] of incoming) {
|
|
529
|
+
const prev = target.get(key);
|
|
530
|
+
if (prev !== void 0) {
|
|
531
|
+
throw new CodegenError(describe(key, prev.owner, owner));
|
|
532
|
+
}
|
|
533
|
+
target.set(key, { value, owner });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
605
536
|
function createExtensionContext(config, getRoutes) {
|
|
606
537
|
let project;
|
|
607
538
|
return {
|
|
@@ -613,7 +544,7 @@ function createExtensionContext(config, getRoutes) {
|
|
|
613
544
|
},
|
|
614
545
|
project() {
|
|
615
546
|
if (!project) {
|
|
616
|
-
project = new
|
|
547
|
+
project = new Project2({
|
|
617
548
|
skipAddingFilesFromTsConfig: true,
|
|
618
549
|
skipLoadingLibFiles: true,
|
|
619
550
|
skipFileDependencyResolution: true,
|
|
@@ -646,29 +577,36 @@ async function collectEmittedFiles(extensions, ctx) {
|
|
|
646
577
|
`Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
|
|
647
578
|
);
|
|
648
579
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
owners.set(key, ext.name);
|
|
580
|
+
mergeExclusive(owners, [[key, file]], {
|
|
581
|
+
owner: ext.name,
|
|
582
|
+
describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
|
|
583
|
+
});
|
|
656
584
|
files.push(file);
|
|
657
585
|
}
|
|
658
586
|
}
|
|
659
587
|
return files;
|
|
660
588
|
}
|
|
661
589
|
|
|
590
|
+
// src/extension/types.ts
|
|
591
|
+
function requestShape(route) {
|
|
592
|
+
const cs = route.contract?.contractSource;
|
|
593
|
+
const isGet = route.method.toUpperCase() === "GET";
|
|
594
|
+
const isQuery = isGet || !!cs?.filterFields?.length;
|
|
595
|
+
const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
|
|
596
|
+
const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
|
|
597
|
+
return { isGet, isQuery, hasBody, hasQuery };
|
|
598
|
+
}
|
|
599
|
+
|
|
662
600
|
// src/emit/emit-api.ts
|
|
663
601
|
async function emitApi(routes, outDir, opts = {}) {
|
|
664
602
|
await mkdir(outDir, { recursive: true });
|
|
665
603
|
const content = buildApiFile(routes, outDir, opts);
|
|
666
|
-
await writeFile(
|
|
604
|
+
await writeFile(join4(outDir, "api.ts"), content, "utf8");
|
|
667
605
|
}
|
|
668
606
|
function splitName(name) {
|
|
669
607
|
return name.split(".");
|
|
670
608
|
}
|
|
671
|
-
function
|
|
609
|
+
function toObjectKey(segment) {
|
|
672
610
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
|
|
673
611
|
return segment;
|
|
674
612
|
}
|
|
@@ -775,7 +713,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
775
713
|
const pad = " ".repeat(indent);
|
|
776
714
|
const lines = [];
|
|
777
715
|
for (const [key, node] of tree) {
|
|
778
|
-
const objKey =
|
|
716
|
+
const objKey = toObjectKey(key);
|
|
779
717
|
if (node.kind === "leaf") {
|
|
780
718
|
const c = node;
|
|
781
719
|
const method = c.method.toUpperCase();
|
|
@@ -801,15 +739,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
801
739
|
return lines;
|
|
802
740
|
}
|
|
803
741
|
function buildRequestModel(c) {
|
|
804
|
-
const isGet = c.method.toUpperCase() === "GET";
|
|
805
742
|
const m = c.method.toLowerCase();
|
|
806
743
|
const flat = JSON.stringify(c.name);
|
|
807
744
|
const path = JSON.stringify(c.path);
|
|
808
745
|
const TA = buildRouterTypeAccess(c.name);
|
|
809
746
|
const withParams = hasPathParams(c.params);
|
|
810
|
-
const
|
|
811
|
-
const isQuery = isGet || !!c.contractSource.filterFields?.length;
|
|
812
|
-
const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
|
|
747
|
+
const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
|
|
813
748
|
const fields = [];
|
|
814
749
|
if (withParams) fields.push(`params: ${TA}['params']`);
|
|
815
750
|
if (hasQuery) fields.push(`query?: ${TA}['query']`);
|
|
@@ -831,7 +766,6 @@ function buildRequestModel(c) {
|
|
|
831
766
|
urlExpr,
|
|
832
767
|
optsExpr,
|
|
833
768
|
responseType: `${TA}['response']`,
|
|
834
|
-
bodyType: `${TA}['body']`,
|
|
835
769
|
queryKeyExpr: `[${flat}, input] as const`
|
|
836
770
|
};
|
|
837
771
|
}
|
|
@@ -879,7 +813,7 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
879
813
|
const pad = " ".repeat(indent);
|
|
880
814
|
const lines = [];
|
|
881
815
|
for (const [key, node] of tree) {
|
|
882
|
-
const objKey =
|
|
816
|
+
const objKey = toObjectKey(key);
|
|
883
817
|
if (node.kind === "branch") {
|
|
884
818
|
lines.push(`${pad}${objKey}: {`);
|
|
885
819
|
lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
|
|
@@ -887,30 +821,28 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
887
821
|
continue;
|
|
888
822
|
}
|
|
889
823
|
const req = buildRequestModel(node);
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
params: node.params,
|
|
895
|
-
contract: { contractSource: node.contractSource },
|
|
896
|
-
...node.controllerRef ? { controllerRef: node.controllerRef } : {}
|
|
824
|
+
const leaf = {
|
|
825
|
+
route: node.route,
|
|
826
|
+
request: req,
|
|
827
|
+
requestExpr: renderFetcherRequest(req)
|
|
897
828
|
};
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
829
|
+
const owned = /* @__PURE__ */ new Map();
|
|
830
|
+
if (p.layer) {
|
|
831
|
+
mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
|
|
832
|
+
owner: p.layer.name,
|
|
833
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
834
|
+
});
|
|
835
|
+
}
|
|
902
836
|
for (const ext of p.memberExts) {
|
|
903
837
|
const extra = ext.apiMembers?.(leaf, p.ctx);
|
|
904
838
|
if (!extra) continue;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
members[name] = value;
|
|
912
|
-
}
|
|
839
|
+
mergeExclusive(owned, Object.entries(extra), {
|
|
840
|
+
owner: ext.name,
|
|
841
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
842
|
+
});
|
|
913
843
|
}
|
|
844
|
+
const members = {};
|
|
845
|
+
for (const [name, { value }] of owned) members[name] = value;
|
|
914
846
|
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
|
|
915
847
|
}
|
|
916
848
|
return lines;
|
|
@@ -919,10 +851,82 @@ function buildRouterTypeAccess(name) {
|
|
|
919
851
|
const segments = splitName(name);
|
|
920
852
|
return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
|
|
921
853
|
}
|
|
854
|
+
var RESOLVER_HELPERS = [
|
|
855
|
+
// --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
|
|
856
|
+
"type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
|
|
857
|
+
" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
|
|
858
|
+
" : P extends keyof R ? R[P] : never;",
|
|
859
|
+
"",
|
|
860
|
+
// --- ResolveByName: resolve a field from a dot-path name ---
|
|
861
|
+
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
|
|
862
|
+
"",
|
|
863
|
+
// --- ResolveByPath: scan all leaves for matching method + url ---
|
|
864
|
+
// Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
|
|
865
|
+
"type _LeafValues<T> = T extends { method: string; url: string }",
|
|
866
|
+
" ? T",
|
|
867
|
+
" : T extends object ? _LeafValues<T[keyof T]> : never;",
|
|
868
|
+
"",
|
|
869
|
+
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
|
|
870
|
+
" ? L extends { method: M; url: U }",
|
|
871
|
+
" ? Field extends keyof L ? L[Field] : never",
|
|
872
|
+
" : never",
|
|
873
|
+
" : never;",
|
|
874
|
+
""
|
|
875
|
+
];
|
|
876
|
+
var ROUTE_NAMESPACE = [
|
|
877
|
+
"export namespace Route {",
|
|
878
|
+
' export type Response<K extends string> = ResolveByName<K, "response">;',
|
|
879
|
+
' export type Body<K extends string> = ResolveByName<K, "body">;',
|
|
880
|
+
' export type Query<K extends string> = ResolveByName<K, "query">;',
|
|
881
|
+
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
882
|
+
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
883
|
+
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
884
|
+
" export type Request<K extends string> = {",
|
|
885
|
+
" body: Body<K>;",
|
|
886
|
+
" query: Query<K>;",
|
|
887
|
+
" params: Params<K>;",
|
|
888
|
+
" };",
|
|
889
|
+
"}",
|
|
890
|
+
""
|
|
891
|
+
];
|
|
892
|
+
var PATH_NAMESPACE = [
|
|
893
|
+
"export namespace Path {",
|
|
894
|
+
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
895
|
+
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
|
|
896
|
+
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
|
|
897
|
+
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
898
|
+
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
899
|
+
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
900
|
+
"}",
|
|
901
|
+
""
|
|
902
|
+
];
|
|
903
|
+
var EMPTY_ROUTE_NAMESPACE = [
|
|
904
|
+
"export namespace Route {",
|
|
905
|
+
" export type Response<K extends string> = never;",
|
|
906
|
+
" export type Body<K extends string> = never;",
|
|
907
|
+
" export type Query<K extends string> = never;",
|
|
908
|
+
" export type Params<K extends string> = never;",
|
|
909
|
+
" export type Error<K extends string> = never;",
|
|
910
|
+
" export type FilterFields<K extends string> = never;",
|
|
911
|
+
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
912
|
+
"}",
|
|
913
|
+
""
|
|
914
|
+
];
|
|
915
|
+
var EMPTY_PATH_NAMESPACE = [
|
|
916
|
+
"export namespace Path {",
|
|
917
|
+
" export type Response<M extends string, U extends string> = never;",
|
|
918
|
+
" export type Body<M extends string, U extends string> = never;",
|
|
919
|
+
" export type Query<M extends string, U extends string> = never;",
|
|
920
|
+
" export type Params<M extends string, U extends string> = never;",
|
|
921
|
+
" export type Error<M extends string, U extends string> = never;",
|
|
922
|
+
" export type FilterFields<M extends string, U extends string> = never;",
|
|
923
|
+
"}",
|
|
924
|
+
""
|
|
925
|
+
];
|
|
922
926
|
function buildApiFile(routes, outDir, opts = {}) {
|
|
923
927
|
const fetcherImportPath = opts.fetcherImportPath;
|
|
924
928
|
const extensions = opts.extensions ?? [];
|
|
925
|
-
const {
|
|
929
|
+
const { layer } = resolveApiSlots(extensions);
|
|
926
930
|
const memberExts = extensions.filter((e) => e.apiMembers);
|
|
927
931
|
const headerExts = extensions.filter((e) => e.apiHeader);
|
|
928
932
|
const contracted = routes.filter((r) => r.contract);
|
|
@@ -967,7 +971,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
967
971
|
seenImports.add(imp);
|
|
968
972
|
extImports.push(imp);
|
|
969
973
|
};
|
|
970
|
-
for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
|
|
971
974
|
for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
|
|
972
975
|
for (const ext of headerExts) {
|
|
973
976
|
for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
|
|
@@ -1012,27 +1015,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1012
1015
|
lines.push("}");
|
|
1013
1016
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
1014
1017
|
lines.push("");
|
|
1015
|
-
lines.push(
|
|
1016
|
-
lines.push(
|
|
1017
|
-
lines.push(" export type Body<K extends string> = never;");
|
|
1018
|
-
lines.push(" export type Query<K extends string> = never;");
|
|
1019
|
-
lines.push(" export type Params<K extends string> = never;");
|
|
1020
|
-
lines.push(" export type Error<K extends string> = never;");
|
|
1021
|
-
lines.push(" export type FilterFields<K extends string> = never;");
|
|
1022
|
-
lines.push(
|
|
1023
|
-
" export type Request<K extends string> = { body: never; query: never; params: never };"
|
|
1024
|
-
);
|
|
1025
|
-
lines.push("}");
|
|
1026
|
-
lines.push("");
|
|
1027
|
-
lines.push("export namespace Path {");
|
|
1028
|
-
lines.push(" export type Response<M extends string, U extends string> = never;");
|
|
1029
|
-
lines.push(" export type Body<M extends string, U extends string> = never;");
|
|
1030
|
-
lines.push(" export type Query<M extends string, U extends string> = never;");
|
|
1031
|
-
lines.push(" export type Params<M extends string, U extends string> = never;");
|
|
1032
|
-
lines.push(" export type Error<M extends string, U extends string> = never;");
|
|
1033
|
-
lines.push(" export type FilterFields<M extends string, U extends string> = never;");
|
|
1034
|
-
lines.push("}");
|
|
1035
|
-
lines.push("");
|
|
1018
|
+
lines.push(...EMPTY_ROUTE_NAMESPACE);
|
|
1019
|
+
lines.push(...EMPTY_PATH_NAMESPACE);
|
|
1036
1020
|
return lines.join("\n");
|
|
1037
1021
|
}
|
|
1038
1022
|
const tree = /* @__PURE__ */ new Map();
|
|
@@ -1050,7 +1034,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1050
1034
|
path: r.path,
|
|
1051
1035
|
params: r.params,
|
|
1052
1036
|
controllerRef: r.controllerRef,
|
|
1053
|
-
contractSource: c.contractSource
|
|
1037
|
+
contractSource: c.contractSource,
|
|
1038
|
+
route: r
|
|
1054
1039
|
};
|
|
1055
1040
|
insertIntoTree(tree, segments, leaf, name);
|
|
1056
1041
|
}
|
|
@@ -1063,7 +1048,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1063
1048
|
lines.push(" return {");
|
|
1064
1049
|
lines.push(
|
|
1065
1050
|
...emitApiObjectBlock(tree, 4, {
|
|
1066
|
-
...transport ? { transport } : {},
|
|
1067
1051
|
...layer ? { layer } : {},
|
|
1068
1052
|
memberExts,
|
|
1069
1053
|
ctx
|
|
@@ -1074,61 +1058,9 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1074
1058
|
lines.push("");
|
|
1075
1059
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
1076
1060
|
lines.push("");
|
|
1077
|
-
lines.push(
|
|
1078
|
-
lines.push(
|
|
1079
|
-
lines.push(
|
|
1080
|
-
lines.push("");
|
|
1081
|
-
lines.push(
|
|
1082
|
-
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
|
|
1083
|
-
);
|
|
1084
|
-
lines.push("");
|
|
1085
|
-
lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
|
|
1086
|
-
lines.push(" ? T");
|
|
1087
|
-
lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
|
|
1088
|
-
lines.push("");
|
|
1089
|
-
lines.push(
|
|
1090
|
-
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
|
|
1091
|
-
);
|
|
1092
|
-
lines.push(" ? L extends { method: M; url: U }");
|
|
1093
|
-
lines.push(" ? Field extends keyof L ? L[Field] : never");
|
|
1094
|
-
lines.push(" : never");
|
|
1095
|
-
lines.push(" : never;");
|
|
1096
|
-
lines.push("");
|
|
1097
|
-
lines.push("export namespace Route {");
|
|
1098
|
-
lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
|
|
1099
|
-
lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
|
|
1100
|
-
lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
|
|
1101
|
-
lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
|
|
1102
|
-
lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
|
|
1103
|
-
lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
|
|
1104
|
-
lines.push(" export type Request<K extends string> = {");
|
|
1105
|
-
lines.push(" body: Body<K>;");
|
|
1106
|
-
lines.push(" query: Query<K>;");
|
|
1107
|
-
lines.push(" params: Params<K>;");
|
|
1108
|
-
lines.push(" };");
|
|
1109
|
-
lines.push("}");
|
|
1110
|
-
lines.push("");
|
|
1111
|
-
lines.push("export namespace Path {");
|
|
1112
|
-
lines.push(
|
|
1113
|
-
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
|
|
1114
|
-
);
|
|
1115
|
-
lines.push(
|
|
1116
|
-
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
|
|
1117
|
-
);
|
|
1118
|
-
lines.push(
|
|
1119
|
-
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
|
|
1120
|
-
);
|
|
1121
|
-
lines.push(
|
|
1122
|
-
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
|
|
1123
|
-
);
|
|
1124
|
-
lines.push(
|
|
1125
|
-
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
|
|
1126
|
-
);
|
|
1127
|
-
lines.push(
|
|
1128
|
-
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
|
|
1129
|
-
);
|
|
1130
|
-
lines.push("}");
|
|
1131
|
-
lines.push("");
|
|
1061
|
+
lines.push(...RESOLVER_HELPERS);
|
|
1062
|
+
lines.push(...ROUTE_NAMESPACE);
|
|
1063
|
+
lines.push(...PATH_NAMESPACE);
|
|
1132
1064
|
for (const ext of headerExts) {
|
|
1133
1065
|
const statements = ext.apiHeader?.(ctx)?.statements;
|
|
1134
1066
|
if (statements?.length) {
|
|
@@ -1140,7 +1072,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1140
1072
|
|
|
1141
1073
|
// src/emit/emit-cache.ts
|
|
1142
1074
|
import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
1143
|
-
import { join as
|
|
1075
|
+
import { join as join5 } from "path";
|
|
1144
1076
|
async function emitCache(pages, outDir) {
|
|
1145
1077
|
await mkdir2(outDir, { recursive: true });
|
|
1146
1078
|
const entries = await Promise.all(
|
|
@@ -1154,95 +1086,21 @@ async function emitCache(pages, outDir) {
|
|
|
1154
1086
|
})
|
|
1155
1087
|
);
|
|
1156
1088
|
const cache = { pages: entries };
|
|
1157
|
-
await writeFile2(
|
|
1089
|
+
await writeFile2(join5(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
|
|
1158
1090
|
`, "utf8");
|
|
1159
1091
|
}
|
|
1160
1092
|
|
|
1161
1093
|
// src/emit/emit-forms.ts
|
|
1162
1094
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1163
|
-
import { join as
|
|
1095
|
+
import { join as join6, relative as relative4 } from "path";
|
|
1164
1096
|
async function emitForms(routes, outDir, config, adapter) {
|
|
1165
1097
|
if (config && config.enabled === false) return false;
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (content2 === null) return false;
|
|
1169
|
-
await mkdir3(outDir, { recursive: true });
|
|
1170
|
-
await writeFile3(join5(outDir, "forms.ts"), content2, "utf8");
|
|
1171
|
-
return true;
|
|
1172
|
-
}
|
|
1173
|
-
const entries = collectFormEntries(routes);
|
|
1174
|
-
if (entries.length === 0) return false;
|
|
1098
|
+
const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
|
|
1099
|
+
if (content === null) return false;
|
|
1175
1100
|
await mkdir3(outDir, { recursive: true });
|
|
1176
|
-
|
|
1177
|
-
await writeFile3(join5(outDir, "forms.ts"), content, "utf8");
|
|
1101
|
+
await writeFile3(join6(outDir, "forms.ts"), content, "utf8");
|
|
1178
1102
|
return true;
|
|
1179
1103
|
}
|
|
1180
|
-
function buildFormsFileWithAdapter(routes, adapter) {
|
|
1181
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1182
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1183
|
-
for (const route of sorted) {
|
|
1184
|
-
const cs = route.contract.contractSource;
|
|
1185
|
-
if (!cs.bodySchema && !cs.querySchema) continue;
|
|
1186
|
-
methodNameCounts.set(
|
|
1187
|
-
deriveBaseName(route.name).method,
|
|
1188
|
-
(methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
const named = /* @__PURE__ */ new Map();
|
|
1192
|
-
const decls = [];
|
|
1193
|
-
const mapEntries = [];
|
|
1194
|
-
let used = false;
|
|
1195
|
-
for (const route of sorted) {
|
|
1196
|
-
const cs = route.contract.contractSource;
|
|
1197
|
-
const { method, full } = deriveBaseName(route.name);
|
|
1198
|
-
const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
|
|
1199
|
-
const block = [];
|
|
1200
|
-
if (cs.bodyZodText && !cs.bodySchema) {
|
|
1201
|
-
block.push(
|
|
1202
|
-
`// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
let bodyConst;
|
|
1206
|
-
if (cs.bodySchema) {
|
|
1207
|
-
used = true;
|
|
1208
|
-
const r = adapter.renderModule(cs.bodySchema);
|
|
1209
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
1210
|
-
bodyConst = `${base}BodySchema`;
|
|
1211
|
-
block.push(`export const ${bodyConst} = ${r.schemaText};`);
|
|
1212
|
-
block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
|
|
1213
|
-
}
|
|
1214
|
-
if (cs.querySchema) {
|
|
1215
|
-
used = true;
|
|
1216
|
-
const r = adapter.renderModule(cs.querySchema);
|
|
1217
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
1218
|
-
const queryConst = `${base}QuerySchema`;
|
|
1219
|
-
block.push(`export const ${queryConst} = ${r.schemaText};`);
|
|
1220
|
-
block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
|
|
1221
|
-
}
|
|
1222
|
-
if (block.length === 0) continue;
|
|
1223
|
-
decls.push(`// ${route.name}`, ...block, "");
|
|
1224
|
-
if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
|
|
1225
|
-
}
|
|
1226
|
-
if (!used) return null;
|
|
1227
|
-
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
1228
|
-
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
1229
|
-
lines.push("");
|
|
1230
|
-
if (named.size > 0) {
|
|
1231
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1232
|
-
for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
|
|
1233
|
-
lines.push("");
|
|
1234
|
-
}
|
|
1235
|
-
lines.push(...decls);
|
|
1236
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
1237
|
-
lines.push("export const formSchemas = {");
|
|
1238
|
-
lines.push(...mapEntries);
|
|
1239
|
-
lines.push("} as const;");
|
|
1240
|
-
lines.push("");
|
|
1241
|
-
return lines.join("\n");
|
|
1242
|
-
}
|
|
1243
|
-
function hasSchema(src) {
|
|
1244
|
-
return !!src && (src.ref !== null || src.text !== null);
|
|
1245
|
-
}
|
|
1246
1104
|
function pascal(segment) {
|
|
1247
1105
|
return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1248
1106
|
}
|
|
@@ -1252,37 +1110,6 @@ function deriveBaseName(routeName) {
|
|
|
1252
1110
|
const full = segments.map(pascal).join("");
|
|
1253
1111
|
return { method, full };
|
|
1254
1112
|
}
|
|
1255
|
-
function collectFormEntries(routes) {
|
|
1256
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1257
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1258
|
-
const candidates = [];
|
|
1259
|
-
for (const route of sorted) {
|
|
1260
|
-
const cs = route.contract.contractSource;
|
|
1261
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
1262
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
1263
|
-
if (!hasSchema(body) && !hasSchema(query)) continue;
|
|
1264
|
-
const { method, full } = deriveBaseName(route.name);
|
|
1265
|
-
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
1266
|
-
candidates.push({ route, method, full });
|
|
1267
|
-
}
|
|
1268
|
-
const entries = [];
|
|
1269
|
-
for (const { route, method, full } of candidates) {
|
|
1270
|
-
const cs = route.contract.contractSource;
|
|
1271
|
-
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
1272
|
-
const baseName = collision ? full : method;
|
|
1273
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
1274
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
1275
|
-
entries.push({
|
|
1276
|
-
routeName: route.name,
|
|
1277
|
-
baseName,
|
|
1278
|
-
body: hasSchema(body) ? body : void 0,
|
|
1279
|
-
query: hasSchema(query) ? query : void 0,
|
|
1280
|
-
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
1281
|
-
warnings: cs.formWarnings ?? []
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
return entries;
|
|
1285
|
-
}
|
|
1286
1113
|
function relImport(outDir, filePath) {
|
|
1287
1114
|
let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
|
|
1288
1115
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
@@ -1291,85 +1118,8 @@ function relImport(outDir, filePath) {
|
|
|
1291
1118
|
function refRootIdentifier(refName) {
|
|
1292
1119
|
return refName.split(".")[0] ?? refName;
|
|
1293
1120
|
}
|
|
1294
|
-
function
|
|
1295
|
-
|
|
1296
|
-
const lines = [
|
|
1297
|
-
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1298
|
-
`import { z } from '${zodImport}';`
|
|
1299
|
-
];
|
|
1300
|
-
const importsByFile = /* @__PURE__ */ new Map();
|
|
1301
|
-
const refAlias = /* @__PURE__ */ new Map();
|
|
1302
|
-
for (const entry of entries) {
|
|
1303
|
-
for (const src of [entry.body, entry.query]) {
|
|
1304
|
-
if (src?.ref && !src.text) {
|
|
1305
|
-
const root = refRootIdentifier(src.ref.name);
|
|
1306
|
-
const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
|
|
1307
|
-
set.add(root);
|
|
1308
|
-
importsByFile.set(src.ref.filePath, set);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
if (importsByFile.size > 0) {
|
|
1313
|
-
const emitted = /* @__PURE__ */ new Set();
|
|
1314
|
-
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
1315
|
-
const relPath = relImport(outDir, filePath);
|
|
1316
|
-
const specifiers = [];
|
|
1317
|
-
for (const root of [...roots].sort()) {
|
|
1318
|
-
if (emitted.has(root)) {
|
|
1319
|
-
const alias = `${root}_${emitted.size}`;
|
|
1320
|
-
specifiers.push(`${root} as ${alias}`);
|
|
1321
|
-
emitted.add(alias);
|
|
1322
|
-
refAlias.set(`${filePath}\0${root}`, alias);
|
|
1323
|
-
} else {
|
|
1324
|
-
specifiers.push(root);
|
|
1325
|
-
emitted.add(root);
|
|
1326
|
-
refAlias.set(`${filePath}\0${root}`, root);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
lines.push("");
|
|
1333
|
-
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
1334
|
-
if (globalSchemas.size > 0) {
|
|
1335
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1336
|
-
for (const [name, text] of globalSchemas) {
|
|
1337
|
-
lines.push(`const ${name} = ${text};`);
|
|
1338
|
-
}
|
|
1339
|
-
lines.push("");
|
|
1340
|
-
}
|
|
1341
|
-
const mapEntries = [];
|
|
1342
|
-
for (const entry of entries) {
|
|
1343
|
-
lines.push(`// ${entry.routeName}`);
|
|
1344
|
-
if (entry.warnings && entry.warnings.length > 0) {
|
|
1345
|
-
for (const w of entry.warnings) {
|
|
1346
|
-
lines.push(`// warning: ${w}`);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
const rename = renamesByEntry.get(entry) ?? null;
|
|
1350
|
-
if (entry.body) {
|
|
1351
|
-
const schemaName = `${entry.baseName}BodySchema`;
|
|
1352
|
-
const typeName = `${entry.baseName}Body`;
|
|
1353
|
-
const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
|
|
1354
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
1355
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
1356
|
-
mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
|
|
1357
|
-
}
|
|
1358
|
-
if (entry.query) {
|
|
1359
|
-
const schemaName = `${entry.baseName}QuerySchema`;
|
|
1360
|
-
const typeName = `${entry.baseName}Query`;
|
|
1361
|
-
const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
|
|
1362
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
1363
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
1364
|
-
}
|
|
1365
|
-
lines.push("");
|
|
1366
|
-
}
|
|
1367
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
1368
|
-
lines.push("export const formSchemas = {");
|
|
1369
|
-
lines.push(...mapEntries);
|
|
1370
|
-
lines.push("} as const;");
|
|
1371
|
-
lines.push("");
|
|
1372
|
-
return lines.join("\n");
|
|
1121
|
+
function hasSource(src) {
|
|
1122
|
+
return !!(src.schema || src.zodText || src.zodRef);
|
|
1373
1123
|
}
|
|
1374
1124
|
function applyRenames(text, renames) {
|
|
1375
1125
|
if (!renames || renames.size === 0) return text;
|
|
@@ -1435,42 +1185,194 @@ function planNestedSchemas(entries) {
|
|
|
1435
1185
|
}
|
|
1436
1186
|
return { globalSchemas, renamesByEntry };
|
|
1437
1187
|
}
|
|
1438
|
-
function
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1188
|
+
function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
|
|
1189
|
+
const acceptsRawZod = adapter.acceptsRawZodSource === true;
|
|
1190
|
+
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1191
|
+
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1192
|
+
const candidates = [];
|
|
1193
|
+
for (const route of sorted) {
|
|
1194
|
+
const cs = route.contract.contractSource;
|
|
1195
|
+
const body = {
|
|
1196
|
+
schema: cs.bodySchema ?? null,
|
|
1197
|
+
zodText: cs.bodyZodText ?? null,
|
|
1198
|
+
zodRef: cs.bodyZodRef ?? null
|
|
1199
|
+
};
|
|
1200
|
+
const query = {
|
|
1201
|
+
schema: cs.querySchema ?? null,
|
|
1202
|
+
zodText: cs.queryZodText ?? null,
|
|
1203
|
+
zodRef: cs.queryZodRef ?? null
|
|
1204
|
+
};
|
|
1205
|
+
if (!hasSource(body) && !hasSource(query)) continue;
|
|
1206
|
+
const { method, full } = deriveBaseName(route.name);
|
|
1207
|
+
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
1208
|
+
candidates.push({
|
|
1209
|
+
routeName: route.name,
|
|
1210
|
+
baseName: full,
|
|
1211
|
+
// resolved below
|
|
1212
|
+
body: hasSource(body) ? body : void 0,
|
|
1213
|
+
query: hasSource(query) ? query : void 0,
|
|
1214
|
+
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
1215
|
+
warnings: cs.formWarnings ?? []
|
|
1216
|
+
});
|
|
1445
1217
|
}
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1218
|
+
const entries = candidates.map((c) => {
|
|
1219
|
+
const { method, full } = deriveBaseName(c.routeName);
|
|
1220
|
+
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
1221
|
+
return { ...c, baseName: collision ? full : method };
|
|
1222
|
+
});
|
|
1223
|
+
if (entries.length === 0) return null;
|
|
1224
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
1225
|
+
const refAlias = /* @__PURE__ */ new Map();
|
|
1226
|
+
for (const entry of entries) {
|
|
1227
|
+
for (const src of [entry.body, entry.query]) {
|
|
1228
|
+
if (src?.zodRef && !src.zodText && !src.schema) {
|
|
1229
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
1230
|
+
const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
|
|
1231
|
+
set.add(root);
|
|
1232
|
+
importsByFile.set(src.zodRef.filePath, set);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1457
1235
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1236
|
+
const importLines = [];
|
|
1237
|
+
if (importsByFile.size > 0) {
|
|
1238
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1239
|
+
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
1240
|
+
const relPath = relImport(outDir, filePath);
|
|
1241
|
+
const specifiers = [];
|
|
1242
|
+
for (const root of [...roots].sort()) {
|
|
1243
|
+
if (emitted.has(root)) {
|
|
1244
|
+
const alias = `${root}_${emitted.size}`;
|
|
1245
|
+
specifiers.push(`${root} as ${alias}`);
|
|
1246
|
+
emitted.add(alias);
|
|
1247
|
+
refAlias.set(`${filePath}\0${root}`, alias);
|
|
1248
|
+
} else {
|
|
1249
|
+
specifiers.push(root);
|
|
1250
|
+
emitted.add(root);
|
|
1251
|
+
refAlias.set(`${filePath}\0${root}`, root);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
1255
|
+
}
|
|
1460
1256
|
}
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1257
|
+
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
1258
|
+
const irNamed = /* @__PURE__ */ new Map();
|
|
1259
|
+
const decls = [];
|
|
1260
|
+
const mapEntries = [];
|
|
1261
|
+
let used = false;
|
|
1262
|
+
const renderSource = (src, rename) => {
|
|
1263
|
+
if (src.schema) {
|
|
1264
|
+
const r = adapter.renderModule(src.schema);
|
|
1265
|
+
for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
|
|
1266
|
+
return { text: r.schemaText };
|
|
1267
|
+
}
|
|
1268
|
+
if (src.zodText) {
|
|
1269
|
+
if (!acceptsRawZod) {
|
|
1270
|
+
return {
|
|
1271
|
+
text: "",
|
|
1272
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
return { text: applyRenames(src.zodText, rename) };
|
|
1276
|
+
}
|
|
1277
|
+
if (src.zodRef) {
|
|
1278
|
+
if (!acceptsRawZod) {
|
|
1279
|
+
return {
|
|
1280
|
+
text: "",
|
|
1281
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
1285
|
+
const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
|
|
1286
|
+
const member = src.zodRef.name.slice(root.length);
|
|
1287
|
+
return { text: `${alias}${member}` };
|
|
1288
|
+
}
|
|
1289
|
+
return null;
|
|
1290
|
+
};
|
|
1291
|
+
for (const entry of entries) {
|
|
1292
|
+
const block = [];
|
|
1293
|
+
const rename = renamesByEntry.get(entry) ?? null;
|
|
1294
|
+
let bodyConst;
|
|
1295
|
+
if (entry.warnings && entry.warnings.length > 0) {
|
|
1296
|
+
for (const w of entry.warnings) block.push(`// warning: ${w}`);
|
|
1297
|
+
}
|
|
1298
|
+
if (entry.body) {
|
|
1299
|
+
const rendered = renderSource(entry.body, rename);
|
|
1300
|
+
if (rendered?.warn) {
|
|
1301
|
+
block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
|
|
1302
|
+
} else if (rendered) {
|
|
1303
|
+
used = true;
|
|
1304
|
+
bodyConst = `${entry.baseName}BodySchema`;
|
|
1305
|
+
block.push(`export const ${bodyConst} = ${rendered.text};`);
|
|
1306
|
+
block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (entry.query) {
|
|
1310
|
+
const rendered = renderSource(entry.query, rename);
|
|
1311
|
+
if (rendered?.warn) {
|
|
1312
|
+
block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
|
|
1313
|
+
} else if (rendered) {
|
|
1314
|
+
used = true;
|
|
1315
|
+
const queryConst = `${entry.baseName}QuerySchema`;
|
|
1316
|
+
block.push(`export const ${queryConst} = ${rendered.text};`);
|
|
1317
|
+
block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (block.length === 0) continue;
|
|
1321
|
+
decls.push(`// ${entry.routeName}`, ...block, "");
|
|
1322
|
+
if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
|
|
1323
|
+
}
|
|
1324
|
+
if (!used) return null;
|
|
1325
|
+
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
1326
|
+
if (acceptsRawZod) {
|
|
1327
|
+
const zodImport = config?.zodImport ?? "zod";
|
|
1328
|
+
lines.push(`import { z } from '${zodImport}';`);
|
|
1329
|
+
} else {
|
|
1330
|
+
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
1331
|
+
}
|
|
1332
|
+
lines.push(...importLines);
|
|
1333
|
+
lines.push("");
|
|
1334
|
+
const allNested = /* @__PURE__ */ new Map();
|
|
1335
|
+
for (const [n, t] of globalSchemas) allNested.set(n, t);
|
|
1336
|
+
for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
|
|
1337
|
+
if (allNested.size > 0) {
|
|
1338
|
+
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1339
|
+
for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
|
|
1340
|
+
lines.push("");
|
|
1341
|
+
}
|
|
1342
|
+
lines.push(...decls);
|
|
1343
|
+
lines.push("/** Route name \u2192 body schema map. */");
|
|
1344
|
+
lines.push("export const formSchemas = {");
|
|
1345
|
+
lines.push(...mapEntries);
|
|
1346
|
+
lines.push("} as const;");
|
|
1347
|
+
lines.push("");
|
|
1348
|
+
return lines.join("\n");
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/emit/emit-index.ts
|
|
1352
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1353
|
+
import { join as join7 } from "path";
|
|
1354
|
+
async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
1355
|
+
await mkdir4(outDir, { recursive: true });
|
|
1356
|
+
const exports = ["export * from './pages.js';", "export * from './routes.js';"];
|
|
1357
|
+
if (hasContracts) {
|
|
1358
|
+
exports.push("export * from './api.js';");
|
|
1359
|
+
}
|
|
1360
|
+
if (hasForms) {
|
|
1361
|
+
exports.push("export * from './forms.js';");
|
|
1362
|
+
}
|
|
1363
|
+
const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
|
|
1364
|
+
"\n"
|
|
1365
|
+
);
|
|
1366
|
+
await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/emit/emit-pages.ts
|
|
1370
|
+
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1371
|
+
import { join as join8, relative as relative5 } from "path";
|
|
1372
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
1373
|
+
await mkdir5(outDir, { recursive: true });
|
|
1374
|
+
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
1375
|
+
const augBody = pages.map((p) => {
|
|
1474
1376
|
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
1475
1377
|
const valueType = buildAugmentationType(p, outDir);
|
|
1476
1378
|
return ` ${key}: ${valueType};`;
|
|
@@ -1487,7 +1389,7 @@ ${augBody}
|
|
|
1487
1389
|
}
|
|
1488
1390
|
${sharedPropsBlock}}
|
|
1489
1391
|
`;
|
|
1490
|
-
await writeFile5(
|
|
1392
|
+
await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
|
|
1491
1393
|
}
|
|
1492
1394
|
function buildSharedPropsBlock(sharedProps) {
|
|
1493
1395
|
if (!sharedProps) return "";
|
|
@@ -1518,11 +1420,11 @@ function needsQuotes(name) {
|
|
|
1518
1420
|
|
|
1519
1421
|
// src/emit/emit-routes.ts
|
|
1520
1422
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1521
|
-
import { join as
|
|
1423
|
+
import { join as join9 } from "path";
|
|
1522
1424
|
async function emitRoutes(routes, outDir) {
|
|
1523
1425
|
await mkdir6(outDir, { recursive: true });
|
|
1524
1426
|
const content = buildRoutesFile(routes);
|
|
1525
|
-
await writeFile6(
|
|
1427
|
+
await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
|
|
1526
1428
|
}
|
|
1527
1429
|
function buildRoutesFile(routes) {
|
|
1528
1430
|
if (routes.length === 0) {
|
|
@@ -1650,30 +1552,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
1650
1552
|
propsExport: pagesConfig.propsExport,
|
|
1651
1553
|
componentNameStrategy: pagesConfig.componentNameStrategy
|
|
1652
1554
|
});
|
|
1653
|
-
|
|
1654
|
-
if (config.app?.moduleEntry) {
|
|
1655
|
-
try {
|
|
1656
|
-
const tsconfigPath = config.app.tsconfig ?? join9(config.codegen.cwd, "tsconfig.json");
|
|
1657
|
-
let project;
|
|
1658
|
-
try {
|
|
1659
|
-
project = new Project2({
|
|
1660
|
-
tsConfigFilePath: tsconfigPath,
|
|
1661
|
-
skipAddingFilesFromTsConfig: true,
|
|
1662
|
-
skipLoadingLibFiles: true,
|
|
1663
|
-
skipFileDependencyResolution: true
|
|
1664
|
-
});
|
|
1665
|
-
} catch {
|
|
1666
|
-
project = new Project2({
|
|
1667
|
-
skipAddingFilesFromTsConfig: true,
|
|
1668
|
-
skipLoadingLibFiles: true,
|
|
1669
|
-
skipFileDependencyResolution: true,
|
|
1670
|
-
compilerOptions: { allowJs: true, strict: false }
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
sharedProps = discoverSharedProps(project, config.app.moduleEntry);
|
|
1674
|
-
} catch {
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1555
|
+
const sharedProps = discoverSharedPropsFromConfig(config);
|
|
1677
1556
|
await emitPages(pages, config.codegen.outDir, {
|
|
1678
1557
|
propsExport: pagesConfig.propsExport,
|
|
1679
1558
|
sharedProps
|
|
@@ -1697,7 +1576,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
1697
1576
|
if (extensions.length > 0) {
|
|
1698
1577
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1699
1578
|
for (const file of extraFiles) {
|
|
1700
|
-
const dest =
|
|
1579
|
+
const dest = join10(config.codegen.outDir, file.path);
|
|
1701
1580
|
await mkdir7(dirname2(dest), { recursive: true });
|
|
1702
1581
|
await writeFile7(dest, file.contents, "utf8");
|
|
1703
1582
|
}
|
|
@@ -1706,15 +1585,20 @@ async function generate(config, inputRoutes = []) {
|
|
|
1706
1585
|
|
|
1707
1586
|
// src/watch/watcher.ts
|
|
1708
1587
|
import { readFile as readFile3 } from "fs/promises";
|
|
1709
|
-
import { join as
|
|
1588
|
+
import { join as join13 } from "path";
|
|
1710
1589
|
import chokidar from "chokidar";
|
|
1711
1590
|
|
|
1712
1591
|
// src/discovery/contracts-fast.ts
|
|
1713
|
-
import { join as
|
|
1592
|
+
import { join as join11, resolve as resolve3 } from "path";
|
|
1714
1593
|
import fg2 from "fast-glob";
|
|
1715
1594
|
import {
|
|
1716
|
-
Node as
|
|
1717
|
-
Project as Project3
|
|
1595
|
+
Node as Node8,
|
|
1596
|
+
Project as Project3
|
|
1597
|
+
} from "ts-morph";
|
|
1598
|
+
|
|
1599
|
+
// src/discovery/dto-type-resolver.ts
|
|
1600
|
+
import {
|
|
1601
|
+
Node as Node6,
|
|
1718
1602
|
SyntaxKind as SyntaxKind3
|
|
1719
1603
|
} from "ts-morph";
|
|
1720
1604
|
|
|
@@ -1729,20 +1613,13 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
|
1729
1613
|
import {
|
|
1730
1614
|
Node as Node2
|
|
1731
1615
|
} from "ts-morph";
|
|
1732
|
-
var
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
return prev;
|
|
1737
|
-
}
|
|
1738
|
-
function restoreDiscoveryContext(ctx) {
|
|
1739
|
-
_ctx = ctx;
|
|
1616
|
+
var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
|
|
1617
|
+
var _ctxByProject = /* @__PURE__ */ new WeakMap();
|
|
1618
|
+
function setDiscoveryContext(project, ctx) {
|
|
1619
|
+
_ctxByProject.set(project, ctx);
|
|
1740
1620
|
}
|
|
1741
|
-
function
|
|
1742
|
-
return
|
|
1743
|
-
}
|
|
1744
|
-
function _tsconfigPaths() {
|
|
1745
|
-
return _ctx.tsconfigPaths;
|
|
1621
|
+
function _ctxFor(project) {
|
|
1622
|
+
return _ctxByProject.get(project) ?? _EMPTY_CTX;
|
|
1746
1623
|
}
|
|
1747
1624
|
var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
|
|
1748
1625
|
function dbg(...args) {
|
|
@@ -1784,7 +1661,7 @@ function findTypeInFile(name, file) {
|
|
|
1784
1661
|
}
|
|
1785
1662
|
return null;
|
|
1786
1663
|
}
|
|
1787
|
-
function resolveModuleSpecifier(moduleSpecifier, sourceFile,
|
|
1664
|
+
function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
1788
1665
|
if (moduleSpecifier.startsWith(".")) {
|
|
1789
1666
|
const dir = dirname3(sourceFile.getFilePath());
|
|
1790
1667
|
const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
|
|
@@ -1794,8 +1671,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
|
|
|
1794
1671
|
resolve2(dir, moduleSpecifier, "index.ts")
|
|
1795
1672
|
];
|
|
1796
1673
|
}
|
|
1797
|
-
const
|
|
1798
|
-
const
|
|
1674
|
+
const ctx = _ctxFor(project);
|
|
1675
|
+
const baseUrl = ctx.projectRoot;
|
|
1676
|
+
const tsconfigPaths = ctx.tsconfigPaths;
|
|
1799
1677
|
dbg(
|
|
1800
1678
|
"resolveModuleSpecifier",
|
|
1801
1679
|
moduleSpecifier,
|
|
@@ -1941,7 +1819,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
1941
1819
|
if (!namedImport) continue;
|
|
1942
1820
|
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
1943
1821
|
if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
1944
|
-
const tsconfigPaths =
|
|
1822
|
+
const tsconfigPaths = _ctxFor(project).tsconfigPaths;
|
|
1945
1823
|
const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
|
|
1946
1824
|
const prefix = p.replace("*", "");
|
|
1947
1825
|
return moduleSpecifier.startsWith(prefix);
|
|
@@ -2294,465 +2172,135 @@ function inSchemaFromDecorator(decorator) {
|
|
|
2294
2172
|
return null;
|
|
2295
2173
|
}
|
|
2296
2174
|
|
|
2297
|
-
// src/discovery/
|
|
2175
|
+
// src/discovery/filter-for.ts
|
|
2298
2176
|
import {
|
|
2299
|
-
Node as
|
|
2177
|
+
Node as Node5
|
|
2300
2178
|
} from "ts-morph";
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
"
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
"IsOptional",
|
|
2321
|
-
"IsNotEmpty",
|
|
2322
|
-
"IsArray",
|
|
2323
|
-
"ValidateNested",
|
|
2324
|
-
"Type",
|
|
2325
|
-
"IsObject",
|
|
2326
|
-
"Allow",
|
|
2327
|
-
"IsDefined"
|
|
2328
|
-
]);
|
|
2329
|
-
function extractZodFromDto(classDecl, sourceFile, project) {
|
|
2330
|
-
const ctx = {
|
|
2331
|
-
sourceFile,
|
|
2332
|
-
project,
|
|
2333
|
-
namedNestedSchemas: /* @__PURE__ */ new Map(),
|
|
2334
|
-
warnings: [],
|
|
2335
|
-
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
2336
|
-
emittedClasses: /* @__PURE__ */ new Map(),
|
|
2337
|
-
visiting: /* @__PURE__ */ new Set(),
|
|
2338
|
-
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
2339
|
-
depth: 0
|
|
2340
|
-
};
|
|
2341
|
-
const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
|
|
2342
|
-
for (const schemaName of ctx.recursiveSchemas) {
|
|
2343
|
-
ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
|
|
2344
|
-
}
|
|
2345
|
-
return {
|
|
2346
|
-
schemaText,
|
|
2347
|
-
namedNestedSchemas: ctx.namedNestedSchemas,
|
|
2348
|
-
warnings: ctx.warnings
|
|
2349
|
-
};
|
|
2179
|
+
|
|
2180
|
+
// src/discovery/filter-field-types.ts
|
|
2181
|
+
import {
|
|
2182
|
+
Node as Node4,
|
|
2183
|
+
SyntaxKind as SyntaxKind2
|
|
2184
|
+
} from "ts-morph";
|
|
2185
|
+
|
|
2186
|
+
// src/discovery/enum-resolution.ts
|
|
2187
|
+
function resolveEnumValues(name, sourceFile, project) {
|
|
2188
|
+
const resolved = findType(name, sourceFile, project);
|
|
2189
|
+
if (!resolved || resolved.kind !== "enum") return null;
|
|
2190
|
+
let numeric = true;
|
|
2191
|
+
const values = resolved.members.map((m) => {
|
|
2192
|
+
const parsed = JSON.parse(m);
|
|
2193
|
+
if (typeof parsed === "string") numeric = false;
|
|
2194
|
+
return String(parsed);
|
|
2195
|
+
});
|
|
2196
|
+
if (values.length === 0) return null;
|
|
2197
|
+
return { values, numeric };
|
|
2350
2198
|
}
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2199
|
+
|
|
2200
|
+
// src/discovery/filter-field-types.ts
|
|
2201
|
+
var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
|
|
2202
|
+
var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
|
|
2203
|
+
var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
|
|
2204
|
+
var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
|
|
2205
|
+
var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
|
|
2206
|
+
function classifyTypeKeyword(raw) {
|
|
2207
|
+
const t = raw.toLowerCase();
|
|
2208
|
+
if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
|
|
2209
|
+
if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
|
|
2210
|
+
if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
|
|
2211
|
+
if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
|
|
2212
|
+
if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
|
|
2213
|
+
return null;
|
|
2363
2214
|
}
|
|
2364
|
-
function
|
|
2365
|
-
return
|
|
2215
|
+
function markNullable(r, nullable) {
|
|
2216
|
+
return nullable ? { ...r, nullable: true } : r;
|
|
2366
2217
|
}
|
|
2367
|
-
function
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
if (has("IsInt")) base = "z.number().int()";
|
|
2394
|
-
if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
|
|
2395
|
-
if (has("Allow")) base = "z.unknown()";
|
|
2396
|
-
if (has("IsEmail")) {
|
|
2397
|
-
base = ensureStringBase(base);
|
|
2398
|
-
refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
|
|
2399
|
-
}
|
|
2400
|
-
if (has("IsUrl")) {
|
|
2401
|
-
base = ensureStringBase(base);
|
|
2402
|
-
refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
|
|
2403
|
-
}
|
|
2404
|
-
if (has("IsUUID")) {
|
|
2405
|
-
base = ensureStringBase(base);
|
|
2406
|
-
refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
|
|
2407
|
-
}
|
|
2408
|
-
if (has("Matches")) {
|
|
2409
|
-
const re = firstArgText2(dec("Matches"));
|
|
2410
|
-
if (re) {
|
|
2411
|
-
base = ensureStringBase(base);
|
|
2412
|
-
refinements.push(`.regex(${re})`);
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
if (has("MinLength")) {
|
|
2416
|
-
const n = numericArg2(dec("MinLength"));
|
|
2417
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
2418
|
-
}
|
|
2419
|
-
if (has("MaxLength")) {
|
|
2420
|
-
const n = numericArg2(dec("MaxLength"));
|
|
2421
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
2422
|
-
}
|
|
2423
|
-
if (has("Length")) {
|
|
2424
|
-
const [min, max] = numericArgs2(dec("Length"));
|
|
2425
|
-
if (min !== null) refinements.push(`.min(${min})`);
|
|
2426
|
-
if (max !== null) refinements.push(`.max(${max})`);
|
|
2427
|
-
}
|
|
2428
|
-
if (has("Min")) {
|
|
2429
|
-
const n = numericArg2(dec("Min"));
|
|
2430
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
2431
|
-
}
|
|
2432
|
-
if (has("Max")) {
|
|
2433
|
-
const n = numericArg2(dec("Max"));
|
|
2434
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
2435
|
-
}
|
|
2436
|
-
if (has("IsPositive")) refinements.push(".positive()");
|
|
2437
|
-
if (has("IsNegative")) refinements.push(".negative()");
|
|
2438
|
-
if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
|
|
2439
|
-
if (has("IsEnum")) {
|
|
2440
|
-
const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
|
|
2441
|
-
if (enumExpr) base = enumExpr;
|
|
2442
|
-
}
|
|
2443
|
-
if (has("IsIn")) {
|
|
2444
|
-
const inExpr = inSchemaFromDecorator2(dec("IsIn"));
|
|
2445
|
-
if (inExpr) base = inExpr;
|
|
2446
|
-
}
|
|
2447
|
-
for (const name of decorators.keys()) {
|
|
2448
|
-
if (!KNOWN_DECORATORS2.has(name)) {
|
|
2449
|
-
comments.push(`/* @${name}: not translatable to zod (server-only) */`);
|
|
2450
|
-
if (!ctx.warnedDecorators.has(name)) {
|
|
2451
|
-
ctx.warnedDecorators.add(name);
|
|
2452
|
-
const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
|
|
2453
|
-
ctx.warnings.push(msg);
|
|
2454
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2218
|
+
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
2219
|
+
if (Node4.isUnionTypeNode(typeNode)) {
|
|
2220
|
+
let nullable = false;
|
|
2221
|
+
const stringLits = [];
|
|
2222
|
+
const numberLits = [];
|
|
2223
|
+
const others = [];
|
|
2224
|
+
for (const member of typeNode.getTypeNodes()) {
|
|
2225
|
+
const kind = member.getKind();
|
|
2226
|
+
if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
|
|
2227
|
+
nullable = true;
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
if (Node4.isLiteralTypeNode(member)) {
|
|
2231
|
+
const lit = member.getLiteral();
|
|
2232
|
+
if (Node4.isStringLiteral(lit)) {
|
|
2233
|
+
stringLits.push(lit.getLiteralValue());
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
if (Node4.isNumericLiteral(lit)) {
|
|
2237
|
+
numberLits.push(lit.getText());
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
if (lit.getKind() === SyntaxKind2.NullKeyword) {
|
|
2241
|
+
nullable = true;
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2455
2244
|
}
|
|
2245
|
+
others.push(member);
|
|
2246
|
+
}
|
|
2247
|
+
if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
|
|
2248
|
+
return markNullable({ kind: "string", enumValues: stringLits }, nullable);
|
|
2249
|
+
}
|
|
2250
|
+
if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
|
|
2251
|
+
return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
|
|
2456
2252
|
}
|
|
2253
|
+
if (others.length === 1) {
|
|
2254
|
+
const inner = classifyTypeNode(others[0], sourceFile, project, opts);
|
|
2255
|
+
return markNullable(inner, nullable || inner.nullable === true);
|
|
2256
|
+
}
|
|
2257
|
+
return markNullable({ kind: "unknown" }, nullable);
|
|
2457
2258
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2259
|
+
switch (typeNode.getKind()) {
|
|
2260
|
+
case SyntaxKind2.StringKeyword:
|
|
2261
|
+
return { kind: "string" };
|
|
2262
|
+
case SyntaxKind2.NumberKeyword:
|
|
2263
|
+
return { kind: "number" };
|
|
2264
|
+
case SyntaxKind2.BooleanKeyword:
|
|
2265
|
+
return { kind: "boolean" };
|
|
2266
|
+
case SyntaxKind2.AnyKeyword:
|
|
2267
|
+
case SyntaxKind2.UnknownKeyword:
|
|
2268
|
+
return { kind: "unknown" };
|
|
2269
|
+
default:
|
|
2270
|
+
break;
|
|
2461
2271
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2272
|
+
if (Node4.isTypeReference(typeNode)) {
|
|
2273
|
+
const refName = typeNode.getTypeName().getText();
|
|
2274
|
+
if (refName === "Date") return { kind: "date" };
|
|
2275
|
+
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
2276
|
+
const typeRef = opts?.resolveRef?.(refName) ?? null;
|
|
2277
|
+
const en = resolveEnumValues(refName, sourceFile, project);
|
|
2278
|
+
if (en) {
|
|
2279
|
+
const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
2280
|
+
return typeRef ? { ...base, typeRef } : base;
|
|
2281
|
+
}
|
|
2282
|
+
if (typeRef) return { kind: "unknown", typeRef };
|
|
2283
|
+
return { kind: "unknown" };
|
|
2465
2284
|
}
|
|
2466
|
-
return
|
|
2467
|
-
}
|
|
2468
|
-
function applyPresence2(expr, decorators) {
|
|
2469
|
-
if (decorators.has("IsDefined")) return expr;
|
|
2470
|
-
if (decorators.has("IsOptional")) return `${expr}.optional()`;
|
|
2471
|
-
return expr;
|
|
2472
|
-
}
|
|
2473
|
-
function baseFromType2(typeText, isArrayType, ctx, classFile) {
|
|
2474
|
-
const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
|
|
2475
|
-
switch (inner) {
|
|
2476
|
-
case "string":
|
|
2477
|
-
return "z.string()";
|
|
2478
|
-
case "number":
|
|
2479
|
-
return "z.number()";
|
|
2480
|
-
case "boolean":
|
|
2481
|
-
return "z.boolean()";
|
|
2482
|
-
case "Date":
|
|
2483
|
-
return "z.coerce.date()";
|
|
2484
|
-
case "File":
|
|
2485
|
-
case "Express.Multer.File":
|
|
2486
|
-
return "z.instanceof(File)";
|
|
2487
|
-
default:
|
|
2488
|
-
return "z.unknown()";
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
function ensureStringBase(base) {
|
|
2492
|
-
return isStringBase(base) ? base : "z.string()";
|
|
2493
|
-
}
|
|
2494
|
-
function isStringBase(base) {
|
|
2495
|
-
return base.startsWith("z.string(");
|
|
2496
|
-
}
|
|
2497
|
-
function buildNestedReference2(className, fromFile, ctx) {
|
|
2498
|
-
if (ctx.visiting.has(className) || ctx.depth >= 8) {
|
|
2499
|
-
const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
|
|
2500
|
-
ctx.emittedClasses.set(className, reserved);
|
|
2501
|
-
ctx.recursiveSchemas.add(reserved);
|
|
2502
|
-
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2503
|
-
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
2504
|
-
const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
|
|
2505
|
-
ctx.warnings.push(msg);
|
|
2506
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2507
|
-
}
|
|
2508
|
-
return `z.lazy(() => ${reserved})`;
|
|
2509
|
-
}
|
|
2510
|
-
const existing = ctx.emittedClasses.get(className);
|
|
2511
|
-
if (existing) return existing;
|
|
2512
|
-
const schemaName = aliasFor2(className, ctx);
|
|
2513
|
-
const resolved = findType(className, fromFile, ctx.project);
|
|
2514
|
-
if (!resolved || resolved.kind !== "class") {
|
|
2515
|
-
return "z.object({}).passthrough()";
|
|
2516
|
-
}
|
|
2517
|
-
ctx.emittedClasses.set(className, schemaName);
|
|
2518
|
-
ctx.visiting.add(className);
|
|
2519
|
-
ctx.depth += 1;
|
|
2520
|
-
const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
|
|
2521
|
-
ctx.depth -= 1;
|
|
2522
|
-
ctx.visiting.delete(className);
|
|
2523
|
-
ctx.namedNestedSchemas.set(schemaName, childText);
|
|
2524
|
-
return schemaName;
|
|
2525
|
-
}
|
|
2526
|
-
function aliasFor2(className, ctx) {
|
|
2527
|
-
const baseName = `${className}Schema`;
|
|
2528
|
-
let candidate = baseName;
|
|
2529
|
-
let i = 1;
|
|
2530
|
-
const used = new Set(ctx.namedNestedSchemas.keys());
|
|
2531
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
2532
|
-
while (used.has(candidate)) {
|
|
2533
|
-
candidate = `${baseName}_${i}`;
|
|
2534
|
-
i += 1;
|
|
2535
|
-
}
|
|
2536
|
-
return candidate;
|
|
2537
|
-
}
|
|
2538
|
-
function firstArg2(decorator) {
|
|
2539
|
-
return decorator?.getArguments()[0];
|
|
2540
|
-
}
|
|
2541
|
-
function firstArgText2(decorator) {
|
|
2542
|
-
const arg = firstArg2(decorator);
|
|
2543
|
-
return arg ? arg.getText() : null;
|
|
2544
|
-
}
|
|
2545
|
-
function numericArg2(decorator) {
|
|
2546
|
-
const arg = firstArg2(decorator);
|
|
2547
|
-
if (arg && Node4.isNumericLiteral(arg)) return arg.getText();
|
|
2548
|
-
return null;
|
|
2549
|
-
}
|
|
2550
|
-
function numericArgs2(decorator) {
|
|
2551
|
-
const args = decorator?.getArguments() ?? [];
|
|
2552
|
-
const num = (n) => n && Node4.isNumericLiteral(n) ? n.getText() : null;
|
|
2553
|
-
return [num(args[0]), num(args[1])];
|
|
2554
|
-
}
|
|
2555
|
-
function messageArg(decorator) {
|
|
2556
|
-
const args = decorator?.getArguments() ?? [];
|
|
2557
|
-
for (const arg of args) {
|
|
2558
|
-
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2559
|
-
for (const prop of arg.getProperties()) {
|
|
2560
|
-
if (Node4.isPropertyAssignment(prop) && prop.getName() === "message") {
|
|
2561
|
-
const init = prop.getInitializer();
|
|
2562
|
-
if (init && Node4.isStringLiteral(init)) {
|
|
2563
|
-
return `{ message: ${init.getText()} }`;
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
return "";
|
|
2570
|
-
}
|
|
2571
|
-
function resolveTypeFactoryName2(decorator) {
|
|
2572
|
-
const arg = firstArg2(decorator);
|
|
2573
|
-
if (!arg) return null;
|
|
2574
|
-
if (Node4.isArrowFunction(arg)) {
|
|
2575
|
-
const body = arg.getBody();
|
|
2576
|
-
if (Node4.isIdentifier(body)) return body.getText();
|
|
2577
|
-
}
|
|
2578
|
-
return null;
|
|
2579
|
-
}
|
|
2580
|
-
function singularClassName2(typeText) {
|
|
2581
|
-
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2582
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2583
|
-
}
|
|
2584
|
-
function enumSchemaFromDecorator2(decorator, classFile, ctx) {
|
|
2585
|
-
const arg = firstArg2(decorator);
|
|
2586
|
-
if (!arg) return null;
|
|
2587
|
-
if (Node4.isIdentifier(arg)) {
|
|
2588
|
-
const name = arg.getText();
|
|
2589
|
-
const resolved = findType(name, classFile, ctx.project);
|
|
2590
|
-
if (resolved && resolved.kind === "enum") {
|
|
2591
|
-
return `z.enum([${resolved.members.join(", ")}])`;
|
|
2592
|
-
}
|
|
2593
|
-
const msg = `@IsEnum(${name}): enum could not be resolved to literal members and is not importable into the generated form schema; falling back to z.unknown().`;
|
|
2594
|
-
if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
|
|
2595
|
-
ctx.warnedDecorators.add(`IsEnum:${name}`);
|
|
2596
|
-
ctx.warnings.push(msg);
|
|
2597
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2598
|
-
}
|
|
2599
|
-
return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
|
|
2600
|
-
}
|
|
2601
|
-
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2602
|
-
const values = [];
|
|
2603
|
-
for (const p of arg.getProperties()) {
|
|
2604
|
-
if (!Node4.isPropertyAssignment(p)) continue;
|
|
2605
|
-
const init = p.getInitializer();
|
|
2606
|
-
if (init && Node4.isStringLiteral(init)) values.push(init.getText());
|
|
2607
|
-
}
|
|
2608
|
-
if (values.length > 0) return `z.enum([${values.join(", ")}])`;
|
|
2609
|
-
}
|
|
2610
|
-
return null;
|
|
2611
|
-
}
|
|
2612
|
-
function inSchemaFromDecorator2(decorator) {
|
|
2613
|
-
const arg = firstArg2(decorator);
|
|
2614
|
-
if (arg && Node4.isArrayLiteralExpression(arg)) {
|
|
2615
|
-
const elements = arg.getElements();
|
|
2616
|
-
const allStrings = elements.every((e) => Node4.isStringLiteral(e));
|
|
2617
|
-
if (allStrings && elements.length > 0) {
|
|
2618
|
-
return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
|
|
2619
|
-
}
|
|
2620
|
-
if (elements.length > 0) {
|
|
2621
|
-
return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
return null;
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
// src/discovery/filter-for.ts
|
|
2628
|
-
import {
|
|
2629
|
-
Node as Node6
|
|
2630
|
-
} from "ts-morph";
|
|
2631
|
-
|
|
2632
|
-
// src/discovery/filter-field-types.ts
|
|
2633
|
-
import {
|
|
2634
|
-
Node as Node5,
|
|
2635
|
-
SyntaxKind as SyntaxKind2
|
|
2636
|
-
} from "ts-morph";
|
|
2637
|
-
|
|
2638
|
-
// src/discovery/enum-resolution.ts
|
|
2639
|
-
function resolveEnumValues(name, sourceFile, project) {
|
|
2640
|
-
const resolved = findType(name, sourceFile, project);
|
|
2641
|
-
if (!resolved || resolved.kind !== "enum") return null;
|
|
2642
|
-
let numeric = true;
|
|
2643
|
-
const values = resolved.members.map((m) => {
|
|
2644
|
-
const parsed = JSON.parse(m);
|
|
2645
|
-
if (typeof parsed === "string") numeric = false;
|
|
2646
|
-
return String(parsed);
|
|
2647
|
-
});
|
|
2648
|
-
if (values.length === 0) return null;
|
|
2649
|
-
return { values, numeric };
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
// src/discovery/filter-field-types.ts
|
|
2653
|
-
var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
|
|
2654
|
-
var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
|
|
2655
|
-
var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
|
|
2656
|
-
var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
|
|
2657
|
-
var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
|
|
2658
|
-
function classifyTypeKeyword(raw) {
|
|
2659
|
-
const t = raw.toLowerCase();
|
|
2660
|
-
if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
|
|
2661
|
-
if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
|
|
2662
|
-
if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
|
|
2663
|
-
if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
|
|
2664
|
-
if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
|
|
2665
|
-
return null;
|
|
2666
|
-
}
|
|
2667
|
-
function markNullable(r, nullable) {
|
|
2668
|
-
return nullable ? { ...r, nullable: true } : r;
|
|
2669
|
-
}
|
|
2670
|
-
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
2671
|
-
if (Node5.isUnionTypeNode(typeNode)) {
|
|
2672
|
-
let nullable = false;
|
|
2673
|
-
const stringLits = [];
|
|
2674
|
-
const numberLits = [];
|
|
2675
|
-
const others = [];
|
|
2676
|
-
for (const member of typeNode.getTypeNodes()) {
|
|
2677
|
-
const kind = member.getKind();
|
|
2678
|
-
if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
|
|
2679
|
-
nullable = true;
|
|
2680
|
-
continue;
|
|
2681
|
-
}
|
|
2682
|
-
if (Node5.isLiteralTypeNode(member)) {
|
|
2683
|
-
const lit = member.getLiteral();
|
|
2684
|
-
if (Node5.isStringLiteral(lit)) {
|
|
2685
|
-
stringLits.push(lit.getLiteralValue());
|
|
2686
|
-
continue;
|
|
2687
|
-
}
|
|
2688
|
-
if (Node5.isNumericLiteral(lit)) {
|
|
2689
|
-
numberLits.push(lit.getText());
|
|
2690
|
-
continue;
|
|
2691
|
-
}
|
|
2692
|
-
if (lit.getKind() === SyntaxKind2.NullKeyword) {
|
|
2693
|
-
nullable = true;
|
|
2694
|
-
continue;
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
others.push(member);
|
|
2698
|
-
}
|
|
2699
|
-
if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
|
|
2700
|
-
return markNullable({ kind: "string", enumValues: stringLits }, nullable);
|
|
2701
|
-
}
|
|
2702
|
-
if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
|
|
2703
|
-
return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
|
|
2704
|
-
}
|
|
2705
|
-
if (others.length === 1) {
|
|
2706
|
-
const inner = classifyTypeNode(others[0], sourceFile, project, opts);
|
|
2707
|
-
return markNullable(inner, nullable || inner.nullable === true);
|
|
2708
|
-
}
|
|
2709
|
-
return markNullable({ kind: "unknown" }, nullable);
|
|
2710
|
-
}
|
|
2711
|
-
switch (typeNode.getKind()) {
|
|
2712
|
-
case SyntaxKind2.StringKeyword:
|
|
2713
|
-
return { kind: "string" };
|
|
2714
|
-
case SyntaxKind2.NumberKeyword:
|
|
2715
|
-
return { kind: "number" };
|
|
2716
|
-
case SyntaxKind2.BooleanKeyword:
|
|
2717
|
-
return { kind: "boolean" };
|
|
2718
|
-
case SyntaxKind2.AnyKeyword:
|
|
2719
|
-
case SyntaxKind2.UnknownKeyword:
|
|
2720
|
-
return { kind: "unknown" };
|
|
2721
|
-
default:
|
|
2722
|
-
break;
|
|
2723
|
-
}
|
|
2724
|
-
if (Node5.isTypeReference(typeNode)) {
|
|
2725
|
-
const refName = typeNode.getTypeName().getText();
|
|
2726
|
-
if (refName === "Date") return { kind: "date" };
|
|
2727
|
-
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
2728
|
-
const typeRef = opts?.resolveRef?.(refName) ?? null;
|
|
2729
|
-
const en = resolveEnumValues(refName, sourceFile, project);
|
|
2730
|
-
if (en) {
|
|
2731
|
-
const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
2732
|
-
return typeRef ? { ...base, typeRef } : base;
|
|
2733
|
-
}
|
|
2734
|
-
if (typeRef) return { kind: "unknown", typeRef };
|
|
2735
|
-
return { kind: "unknown" };
|
|
2736
|
-
}
|
|
2737
|
-
if (Node5.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
2738
|
-
return { kind: "unknown" };
|
|
2285
|
+
if (Node4.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
2286
|
+
return { kind: "unknown" };
|
|
2739
2287
|
}
|
|
2740
2288
|
function enumFromDecoratorArgs(args, sourceFile, project) {
|
|
2741
2289
|
for (const arg of args) {
|
|
2742
|
-
if (
|
|
2290
|
+
if (Node4.isArrowFunction(arg)) {
|
|
2743
2291
|
const body = arg.getBody();
|
|
2744
|
-
if (
|
|
2292
|
+
if (Node4.isIdentifier(body)) {
|
|
2745
2293
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
2746
2294
|
if (en) return en;
|
|
2747
2295
|
}
|
|
2748
2296
|
}
|
|
2749
|
-
if (
|
|
2297
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2750
2298
|
const itemsProp = arg.getProperty("items");
|
|
2751
|
-
if (itemsProp &&
|
|
2299
|
+
if (itemsProp && Node4.isPropertyAssignment(itemsProp)) {
|
|
2752
2300
|
const init = itemsProp.getInitializer();
|
|
2753
|
-
if (init &&
|
|
2301
|
+
if (init && Node4.isArrowFunction(init)) {
|
|
2754
2302
|
const body = init.getBody();
|
|
2755
|
-
if (
|
|
2303
|
+
if (Node4.isIdentifier(body)) {
|
|
2756
2304
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
2757
2305
|
if (en) return en;
|
|
2758
2306
|
}
|
|
@@ -2775,7 +2323,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2775
2323
|
return { kind: "string" };
|
|
2776
2324
|
}
|
|
2777
2325
|
for (const arg of args) {
|
|
2778
|
-
if (
|
|
2326
|
+
if (Node4.isStringLiteral(arg)) {
|
|
2779
2327
|
const raw = arg.getLiteralValue();
|
|
2780
2328
|
const kind = classifyTypeKeyword(raw);
|
|
2781
2329
|
if (kind) {
|
|
@@ -2783,11 +2331,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2783
2331
|
return { kind };
|
|
2784
2332
|
}
|
|
2785
2333
|
}
|
|
2786
|
-
if (
|
|
2334
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2787
2335
|
const enumProp = arg.getProperty("enum");
|
|
2788
|
-
if (enumProp &&
|
|
2336
|
+
if (enumProp && Node4.isPropertyAssignment(enumProp)) {
|
|
2789
2337
|
const init = enumProp.getInitializer();
|
|
2790
|
-
if (init &&
|
|
2338
|
+
if (init && Node4.isIdentifier(init)) {
|
|
2791
2339
|
const en = resolveEnumValues(init.getText(), sourceFile, project);
|
|
2792
2340
|
if (en) {
|
|
2793
2341
|
return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
@@ -2796,9 +2344,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2796
2344
|
}
|
|
2797
2345
|
}
|
|
2798
2346
|
const typeProp = arg.getProperty("type");
|
|
2799
|
-
if (typeProp &&
|
|
2347
|
+
if (typeProp && Node4.isPropertyAssignment(typeProp)) {
|
|
2800
2348
|
const init = typeProp.getInitializer();
|
|
2801
|
-
if (init &&
|
|
2349
|
+
if (init && Node4.isStringLiteral(init)) {
|
|
2802
2350
|
const kind = classifyTypeKeyword(init.getLiteralValue());
|
|
2803
2351
|
if (kind) return { kind };
|
|
2804
2352
|
}
|
|
@@ -2834,7 +2382,7 @@ function toFilterFieldType(name, r) {
|
|
|
2834
2382
|
|
|
2835
2383
|
// src/discovery/filter-for.ts
|
|
2836
2384
|
function classifyFilterForHint(typeInit) {
|
|
2837
|
-
if (
|
|
2385
|
+
if (Node5.isStringLiteral(typeInit)) {
|
|
2838
2386
|
switch (typeInit.getLiteralValue()) {
|
|
2839
2387
|
case "string":
|
|
2840
2388
|
return { kind: "string" };
|
|
@@ -2848,10 +2396,10 @@ function classifyFilterForHint(typeInit) {
|
|
|
2848
2396
|
return null;
|
|
2849
2397
|
}
|
|
2850
2398
|
}
|
|
2851
|
-
if (
|
|
2399
|
+
if (Node5.isArrayLiteralExpression(typeInit)) {
|
|
2852
2400
|
const values = [];
|
|
2853
2401
|
for (const el of typeInit.getElements()) {
|
|
2854
|
-
if (!
|
|
2402
|
+
if (!Node5.isStringLiteral(el)) return null;
|
|
2855
2403
|
values.push(el.getLiteralValue());
|
|
2856
2404
|
}
|
|
2857
2405
|
if (values.length === 0) return null;
|
|
@@ -2886,11 +2434,11 @@ function extractFilterForHints(classDecl, project) {
|
|
|
2886
2434
|
if (!filterForDec) continue;
|
|
2887
2435
|
const args = filterForDec.getArguments();
|
|
2888
2436
|
const keyArg = args[0];
|
|
2889
|
-
const inputKey = keyArg &&
|
|
2437
|
+
const inputKey = keyArg && Node5.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
|
|
2890
2438
|
const optsArg = args[1];
|
|
2891
|
-
if (optsArg &&
|
|
2439
|
+
if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
|
|
2892
2440
|
const typeProp = optsArg.getProperty("type");
|
|
2893
|
-
if (typeProp &&
|
|
2441
|
+
if (typeProp && Node5.isPropertyAssignment(typeProp)) {
|
|
2894
2442
|
const typeInit = typeProp.getInitializer();
|
|
2895
2443
|
if (typeInit) {
|
|
2896
2444
|
const classified = classifyFilterForHint(typeInit);
|
|
@@ -2913,14 +2461,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
|
|
|
2913
2461
|
const args = filterDecorator.getArguments();
|
|
2914
2462
|
if (args.length === 0) continue;
|
|
2915
2463
|
const filterClassArg = args[0];
|
|
2916
|
-
if (!filterClassArg || !
|
|
2464
|
+
if (!filterClassArg || !Node5.isIdentifier(filterClassArg)) continue;
|
|
2917
2465
|
let source = "query";
|
|
2918
2466
|
const optionsArg = args[1];
|
|
2919
|
-
if (optionsArg &&
|
|
2467
|
+
if (optionsArg && Node5.isObjectLiteralExpression(optionsArg)) {
|
|
2920
2468
|
const sourceProp = optionsArg.getProperty("source");
|
|
2921
|
-
if (sourceProp &&
|
|
2469
|
+
if (sourceProp && Node5.isPropertyAssignment(sourceProp)) {
|
|
2922
2470
|
const init = sourceProp.getInitializer();
|
|
2923
|
-
if (init &&
|
|
2471
|
+
if (init && Node5.isStringLiteral(init) && init.getLiteralValue() === "body") {
|
|
2924
2472
|
source = "body";
|
|
2925
2473
|
}
|
|
2926
2474
|
}
|
|
@@ -2983,22 +2531,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
|
|
|
2983
2531
|
const args = dec.getArguments();
|
|
2984
2532
|
if (args.length === 0) continue;
|
|
2985
2533
|
const arg = args[0];
|
|
2986
|
-
if (
|
|
2534
|
+
if (Node5.isObjectLiteralExpression(arg)) {
|
|
2987
2535
|
const entityProp = arg.getProperty("entity");
|
|
2988
|
-
if (entityProp &&
|
|
2536
|
+
if (entityProp && Node5.isPropertyAssignment(entityProp)) {
|
|
2989
2537
|
const init = entityProp.getInitializer();
|
|
2990
|
-
if (init &&
|
|
2538
|
+
if (init && Node5.isArrowFunction(init)) {
|
|
2991
2539
|
const body = init.getBody();
|
|
2992
|
-
if (
|
|
2540
|
+
if (Node5.isIdentifier(body)) {
|
|
2993
2541
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
2994
2542
|
if (resolved?.kind === "class") return resolved.decl;
|
|
2995
2543
|
}
|
|
2996
2544
|
}
|
|
2997
2545
|
}
|
|
2998
2546
|
}
|
|
2999
|
-
if (
|
|
2547
|
+
if (Node5.isArrowFunction(arg)) {
|
|
3000
2548
|
const body = arg.getBody();
|
|
3001
|
-
if (
|
|
2549
|
+
if (Node5.isIdentifier(body)) {
|
|
3002
2550
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
3003
2551
|
if (resolved?.kind === "class") return resolved.decl;
|
|
3004
2552
|
}
|
|
@@ -3022,11 +2570,11 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3022
2570
|
const args = filterableDecorator.getArguments();
|
|
3023
2571
|
if (args.length === 0) return [];
|
|
3024
2572
|
const optionsArg = args[0];
|
|
3025
|
-
if (!
|
|
2573
|
+
if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
|
|
3026
2574
|
const entityProp = optionsArg.getProperty("entity");
|
|
3027
|
-
if (!entityProp || !
|
|
2575
|
+
if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
|
|
3028
2576
|
const entityInit = entityProp.getInitializer();
|
|
3029
|
-
if (!entityInit || !
|
|
2577
|
+
if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
|
|
3030
2578
|
const entityName = entityInit.getText();
|
|
3031
2579
|
const filterSourceFile = filterClass.getSourceFile();
|
|
3032
2580
|
const resolvedEntity = findType(entityName, filterSourceFile, project);
|
|
@@ -3042,17 +2590,17 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3042
2590
|
const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
|
|
3043
2591
|
if (relationsDecorator) {
|
|
3044
2592
|
const relArgs = relationsDecorator.getArguments();
|
|
3045
|
-
if (relArgs.length > 0 &&
|
|
2593
|
+
if (relArgs.length > 0 && Node5.isObjectLiteralExpression(relArgs[0])) {
|
|
3046
2594
|
for (const relProp of relArgs[0].getProperties()) {
|
|
3047
|
-
if (!
|
|
2595
|
+
if (!Node5.isPropertyAssignment(relProp)) continue;
|
|
3048
2596
|
const relInit = relProp.getInitializer();
|
|
3049
|
-
if (!relInit || !
|
|
2597
|
+
if (!relInit || !Node5.isObjectLiteralExpression(relInit)) continue;
|
|
3050
2598
|
const keysProp = relInit.getProperty("keys");
|
|
3051
|
-
if (!keysProp || !
|
|
2599
|
+
if (!keysProp || !Node5.isPropertyAssignment(keysProp)) continue;
|
|
3052
2600
|
const keysInit = keysProp.getInitializer();
|
|
3053
|
-
if (!keysInit || !
|
|
2601
|
+
if (!keysInit || !Node5.isArrayLiteralExpression(keysInit)) continue;
|
|
3054
2602
|
for (const el of keysInit.getElements()) {
|
|
3055
|
-
if (
|
|
2603
|
+
if (Node5.isStringLiteral(el)) {
|
|
3056
2604
|
fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
|
|
3057
2605
|
}
|
|
3058
2606
|
}
|
|
@@ -3062,267 +2610,65 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3062
2610
|
return fields;
|
|
3063
2611
|
}
|
|
3064
2612
|
|
|
3065
|
-
// src/discovery/
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
2613
|
+
// src/discovery/dto-type-resolver.ts
|
|
2614
|
+
var WRAPPER_TYPES = {
|
|
2615
|
+
// MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
|
|
2616
|
+
// wrappers around related entities; the wire shape is just the referenced
|
|
2617
|
+
// entity. Unwrap to the type argument.
|
|
2618
|
+
Ref: "unwrap",
|
|
2619
|
+
Reference: "unwrap",
|
|
2620
|
+
LoadedReference: "unwrap",
|
|
2621
|
+
IdentifiedReference: "unwrap",
|
|
2622
|
+
// MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
|
|
2623
|
+
Opt: "unwrap",
|
|
2624
|
+
Loaded: "unwrap",
|
|
2625
|
+
// Promise<T> — unwrap
|
|
2626
|
+
Promise: "unwrap",
|
|
2627
|
+
// MikroORM Collection<T> serializes as an array of T on the wire.
|
|
2628
|
+
Collection: "arrayOf",
|
|
2629
|
+
// Array<T> generic form
|
|
2630
|
+
Array: "arrayOf"
|
|
2631
|
+
};
|
|
2632
|
+
var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
2633
|
+
"Record",
|
|
2634
|
+
"Omit",
|
|
2635
|
+
"Pick",
|
|
2636
|
+
"Partial",
|
|
2637
|
+
"Required",
|
|
2638
|
+
"Readonly",
|
|
2639
|
+
"Map",
|
|
2640
|
+
"Set"
|
|
2641
|
+
]);
|
|
2642
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
2643
|
+
if (depth <= 0) return "unknown";
|
|
2644
|
+
if (Node6.isArrayTypeNode(typeNode)) {
|
|
2645
|
+
const elementType = typeNode.getElementTypeNode();
|
|
2646
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3088
2647
|
}
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
project.addSourceFileAtPath(f);
|
|
2648
|
+
if (Node6.isUnionTypeNode(typeNode)) {
|
|
2649
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3092
2650
|
}
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
projectRoot: cwd,
|
|
3096
|
-
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3097
|
-
});
|
|
3098
|
-
try {
|
|
3099
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
3100
|
-
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3101
|
-
}
|
|
3102
|
-
} finally {
|
|
3103
|
-
restoreDiscoveryContext(prevCtx);
|
|
2651
|
+
if (Node6.isIntersectionTypeNode(typeNode)) {
|
|
2652
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3104
2653
|
}
|
|
3105
|
-
|
|
3106
|
-
}
|
|
3107
|
-
function zodAstToTs(node) {
|
|
3108
|
-
if (!Node7.isCallExpression(node)) return "unknown";
|
|
3109
|
-
const expr = node.getExpression();
|
|
3110
|
-
if (Node7.isPropertyAccessExpression(expr)) {
|
|
3111
|
-
const methodName = expr.getName();
|
|
3112
|
-
const receiver = expr.getExpression();
|
|
3113
|
-
if (methodName === "optional") {
|
|
3114
|
-
return `${zodAstToTs(receiver)} | undefined`;
|
|
3115
|
-
}
|
|
3116
|
-
if (methodName === "nullable") {
|
|
3117
|
-
return `${zodAstToTs(receiver)} | null`;
|
|
3118
|
-
}
|
|
3119
|
-
const args = node.getArguments();
|
|
3120
|
-
switch (methodName) {
|
|
3121
|
-
case "string":
|
|
3122
|
-
return "string";
|
|
3123
|
-
case "number":
|
|
3124
|
-
return "number";
|
|
3125
|
-
case "boolean":
|
|
3126
|
-
return "boolean";
|
|
3127
|
-
case "unknown":
|
|
3128
|
-
return "unknown";
|
|
3129
|
-
case "any":
|
|
3130
|
-
return "unknown";
|
|
3131
|
-
case "literal": {
|
|
3132
|
-
const lit = args[0];
|
|
3133
|
-
if (!lit) return "unknown";
|
|
3134
|
-
if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
3135
|
-
if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
3136
|
-
if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
|
|
3137
|
-
if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
|
|
3138
|
-
return "unknown";
|
|
3139
|
-
}
|
|
3140
|
-
case "enum": {
|
|
3141
|
-
const arrArg = args[0];
|
|
3142
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3143
|
-
const members = arrArg.getElements().map(
|
|
3144
|
-
(el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
3145
|
-
);
|
|
3146
|
-
return members.join(" | ");
|
|
3147
|
-
}
|
|
3148
|
-
case "array": {
|
|
3149
|
-
const inner = args[0];
|
|
3150
|
-
if (!inner) return "unknown";
|
|
3151
|
-
return `Array<${zodAstToTs(inner)}>`;
|
|
3152
|
-
}
|
|
3153
|
-
case "object": {
|
|
3154
|
-
const objArg = args[0];
|
|
3155
|
-
if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
|
|
3156
|
-
const lines = [];
|
|
3157
|
-
for (const prop of objArg.getProperties()) {
|
|
3158
|
-
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3159
|
-
const key = prop.getName();
|
|
3160
|
-
const valNode = prop.getInitializer();
|
|
3161
|
-
if (!valNode) continue;
|
|
3162
|
-
const tsType = zodAstToTs(valNode);
|
|
3163
|
-
const isOpt = isOptionalChain(valNode);
|
|
3164
|
-
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
3165
|
-
}
|
|
3166
|
-
return `{ ${lines.join("; ")} }`;
|
|
3167
|
-
}
|
|
3168
|
-
case "union": {
|
|
3169
|
-
const arrArg = args[0];
|
|
3170
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3171
|
-
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
3172
|
-
}
|
|
3173
|
-
case "record": {
|
|
3174
|
-
const valArg = args.length === 1 ? args[0] : args[1];
|
|
3175
|
-
if (!valArg) return "unknown";
|
|
3176
|
-
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
3177
|
-
}
|
|
3178
|
-
case "tuple": {
|
|
3179
|
-
const arrArg = args[0];
|
|
3180
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3181
|
-
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
3182
|
-
}
|
|
3183
|
-
default:
|
|
3184
|
-
return "unknown";
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
return "unknown";
|
|
3188
|
-
}
|
|
3189
|
-
function isOptionalChain(node) {
|
|
3190
|
-
if (!Node7.isCallExpression(node)) return false;
|
|
3191
|
-
const expr = node.getExpression();
|
|
3192
|
-
return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
3193
|
-
}
|
|
3194
|
-
function decoratorStringArg(decoratorExpr) {
|
|
3195
|
-
if (!decoratorExpr) return void 0;
|
|
3196
|
-
if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
3197
|
-
if (Node7.isArrayLiteralExpression(decoratorExpr)) {
|
|
3198
|
-
const first = decoratorExpr.getElements()[0];
|
|
3199
|
-
if (first && Node7.isStringLiteral(first)) return first.getLiteralValue();
|
|
3200
|
-
}
|
|
3201
|
-
return void 0;
|
|
3202
|
-
}
|
|
3203
|
-
function parseDefineContractCall(callExpr) {
|
|
3204
|
-
if (!Node7.isCallExpression(callExpr)) return null;
|
|
3205
|
-
const callee = callExpr.getExpression();
|
|
3206
|
-
const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
3207
|
-
if (calleeName !== "defineContract") return null;
|
|
3208
|
-
const args = callExpr.getArguments();
|
|
3209
|
-
const optsArg = args[0];
|
|
3210
|
-
if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
|
|
3211
|
-
let query = null;
|
|
3212
|
-
let body = null;
|
|
3213
|
-
let response = "unknown";
|
|
3214
|
-
let bodyZodText = null;
|
|
3215
|
-
let queryZodText = null;
|
|
3216
|
-
for (const prop of optsArg.getProperties()) {
|
|
3217
|
-
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3218
|
-
const propName = prop.getName();
|
|
3219
|
-
const val = prop.getInitializer();
|
|
3220
|
-
if (!val) continue;
|
|
3221
|
-
if (propName === "query") {
|
|
3222
|
-
query = zodAstToTs(val);
|
|
3223
|
-
queryZodText = val.getText();
|
|
3224
|
-
} else if (propName === "body") {
|
|
3225
|
-
body = zodAstToTs(val);
|
|
3226
|
-
bodyZodText = val.getText();
|
|
3227
|
-
} else if (propName === "response") {
|
|
3228
|
-
response = zodAstToTs(val);
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3232
|
-
}
|
|
3233
|
-
function deriveClassSegment(className) {
|
|
3234
|
-
const noSuffix = className.replace(/Controller$/, "");
|
|
3235
|
-
if (!noSuffix) {
|
|
3236
|
-
throw new Error(
|
|
3237
|
-
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
3238
|
-
);
|
|
3239
|
-
}
|
|
3240
|
-
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
3241
|
-
}
|
|
3242
|
-
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
3243
|
-
const classPortion = classAs ?? deriveClassSegment(className);
|
|
3244
|
-
const methodPortion = methodAs ?? methodName;
|
|
3245
|
-
return `${classPortion}.${methodPortion}`;
|
|
3246
|
-
}
|
|
3247
|
-
function joinPaths(prefix, suffix) {
|
|
3248
|
-
if (!prefix && !suffix) return "/";
|
|
3249
|
-
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3250
|
-
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
3251
|
-
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3252
|
-
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3253
|
-
const combined = p + s;
|
|
3254
|
-
return combined === "" ? "/" : combined;
|
|
3255
|
-
}
|
|
3256
|
-
function extractParams(path) {
|
|
3257
|
-
const matches = path.matchAll(/:(\w+)/g);
|
|
3258
|
-
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
3259
|
-
}
|
|
3260
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3261
|
-
if (depth <= 0) return "unknown";
|
|
3262
|
-
if (Node7.isArrayTypeNode(typeNode)) {
|
|
3263
|
-
const elementType = typeNode.getElementTypeNode();
|
|
3264
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3265
|
-
}
|
|
3266
|
-
if (Node7.isUnionTypeNode(typeNode)) {
|
|
3267
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3268
|
-
}
|
|
3269
|
-
if (Node7.isIntersectionTypeNode(typeNode)) {
|
|
3270
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3271
|
-
}
|
|
3272
|
-
if (Node7.isParenthesizedTypeNode(typeNode)) {
|
|
2654
|
+
if (Node6.isParenthesizedTypeNode(typeNode)) {
|
|
3273
2655
|
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3274
2656
|
}
|
|
3275
|
-
if (
|
|
2657
|
+
if (Node6.isTypeReference(typeNode)) {
|
|
3276
2658
|
const typeName = typeNode.getTypeName();
|
|
3277
|
-
const name =
|
|
2659
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3278
2660
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
3279
2661
|
if (name === "Date") return "string";
|
|
3280
2662
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
3281
2663
|
if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
|
|
3282
2664
|
return "unknown";
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3287
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3288
|
-
}
|
|
3289
|
-
return "unknown";
|
|
2665
|
+
const wrapperMode = WRAPPER_TYPES[name];
|
|
2666
|
+
if (wrapperMode) {
|
|
2667
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3290
2668
|
}
|
|
3291
|
-
if (name
|
|
3292
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3293
|
-
const firstTypeArg = typeArgs[0];
|
|
3294
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3295
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
3296
|
-
}
|
|
3297
|
-
return "Array<unknown>";
|
|
3298
|
-
}
|
|
3299
|
-
if (name === "Opt" || name === "Loaded") {
|
|
3300
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3301
|
-
const firstTypeArg = typeArgs[0];
|
|
3302
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3303
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3304
|
-
}
|
|
3305
|
-
return "unknown";
|
|
3306
|
-
}
|
|
3307
|
-
if (name === "Array") {
|
|
3308
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3309
|
-
const firstTypeArg = typeArgs[0];
|
|
3310
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3311
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
3312
|
-
}
|
|
3313
|
-
return "Array<unknown>";
|
|
3314
|
-
}
|
|
3315
|
-
if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
|
|
2669
|
+
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
3316
2670
|
return typeNode.getText();
|
|
3317
2671
|
}
|
|
3318
|
-
if (name === "Promise") {
|
|
3319
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3320
|
-
const firstTypeArg = typeArgs[0];
|
|
3321
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3322
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3323
|
-
}
|
|
3324
|
-
return "unknown";
|
|
3325
|
-
}
|
|
3326
2672
|
const resolved = findType(name, sourceFile, project);
|
|
3327
2673
|
if (resolved) {
|
|
3328
2674
|
return expandTypeDecl(resolved, project, depth - 1);
|
|
@@ -3338,6 +2684,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
3338
2684
|
if (kind === SyntaxKind3.AnyKeyword) return "unknown";
|
|
3339
2685
|
return typeNode.getText();
|
|
3340
2686
|
}
|
|
2687
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
2688
|
+
const typeArgs = typeNode.getTypeArguments();
|
|
2689
|
+
const firstTypeArg = typeArgs[0];
|
|
2690
|
+
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2691
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
2692
|
+
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2693
|
+
}
|
|
2694
|
+
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2695
|
+
}
|
|
3341
2696
|
function expandTypeDecl(result, project, depth) {
|
|
3342
2697
|
if (depth < 0) return "unknown";
|
|
3343
2698
|
switch (result.kind) {
|
|
@@ -3403,7 +2758,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
3403
2758
|
const paramArgs = paramDecorator.getArguments();
|
|
3404
2759
|
if (paramArgs.length === 0) continue;
|
|
3405
2760
|
const nameArg = paramArgs[0];
|
|
3406
|
-
if (!
|
|
2761
|
+
if (!Node6.isStringLiteral(nameArg)) continue;
|
|
3407
2762
|
const paramName = nameArg.getLiteralValue();
|
|
3408
2763
|
const typeNode = param.getTypeNode();
|
|
3409
2764
|
const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
|
|
@@ -3416,13 +2771,13 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
3416
2771
|
if (apiResponseDecorator) {
|
|
3417
2772
|
const args = apiResponseDecorator.getArguments();
|
|
3418
2773
|
const optsArg = args[0];
|
|
3419
|
-
if (optsArg &&
|
|
2774
|
+
if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
|
|
3420
2775
|
for (const prop of optsArg.getProperties()) {
|
|
3421
|
-
if (!
|
|
2776
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3422
2777
|
if (prop.getName() !== "type") continue;
|
|
3423
2778
|
const val = prop.getInitializer();
|
|
3424
2779
|
if (!val) continue;
|
|
3425
|
-
if (
|
|
2780
|
+
if (Node6.isArrayLiteralExpression(val)) {
|
|
3426
2781
|
const elements = val.getElements();
|
|
3427
2782
|
const firstEl = elements[0];
|
|
3428
2783
|
if (elements.length > 0 && firstEl !== void 0) {
|
|
@@ -3442,7 +2797,7 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
3442
2797
|
return "unknown";
|
|
3443
2798
|
}
|
|
3444
2799
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
3445
|
-
if (!
|
|
2800
|
+
if (!Node6.isIdentifier(node)) return "unknown";
|
|
3446
2801
|
const name = node.getText();
|
|
3447
2802
|
const resolved = findType(name, sourceFile, project);
|
|
3448
2803
|
if (resolved) {
|
|
@@ -3489,11 +2844,11 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3489
2844
|
if (apiResp) {
|
|
3490
2845
|
const args = apiResp.getArguments();
|
|
3491
2846
|
const optsArg = args[0];
|
|
3492
|
-
if (optsArg &&
|
|
2847
|
+
if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
|
|
3493
2848
|
for (const prop of optsArg.getProperties()) {
|
|
3494
|
-
if (
|
|
2849
|
+
if (Node6.isPropertyAssignment(prop) && prop.getName() === "type") {
|
|
3495
2850
|
const val = prop.getInitializer();
|
|
3496
|
-
if (val &&
|
|
2851
|
+
if (val && Node6.isIdentifier(val)) {
|
|
3497
2852
|
const name = val.getText();
|
|
3498
2853
|
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3499
2854
|
if (localDecl?.isExported()) {
|
|
@@ -3510,27 +2865,18 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3510
2865
|
}
|
|
3511
2866
|
}
|
|
3512
2867
|
}
|
|
3513
|
-
let bodyZodText = null;
|
|
3514
|
-
let queryZodText = null;
|
|
3515
2868
|
let bodySchema = null;
|
|
3516
2869
|
let querySchema = null;
|
|
3517
|
-
const formNested = {};
|
|
3518
2870
|
const formWarnings = [];
|
|
3519
2871
|
const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
|
|
3520
2872
|
if (bodyClass) {
|
|
3521
|
-
const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
|
|
3522
|
-
bodyZodText = result.schemaText;
|
|
3523
|
-
for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
|
|
3524
|
-
formWarnings.push(...result.warnings);
|
|
3525
2873
|
bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
|
|
2874
|
+
formWarnings.push(...bodySchema.warnings);
|
|
3526
2875
|
}
|
|
3527
2876
|
const queryClass = resolveParamClass(method, "Query", sourceFile, project);
|
|
3528
2877
|
if (queryClass) {
|
|
3529
|
-
const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
|
|
3530
|
-
queryZodText = result.schemaText;
|
|
3531
|
-
for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
|
|
3532
|
-
formWarnings.push(...result.warnings);
|
|
3533
2878
|
querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
|
|
2879
|
+
formWarnings.push(...querySchema.warnings);
|
|
3534
2880
|
}
|
|
3535
2881
|
return {
|
|
3536
2882
|
query,
|
|
@@ -3543,9 +2889,6 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3543
2889
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
3544
2890
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
3545
2891
|
filterSource: filterInfo?.source ?? null,
|
|
3546
|
-
bodyZodText,
|
|
3547
|
-
queryZodText,
|
|
3548
|
-
formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
|
|
3549
2892
|
formWarnings,
|
|
3550
2893
|
bodySchema,
|
|
3551
2894
|
querySchema
|
|
@@ -3565,6 +2908,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
|
3565
2908
|
}
|
|
3566
2909
|
return null;
|
|
3567
2910
|
}
|
|
2911
|
+
|
|
2912
|
+
// src/discovery/zod-ast-to-ts.ts
|
|
2913
|
+
import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
2914
|
+
function zodAstToTs(node) {
|
|
2915
|
+
if (!Node7.isCallExpression(node)) return "unknown";
|
|
2916
|
+
const expr = node.getExpression();
|
|
2917
|
+
if (Node7.isPropertyAccessExpression(expr)) {
|
|
2918
|
+
const methodName = expr.getName();
|
|
2919
|
+
const receiver = expr.getExpression();
|
|
2920
|
+
if (methodName === "optional") {
|
|
2921
|
+
return `${zodAstToTs(receiver)} | undefined`;
|
|
2922
|
+
}
|
|
2923
|
+
if (methodName === "nullable") {
|
|
2924
|
+
return `${zodAstToTs(receiver)} | null`;
|
|
2925
|
+
}
|
|
2926
|
+
const args = node.getArguments();
|
|
2927
|
+
switch (methodName) {
|
|
2928
|
+
case "string":
|
|
2929
|
+
return "string";
|
|
2930
|
+
case "number":
|
|
2931
|
+
return "number";
|
|
2932
|
+
case "boolean":
|
|
2933
|
+
return "boolean";
|
|
2934
|
+
case "unknown":
|
|
2935
|
+
return "unknown";
|
|
2936
|
+
case "any":
|
|
2937
|
+
return "unknown";
|
|
2938
|
+
case "literal": {
|
|
2939
|
+
const lit = args[0];
|
|
2940
|
+
if (!lit) return "unknown";
|
|
2941
|
+
if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
2942
|
+
if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
2943
|
+
if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
|
|
2944
|
+
if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
|
|
2945
|
+
return "unknown";
|
|
2946
|
+
}
|
|
2947
|
+
case "enum": {
|
|
2948
|
+
const arrArg = args[0];
|
|
2949
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2950
|
+
const members = arrArg.getElements().map(
|
|
2951
|
+
(el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
2952
|
+
);
|
|
2953
|
+
return members.join(" | ");
|
|
2954
|
+
}
|
|
2955
|
+
case "array": {
|
|
2956
|
+
const inner = args[0];
|
|
2957
|
+
if (!inner) return "unknown";
|
|
2958
|
+
return `Array<${zodAstToTs(inner)}>`;
|
|
2959
|
+
}
|
|
2960
|
+
case "object": {
|
|
2961
|
+
const objArg = args[0];
|
|
2962
|
+
if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
|
|
2963
|
+
const lines = [];
|
|
2964
|
+
for (const prop of objArg.getProperties()) {
|
|
2965
|
+
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
2966
|
+
const key = prop.getName();
|
|
2967
|
+
const valNode = prop.getInitializer();
|
|
2968
|
+
if (!valNode) continue;
|
|
2969
|
+
const tsType = zodAstToTs(valNode);
|
|
2970
|
+
const isOpt = isOptionalChain(valNode);
|
|
2971
|
+
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
2972
|
+
}
|
|
2973
|
+
return `{ ${lines.join("; ")} }`;
|
|
2974
|
+
}
|
|
2975
|
+
case "union": {
|
|
2976
|
+
const arrArg = args[0];
|
|
2977
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2978
|
+
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
2979
|
+
}
|
|
2980
|
+
case "record": {
|
|
2981
|
+
const valArg = args.length === 1 ? args[0] : args[1];
|
|
2982
|
+
if (!valArg) return "unknown";
|
|
2983
|
+
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
2984
|
+
}
|
|
2985
|
+
case "tuple": {
|
|
2986
|
+
const arrArg = args[0];
|
|
2987
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2988
|
+
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
2989
|
+
}
|
|
2990
|
+
default:
|
|
2991
|
+
return "unknown";
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return "unknown";
|
|
2995
|
+
}
|
|
2996
|
+
function isOptionalChain(node) {
|
|
2997
|
+
if (!Node7.isCallExpression(node)) return false;
|
|
2998
|
+
const expr = node.getExpression();
|
|
2999
|
+
return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
3000
|
+
}
|
|
3001
|
+
function parseDefineContractCall(callExpr) {
|
|
3002
|
+
if (!Node7.isCallExpression(callExpr)) return null;
|
|
3003
|
+
const callee = callExpr.getExpression();
|
|
3004
|
+
const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
3005
|
+
if (calleeName !== "defineContract") return null;
|
|
3006
|
+
const args = callExpr.getArguments();
|
|
3007
|
+
const optsArg = args[0];
|
|
3008
|
+
if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
|
|
3009
|
+
let query = null;
|
|
3010
|
+
let body = null;
|
|
3011
|
+
let response = "unknown";
|
|
3012
|
+
let bodyZodText = null;
|
|
3013
|
+
let queryZodText = null;
|
|
3014
|
+
for (const prop of optsArg.getProperties()) {
|
|
3015
|
+
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3016
|
+
const propName = prop.getName();
|
|
3017
|
+
const val = prop.getInitializer();
|
|
3018
|
+
if (!val) continue;
|
|
3019
|
+
if (propName === "query") {
|
|
3020
|
+
query = zodAstToTs(val);
|
|
3021
|
+
queryZodText = val.getText();
|
|
3022
|
+
} else if (propName === "body") {
|
|
3023
|
+
body = zodAstToTs(val);
|
|
3024
|
+
bodyZodText = val.getText();
|
|
3025
|
+
} else if (propName === "response") {
|
|
3026
|
+
response = zodAstToTs(val);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
return { query, body, response, bodyZodText, queryZodText };
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// src/discovery/contracts-fast.ts
|
|
3033
|
+
async function discoverContractsFast(opts) {
|
|
3034
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3035
|
+
const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
|
|
3036
|
+
let project;
|
|
3037
|
+
try {
|
|
3038
|
+
project = new Project3({
|
|
3039
|
+
tsConfigFilePath: tsconfigPath,
|
|
3040
|
+
skipAddingFilesFromTsConfig: true,
|
|
3041
|
+
skipLoadingLibFiles: true,
|
|
3042
|
+
skipFileDependencyResolution: true
|
|
3043
|
+
});
|
|
3044
|
+
} catch {
|
|
3045
|
+
project = new Project3({
|
|
3046
|
+
skipAddingFilesFromTsConfig: true,
|
|
3047
|
+
skipLoadingLibFiles: true,
|
|
3048
|
+
skipFileDependencyResolution: true,
|
|
3049
|
+
compilerOptions: {
|
|
3050
|
+
allowJs: true,
|
|
3051
|
+
resolveJsonModule: false,
|
|
3052
|
+
strict: false
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3057
|
+
for (const f of files) {
|
|
3058
|
+
project.addSourceFileAtPath(f);
|
|
3059
|
+
}
|
|
3060
|
+
const routes = [];
|
|
3061
|
+
setDiscoveryContext(project, {
|
|
3062
|
+
projectRoot: cwd,
|
|
3063
|
+
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3064
|
+
});
|
|
3065
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
3066
|
+
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3067
|
+
}
|
|
3068
|
+
return routes;
|
|
3069
|
+
}
|
|
3070
|
+
function decoratorStringArg(decoratorExpr) {
|
|
3071
|
+
if (!decoratorExpr) return void 0;
|
|
3072
|
+
if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
3073
|
+
if (Node8.isArrayLiteralExpression(decoratorExpr)) {
|
|
3074
|
+
const first = decoratorExpr.getElements()[0];
|
|
3075
|
+
if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
|
|
3076
|
+
}
|
|
3077
|
+
return void 0;
|
|
3078
|
+
}
|
|
3079
|
+
function deriveClassSegment(className) {
|
|
3080
|
+
const noSuffix = className.replace(/Controller$/, "");
|
|
3081
|
+
if (!noSuffix) {
|
|
3082
|
+
throw new Error(
|
|
3083
|
+
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
3087
|
+
}
|
|
3088
|
+
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
3089
|
+
const classPortion = classAs ?? deriveClassSegment(className);
|
|
3090
|
+
const methodPortion = methodAs ?? methodName;
|
|
3091
|
+
return `${classPortion}.${methodPortion}`;
|
|
3092
|
+
}
|
|
3093
|
+
function joinPaths(prefix, suffix) {
|
|
3094
|
+
if (!prefix && !suffix) return "/";
|
|
3095
|
+
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3096
|
+
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
3097
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3098
|
+
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3099
|
+
const combined = p + s;
|
|
3100
|
+
return combined === "" ? "/" : combined;
|
|
3101
|
+
}
|
|
3102
|
+
function extractParams(path) {
|
|
3103
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
3104
|
+
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
3105
|
+
}
|
|
3568
3106
|
var HTTP_METHOD_DECORATORS = {
|
|
3569
3107
|
Get: "GET",
|
|
3570
3108
|
Post: "POST",
|
|
@@ -3575,176 +3113,186 @@ var HTTP_METHOD_DECORATORS = {
|
|
|
3575
3113
|
Head: "HEAD",
|
|
3576
3114
|
All: "ALL"
|
|
3577
3115
|
};
|
|
3116
|
+
function resolveVerb(method) {
|
|
3117
|
+
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
3118
|
+
const httpDecorator = method.getDecorator(decoratorName);
|
|
3119
|
+
if (httpDecorator) {
|
|
3120
|
+
const httpArgs = httpDecorator.getArguments();
|
|
3121
|
+
const pathArg = httpArgs[0];
|
|
3122
|
+
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
return null;
|
|
3126
|
+
}
|
|
3127
|
+
function readAsDecorator(node, label) {
|
|
3128
|
+
const asDecorator = node.getDecorator("As");
|
|
3129
|
+
if (!asDecorator) return void 0;
|
|
3130
|
+
const asName = decoratorStringArg(asDecorator.getArguments()[0]);
|
|
3131
|
+
if (!asName) {
|
|
3132
|
+
throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
|
|
3133
|
+
}
|
|
3134
|
+
return asName;
|
|
3135
|
+
}
|
|
3136
|
+
function buildRoute(args) {
|
|
3137
|
+
const {
|
|
3138
|
+
className,
|
|
3139
|
+
methodName,
|
|
3140
|
+
resolvedMethod,
|
|
3141
|
+
combinedPath,
|
|
3142
|
+
classAs,
|
|
3143
|
+
methodAs,
|
|
3144
|
+
sourceFile,
|
|
3145
|
+
seenNames,
|
|
3146
|
+
contractSource
|
|
3147
|
+
} = args;
|
|
3148
|
+
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3149
|
+
const qualifiedRef = `${className}.${methodName}`;
|
|
3150
|
+
const existing = seenNames.get(routeName);
|
|
3151
|
+
if (existing !== void 0) {
|
|
3152
|
+
throw new Error(
|
|
3153
|
+
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
seenNames.set(routeName, qualifiedRef);
|
|
3157
|
+
return {
|
|
3158
|
+
method: resolvedMethod,
|
|
3159
|
+
path: combinedPath,
|
|
3160
|
+
name: routeName,
|
|
3161
|
+
params: extractParams(combinedPath),
|
|
3162
|
+
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3163
|
+
contract: { contractSource }
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
function extractContractRoute(args) {
|
|
3167
|
+
const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
|
|
3168
|
+
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3169
|
+
if (!firstDecoratorArg) return null;
|
|
3170
|
+
let contractDef = null;
|
|
3171
|
+
let bodyZodRef = null;
|
|
3172
|
+
let queryZodRef = null;
|
|
3173
|
+
if (Node8.isCallExpression(firstDecoratorArg)) {
|
|
3174
|
+
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3175
|
+
} else if (Node8.isIdentifier(firstDecoratorArg)) {
|
|
3176
|
+
const identName = firstDecoratorArg.getText();
|
|
3177
|
+
const varDecl = sourceFile.getVariableDeclaration(identName);
|
|
3178
|
+
if (!varDecl) {
|
|
3179
|
+
console.warn(
|
|
3180
|
+
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
|
|
3181
|
+
);
|
|
3182
|
+
return null;
|
|
3183
|
+
}
|
|
3184
|
+
const initializer = varDecl.getInitializer();
|
|
3185
|
+
if (!initializer) return null;
|
|
3186
|
+
contractDef = parseDefineContractCall(initializer);
|
|
3187
|
+
if (contractDef && varDecl.isExported()) {
|
|
3188
|
+
const filePath = sourceFile.getFilePath();
|
|
3189
|
+
if (contractDef.body !== null) {
|
|
3190
|
+
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3191
|
+
}
|
|
3192
|
+
if (contractDef.query !== null) {
|
|
3193
|
+
queryZodRef = { name: `${identName}.query`, filePath };
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
} else {
|
|
3197
|
+
console.warn(
|
|
3198
|
+
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
3199
|
+
);
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
if (!contractDef) return null;
|
|
3203
|
+
if (!verb) return null;
|
|
3204
|
+
const resolvedPath = joinPaths(prefix, verb.handlerPath);
|
|
3205
|
+
const methodName = method.getName();
|
|
3206
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
3207
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
3208
|
+
return buildRoute({
|
|
3209
|
+
className,
|
|
3210
|
+
methodName,
|
|
3211
|
+
resolvedMethod: verb.httpMethod,
|
|
3212
|
+
combinedPath: resolvedPath,
|
|
3213
|
+
classAs,
|
|
3214
|
+
methodAs,
|
|
3215
|
+
sourceFile,
|
|
3216
|
+
seenNames,
|
|
3217
|
+
contractSource: {
|
|
3218
|
+
query: contractDef.query,
|
|
3219
|
+
body: contractDef.body,
|
|
3220
|
+
response: contractDef.response,
|
|
3221
|
+
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3222
|
+
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3223
|
+
// would drag server-only deps into the client bundle).
|
|
3224
|
+
bodyZodRef,
|
|
3225
|
+
bodyZodText: contractDef.bodyZodText,
|
|
3226
|
+
queryZodRef,
|
|
3227
|
+
queryZodText: contractDef.queryZodText
|
|
3228
|
+
}
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
function extractDtoRoute(args) {
|
|
3232
|
+
const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
|
|
3233
|
+
if (!verb) return null;
|
|
3234
|
+
const combined = joinPaths(prefix, verb.handlerPath);
|
|
3235
|
+
const methodName = method.getName();
|
|
3236
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
3237
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
3238
|
+
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
3239
|
+
return buildRoute({
|
|
3240
|
+
className,
|
|
3241
|
+
methodName,
|
|
3242
|
+
resolvedMethod: verb.httpMethod,
|
|
3243
|
+
combinedPath: combined,
|
|
3244
|
+
classAs,
|
|
3245
|
+
methodAs,
|
|
3246
|
+
sourceFile,
|
|
3247
|
+
seenNames,
|
|
3248
|
+
contractSource: {
|
|
3249
|
+
query: dtoContract?.query ?? null,
|
|
3250
|
+
body: dtoContract?.body ?? null,
|
|
3251
|
+
response: dtoContract?.response ?? "unknown",
|
|
3252
|
+
queryRef: dtoContract?.queryRef ?? null,
|
|
3253
|
+
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3254
|
+
responseRef: dtoContract?.responseRef ?? null,
|
|
3255
|
+
filterFields: dtoContract?.filterFields ?? null,
|
|
3256
|
+
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3257
|
+
filterSource: dtoContract?.filterSource ?? null,
|
|
3258
|
+
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3259
|
+
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3260
|
+
querySchema: dtoContract?.querySchema ?? null
|
|
3261
|
+
}
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3578
3264
|
function extractFromSourceFile(sourceFile, project) {
|
|
3579
3265
|
const routes = [];
|
|
3580
3266
|
const seenNames = /* @__PURE__ */ new Map();
|
|
3581
|
-
const
|
|
3582
|
-
for (const cls of classes) {
|
|
3267
|
+
for (const cls of sourceFile.getClasses()) {
|
|
3583
3268
|
const controllerDecorator = cls.getDecorator("Controller");
|
|
3584
3269
|
if (!controllerDecorator) continue;
|
|
3585
|
-
const
|
|
3586
|
-
const
|
|
3587
|
-
const prefix = decoratorStringArg(firstArg3) ?? "";
|
|
3270
|
+
const firstArg2 = controllerDecorator.getArguments()[0];
|
|
3271
|
+
const prefix = decoratorStringArg(firstArg2) ?? "";
|
|
3588
3272
|
const className = cls.getName() ?? "Unknown";
|
|
3589
3273
|
for (const method of cls.getMethods()) {
|
|
3590
|
-
|
|
3591
|
-
let handlerPath = "";
|
|
3592
|
-
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
3593
|
-
const httpDecorator = method.getDecorator(decoratorName);
|
|
3594
|
-
if (httpDecorator) {
|
|
3595
|
-
httpMethod = verb;
|
|
3596
|
-
const httpArgs = httpDecorator.getArguments();
|
|
3597
|
-
const pathArg = httpArgs[0];
|
|
3598
|
-
handlerPath = decoratorStringArg(pathArg) ?? "";
|
|
3599
|
-
break;
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3274
|
+
const verb = resolveVerb(method);
|
|
3602
3275
|
const applyContractDecorator = method.getDecorator("ApplyContract");
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
contractDef = parseDefineContractCall(initializer);
|
|
3624
|
-
if (contractDef && varDecl.isExported()) {
|
|
3625
|
-
const filePath = sourceFile.getFilePath();
|
|
3626
|
-
if (contractDef.body !== null) {
|
|
3627
|
-
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3628
|
-
}
|
|
3629
|
-
if (contractDef.query !== null) {
|
|
3630
|
-
queryZodRef = { name: `${identName}.query`, filePath };
|
|
3631
|
-
}
|
|
3632
|
-
}
|
|
3633
|
-
} else {
|
|
3634
|
-
console.warn(
|
|
3635
|
-
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
3636
|
-
);
|
|
3637
|
-
continue;
|
|
3638
|
-
}
|
|
3639
|
-
if (!contractDef) continue;
|
|
3640
|
-
if (!httpMethod) continue;
|
|
3641
|
-
const resolvedMethod = httpMethod;
|
|
3642
|
-
const resolvedPath = joinPaths(prefix, handlerPath);
|
|
3643
|
-
const combined = resolvedPath;
|
|
3644
|
-
const params = extractParams(combined);
|
|
3645
|
-
const methodName = method.getName();
|
|
3646
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
3647
|
-
let classAs;
|
|
3648
|
-
if (classAsDecorator) {
|
|
3649
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
3650
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
3651
|
-
if (!classAsName) {
|
|
3652
|
-
throw new Error(
|
|
3653
|
-
`@As decorator on class ${className} must have a non-empty string argument.`
|
|
3654
|
-
);
|
|
3655
|
-
}
|
|
3656
|
-
classAs = classAsName;
|
|
3657
|
-
}
|
|
3658
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
3659
|
-
let methodAs;
|
|
3660
|
-
if (methodAsDecorator) {
|
|
3661
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
3662
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
3663
|
-
if (!methodAsName) {
|
|
3664
|
-
throw new Error(
|
|
3665
|
-
`@As decorator on ${className}.${methodName} must have a non-empty string argument.`
|
|
3666
|
-
);
|
|
3667
|
-
}
|
|
3668
|
-
methodAs = methodAsName;
|
|
3669
|
-
}
|
|
3670
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3671
|
-
const qualifiedRef = `${className}.${methodName}`;
|
|
3672
|
-
const existing = seenNames.get(routeName);
|
|
3673
|
-
if (existing !== void 0) {
|
|
3674
|
-
throw new Error(
|
|
3675
|
-
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
3676
|
-
);
|
|
3677
|
-
}
|
|
3678
|
-
seenNames.set(routeName, qualifiedRef);
|
|
3679
|
-
routes.push({
|
|
3680
|
-
method: resolvedMethod,
|
|
3681
|
-
path: combined,
|
|
3682
|
-
name: routeName,
|
|
3683
|
-
params,
|
|
3684
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3685
|
-
contract: {
|
|
3686
|
-
contractSource: {
|
|
3687
|
-
query: contractDef.query,
|
|
3688
|
-
body: contractDef.body,
|
|
3689
|
-
response: contractDef.response,
|
|
3690
|
-
// Path A: capture both the importable ref and the raw text. The
|
|
3691
|
-
// emitter prefers inlining the text (client-safe — re-exporting from
|
|
3692
|
-
// a controller would drag server-only deps into the client bundle).
|
|
3693
|
-
bodyZodRef,
|
|
3694
|
-
bodyZodText: contractDef.bodyZodText,
|
|
3695
|
-
queryZodRef,
|
|
3696
|
-
queryZodText: contractDef.queryZodText
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
});
|
|
3700
|
-
} else {
|
|
3701
|
-
if (!httpMethod) continue;
|
|
3702
|
-
const combined = joinPaths(prefix, handlerPath);
|
|
3703
|
-
const params = extractParams(combined);
|
|
3704
|
-
const methodName = method.getName();
|
|
3705
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
3706
|
-
let classAs;
|
|
3707
|
-
if (classAsDecorator) {
|
|
3708
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
3709
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
3710
|
-
if (classAsName) classAs = classAsName;
|
|
3711
|
-
}
|
|
3712
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
3713
|
-
let methodAs;
|
|
3714
|
-
if (methodAsDecorator) {
|
|
3715
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
3716
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
3717
|
-
if (methodAsName) methodAs = methodAsName;
|
|
3718
|
-
}
|
|
3719
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3720
|
-
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
3721
|
-
routes.push({
|
|
3722
|
-
method: httpMethod,
|
|
3723
|
-
path: combined,
|
|
3724
|
-
name: routeName,
|
|
3725
|
-
params,
|
|
3726
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3727
|
-
contract: {
|
|
3728
|
-
contractSource: {
|
|
3729
|
-
query: dtoContract?.query ?? null,
|
|
3730
|
-
body: dtoContract?.body ?? null,
|
|
3731
|
-
response: dtoContract?.response ?? "unknown",
|
|
3732
|
-
queryRef: dtoContract?.queryRef ?? null,
|
|
3733
|
-
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3734
|
-
responseRef: dtoContract?.responseRef ?? null,
|
|
3735
|
-
filterFields: dtoContract?.filterFields ?? null,
|
|
3736
|
-
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3737
|
-
filterSource: dtoContract?.filterSource ?? null,
|
|
3738
|
-
bodyZodText: dtoContract?.bodyZodText ?? null,
|
|
3739
|
-
queryZodText: dtoContract?.queryZodText ?? null,
|
|
3740
|
-
formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
|
|
3741
|
-
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3742
|
-
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3743
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
3744
|
-
}
|
|
3745
|
-
}
|
|
3746
|
-
});
|
|
3747
|
-
}
|
|
3276
|
+
const route = applyContractDecorator ? extractContractRoute({
|
|
3277
|
+
cls,
|
|
3278
|
+
method,
|
|
3279
|
+
applyContractDecorator,
|
|
3280
|
+
verb,
|
|
3281
|
+
prefix,
|
|
3282
|
+
className,
|
|
3283
|
+
sourceFile,
|
|
3284
|
+
seenNames
|
|
3285
|
+
}) : extractDtoRoute({
|
|
3286
|
+
cls,
|
|
3287
|
+
method,
|
|
3288
|
+
verb,
|
|
3289
|
+
prefix,
|
|
3290
|
+
className,
|
|
3291
|
+
sourceFile,
|
|
3292
|
+
project,
|
|
3293
|
+
seenNames
|
|
3294
|
+
});
|
|
3295
|
+
if (route) routes.push(route);
|
|
3748
3296
|
}
|
|
3749
3297
|
}
|
|
3750
3298
|
return routes;
|
|
@@ -3753,7 +3301,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3753
3301
|
// src/watch/lock-file.ts
|
|
3754
3302
|
import { open } from "fs/promises";
|
|
3755
3303
|
import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
|
|
3756
|
-
import { join as
|
|
3304
|
+
import { join as join12 } from "path";
|
|
3757
3305
|
var LOCK_FILE = ".watcher.lock";
|
|
3758
3306
|
function isProcessAlive(pid) {
|
|
3759
3307
|
try {
|
|
@@ -3765,7 +3313,7 @@ function isProcessAlive(pid) {
|
|
|
3765
3313
|
}
|
|
3766
3314
|
async function acquireLock(outDir) {
|
|
3767
3315
|
await mkdir8(outDir, { recursive: true });
|
|
3768
|
-
const lockPath =
|
|
3316
|
+
const lockPath = join12(outDir, LOCK_FILE);
|
|
3769
3317
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3770
3318
|
try {
|
|
3771
3319
|
const fd = await open(lockPath, "wx");
|
|
@@ -3805,7 +3353,7 @@ async function watch(config, onChange) {
|
|
|
3805
3353
|
if (lock === null) {
|
|
3806
3354
|
let holderPid = "unknown";
|
|
3807
3355
|
try {
|
|
3808
|
-
const raw = await readFile3(
|
|
3356
|
+
const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3809
3357
|
const data = JSON.parse(raw);
|
|
3810
3358
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3811
3359
|
} catch {
|
|
@@ -3833,7 +3381,7 @@ async function watch(config, onChange) {
|
|
|
3833
3381
|
}
|
|
3834
3382
|
let pagesDebounceTimer;
|
|
3835
3383
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3836
|
-
const pagesWatcher = chokidar.watch(
|
|
3384
|
+
const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
|
|
3837
3385
|
ignoreInitial: true,
|
|
3838
3386
|
persistent: true,
|
|
3839
3387
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3859,7 +3407,7 @@ async function watch(config, onChange) {
|
|
|
3859
3407
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3860
3408
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3861
3409
|
let contractsDebounceTimer;
|
|
3862
|
-
const contractsWatcher = chokidar.watch(
|
|
3410
|
+
const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
|
|
3863
3411
|
ignoreInitial: true,
|
|
3864
3412
|
persistent: true,
|
|
3865
3413
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3889,7 +3437,7 @@ async function watch(config, onChange) {
|
|
|
3889
3437
|
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3890
3438
|
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3891
3439
|
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3892
|
-
const formsWatcher = chokidar.watch(
|
|
3440
|
+
const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
|
|
3893
3441
|
ignoreInitial: true,
|
|
3894
3442
|
persistent: true,
|
|
3895
3443
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3916,7 +3464,7 @@ async function watch(config, onChange) {
|
|
|
3916
3464
|
}
|
|
3917
3465
|
|
|
3918
3466
|
// src/index.ts
|
|
3919
|
-
var VERSION = "0.
|
|
3467
|
+
var VERSION = "0.3.0";
|
|
3920
3468
|
export {
|
|
3921
3469
|
CodegenError,
|
|
3922
3470
|
ConfigError,
|
|
@@ -3932,7 +3480,6 @@ export {
|
|
|
3932
3480
|
loadConfig,
|
|
3933
3481
|
resolveAdapter,
|
|
3934
3482
|
resolveConfig,
|
|
3935
|
-
watch
|
|
3936
|
-
zodAdapter
|
|
3483
|
+
watch
|
|
3937
3484
|
};
|
|
3938
3485
|
//# sourceMappingURL=index.js.map
|