@dudousxd/nestjs-codegen 0.2.1 → 0.4.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 +57 -0
- package/dist/cli/main.cjs +1229 -1612
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1211 -1594
- 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-DA4uySjo.d.cts} +84 -41
- package/dist/{index-BwIRjOQA.d.ts → index-DA4uySjo.d.ts} +84 -41
- package/dist/index.cjs +1073 -1460
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -15
- package/dist/index.d.ts +56 -15
- package/dist/index.js +1043 -1430
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +934 -1365
- 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 +921 -1352
- package/dist/nest/index.js.map +1 -1
- package/package.json +3 -2
package/dist/nest/index.js
CHANGED
|
@@ -32,112 +32,9 @@ var CodegenError = class extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
// src/adapters/zod.ts
|
|
36
|
-
function toObjectKey(name) {
|
|
37
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
38
|
-
}
|
|
39
|
-
function messageSuffix(messageRaw2) {
|
|
40
|
-
return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
|
|
41
|
-
}
|
|
42
|
-
function renderStringChecks(checks) {
|
|
43
|
-
let out = "";
|
|
44
|
-
for (const c of checks) {
|
|
45
|
-
switch (c.check) {
|
|
46
|
-
case "email":
|
|
47
|
-
out += `.email(${messageSuffix(c.messageRaw)})`;
|
|
48
|
-
break;
|
|
49
|
-
case "url":
|
|
50
|
-
out += `.url(${messageSuffix(c.messageRaw)})`;
|
|
51
|
-
break;
|
|
52
|
-
case "uuid":
|
|
53
|
-
out += `.uuid(${messageSuffix(c.messageRaw)})`;
|
|
54
|
-
break;
|
|
55
|
-
case "regex":
|
|
56
|
-
out += `.regex(${c.pattern})`;
|
|
57
|
-
break;
|
|
58
|
-
case "min":
|
|
59
|
-
out += `.min(${c.value})`;
|
|
60
|
-
break;
|
|
61
|
-
case "max":
|
|
62
|
-
out += `.max(${c.value})`;
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
function render(node, ctx) {
|
|
69
|
-
switch (node.kind) {
|
|
70
|
-
case "string":
|
|
71
|
-
return `z.string()${renderStringChecks(node.checks)}`;
|
|
72
|
-
case "number": {
|
|
73
|
-
let out = "z.number()";
|
|
74
|
-
for (const c of node.checks) {
|
|
75
|
-
if (c.check === "int") out += ".int()";
|
|
76
|
-
else if (c.check === "positive") out += ".positive()";
|
|
77
|
-
else if (c.check === "negative") out += ".negative()";
|
|
78
|
-
else if (c.check === "min") out += `.min(${c.value})`;
|
|
79
|
-
else if (c.check === "max") out += `.max(${c.value})`;
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
|
-
case "boolean":
|
|
84
|
-
return "z.boolean()";
|
|
85
|
-
case "date":
|
|
86
|
-
return "z.coerce.date()";
|
|
87
|
-
case "unknown":
|
|
88
|
-
return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
|
|
89
|
-
case "instanceof":
|
|
90
|
-
return `z.instanceof(${node.ctor})`;
|
|
91
|
-
case "enum":
|
|
92
|
-
return `z.enum([${node.literals.join(", ")}])`;
|
|
93
|
-
case "literal":
|
|
94
|
-
return `z.literal(${node.raw})`;
|
|
95
|
-
case "union":
|
|
96
|
-
return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
|
|
97
|
-
case "object": {
|
|
98
|
-
if (node.fields.length === 0) {
|
|
99
|
-
return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
|
|
100
|
-
}
|
|
101
|
-
const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
|
|
102
|
-
return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
|
|
103
|
-
}
|
|
104
|
-
case "array":
|
|
105
|
-
return `z.array(${render(node.element, ctx)})`;
|
|
106
|
-
case "optional":
|
|
107
|
-
return `${render(node.inner, ctx)}.optional()`;
|
|
108
|
-
case "ref":
|
|
109
|
-
return node.name;
|
|
110
|
-
case "lazyRef":
|
|
111
|
-
return `z.lazy(() => ${node.name})`;
|
|
112
|
-
case "annotated": {
|
|
113
|
-
const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
|
|
114
|
-
return `${render(node.inner, ctx)} ${comments}`;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
var zodAdapter = {
|
|
119
|
-
name: "zod",
|
|
120
|
-
importStatements(usage) {
|
|
121
|
-
return usage.used ? ["import { z } from 'zod';"] : [];
|
|
122
|
-
},
|
|
123
|
-
render,
|
|
124
|
-
inferType(schemaConst) {
|
|
125
|
-
return `z.infer<typeof ${schemaConst}>`;
|
|
126
|
-
},
|
|
127
|
-
renderModule(mod) {
|
|
128
|
-
const ctx = { named: mod.named };
|
|
129
|
-
const namedNestedSchemas = /* @__PURE__ */ new Map();
|
|
130
|
-
for (const [name, node] of mod.named) {
|
|
131
|
-
namedNestedSchemas.set(name, render(node, ctx));
|
|
132
|
-
}
|
|
133
|
-
return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
35
|
// src/adapters/registry.ts
|
|
138
36
|
function resolveAdapter(option) {
|
|
139
37
|
if (typeof option !== "string") return option;
|
|
140
|
-
if (option === "zod") return zodAdapter;
|
|
141
38
|
const pkg = `@dudousxd/nestjs-codegen-${option}`;
|
|
142
39
|
const named = `${option}Adapter`;
|
|
143
40
|
throw new ConfigError(
|
|
@@ -167,8 +64,21 @@ If this is intentional, move the file inside your project directory.`
|
|
|
167
64
|
function resolveConfig(userConfig, cwd) {
|
|
168
65
|
return applyDefaults(userConfig, cwd ?? process.cwd());
|
|
169
66
|
}
|
|
67
|
+
function validateUserConfig(userConfig) {
|
|
68
|
+
if (userConfig.validation == null) {
|
|
69
|
+
throw new ConfigError(
|
|
70
|
+
"validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
|
|
74
|
+
throw new ConfigError(
|
|
75
|
+
"Config validation failed: `pages.glob` must be a string when `pages` is set"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
170
79
|
function applyDefaults(userConfig, cwd) {
|
|
171
|
-
|
|
80
|
+
validateUserConfig(userConfig);
|
|
81
|
+
const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
|
|
172
82
|
const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
|
|
173
83
|
let app = null;
|
|
174
84
|
if (userConfig.app) {
|
|
@@ -186,7 +96,8 @@ function applyDefaults(userConfig, cwd) {
|
|
|
186
96
|
}
|
|
187
97
|
return {
|
|
188
98
|
extensions: userConfig.extensions ?? [],
|
|
189
|
-
|
|
99
|
+
// Non-null: validateUserConfig() above throws when `validation` is absent.
|
|
100
|
+
validation: resolveAdapter(userConfig.validation),
|
|
190
101
|
pages: userConfig.pages ? {
|
|
191
102
|
glob: userConfig.pages.glob,
|
|
192
103
|
propsExport: userConfig.pages.propsExport ?? "ComponentProps",
|
|
@@ -213,15 +124,20 @@ function applyDefaults(userConfig, cwd) {
|
|
|
213
124
|
|
|
214
125
|
// src/watch/watcher.ts
|
|
215
126
|
import { readFile as readFile3 } from "fs/promises";
|
|
216
|
-
import { join as
|
|
127
|
+
import { join as join13 } from "path";
|
|
217
128
|
import chokidar from "chokidar";
|
|
218
129
|
|
|
219
130
|
// src/discovery/contracts-fast.ts
|
|
220
131
|
import { join as join2, resolve as resolve3 } from "path";
|
|
221
132
|
import fg from "fast-glob";
|
|
222
133
|
import {
|
|
223
|
-
Node as
|
|
224
|
-
Project
|
|
134
|
+
Node as Node7,
|
|
135
|
+
Project
|
|
136
|
+
} from "ts-morph";
|
|
137
|
+
|
|
138
|
+
// src/discovery/dto-type-resolver.ts
|
|
139
|
+
import {
|
|
140
|
+
Node as Node5,
|
|
225
141
|
SyntaxKind as SyntaxKind2
|
|
226
142
|
} from "ts-morph";
|
|
227
143
|
|
|
@@ -236,20 +152,13 @@ import { dirname, resolve as resolve2 } from "path";
|
|
|
236
152
|
import {
|
|
237
153
|
Node
|
|
238
154
|
} from "ts-morph";
|
|
239
|
-
var
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return prev;
|
|
244
|
-
}
|
|
245
|
-
function restoreDiscoveryContext(ctx) {
|
|
246
|
-
_ctx = ctx;
|
|
155
|
+
var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
|
|
156
|
+
var _ctxByProject = /* @__PURE__ */ new WeakMap();
|
|
157
|
+
function setDiscoveryContext(project, ctx) {
|
|
158
|
+
_ctxByProject.set(project, ctx);
|
|
247
159
|
}
|
|
248
|
-
function
|
|
249
|
-
return
|
|
250
|
-
}
|
|
251
|
-
function _tsconfigPaths() {
|
|
252
|
-
return _ctx.tsconfigPaths;
|
|
160
|
+
function _ctxFor(project) {
|
|
161
|
+
return _ctxByProject.get(project) ?? _EMPTY_CTX;
|
|
253
162
|
}
|
|
254
163
|
var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
|
|
255
164
|
function dbg(...args) {
|
|
@@ -291,7 +200,7 @@ function findTypeInFile(name, file) {
|
|
|
291
200
|
}
|
|
292
201
|
return null;
|
|
293
202
|
}
|
|
294
|
-
function resolveModuleSpecifier(moduleSpecifier, sourceFile,
|
|
203
|
+
function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
295
204
|
if (moduleSpecifier.startsWith(".")) {
|
|
296
205
|
const dir = dirname(sourceFile.getFilePath());
|
|
297
206
|
const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
|
|
@@ -301,8 +210,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
|
|
|
301
210
|
resolve2(dir, moduleSpecifier, "index.ts")
|
|
302
211
|
];
|
|
303
212
|
}
|
|
304
|
-
const
|
|
305
|
-
const
|
|
213
|
+
const ctx = _ctxFor(project);
|
|
214
|
+
const baseUrl = ctx.projectRoot;
|
|
215
|
+
const tsconfigPaths = ctx.tsconfigPaths;
|
|
306
216
|
dbg(
|
|
307
217
|
"resolveModuleSpecifier",
|
|
308
218
|
moduleSpecifier,
|
|
@@ -448,7 +358,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
448
358
|
if (!namedImport) continue;
|
|
449
359
|
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
450
360
|
if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
451
|
-
const tsconfigPaths =
|
|
361
|
+
const tsconfigPaths = _ctxFor(project).tsconfigPaths;
|
|
452
362
|
const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
|
|
453
363
|
const prefix = p.replace("*", "");
|
|
454
364
|
return moduleSpecifier.startsWith(prefix);
|
|
@@ -517,10 +427,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
517
427
|
depth: 0
|
|
518
428
|
};
|
|
519
429
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
520
|
-
|
|
521
|
-
ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
|
|
522
|
-
}
|
|
523
|
-
return { root, named: ctx.named, warnings: ctx.warnings };
|
|
430
|
+
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
524
431
|
}
|
|
525
432
|
function buildObject(classDecl, classFile, ctx) {
|
|
526
433
|
const props = classDecl.getProperties();
|
|
@@ -540,7 +447,7 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
540
447
|
const dec = (n) => decorators.get(n);
|
|
541
448
|
const typeNode = prop.getTypeNode();
|
|
542
449
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
543
|
-
const isArrayType = !!typeNode &&
|
|
450
|
+
const isArrayType = !!typeNode && Node2.isArrayTypeNode(typeNode);
|
|
544
451
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
545
452
|
if (has("ValidateNested") || typeRefName) {
|
|
546
453
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
@@ -671,18 +578,27 @@ function baseFromType(typeText, isArrayType) {
|
|
|
671
578
|
}
|
|
672
579
|
}
|
|
673
580
|
function buildNestedReference(className, fromFile, ctx) {
|
|
674
|
-
if (ctx.visiting.has(className)
|
|
581
|
+
if (ctx.visiting.has(className)) {
|
|
675
582
|
const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
|
|
676
583
|
ctx.emittedClasses.set(className, reserved);
|
|
677
584
|
ctx.recursiveSchemas.add(reserved);
|
|
678
585
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
679
586
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
680
|
-
const msg = `${className} is a recursive type
|
|
587
|
+
const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
|
|
681
588
|
ctx.warnings.push(msg);
|
|
682
589
|
console.warn(`[nestjs-codegen] ${msg}`);
|
|
683
590
|
}
|
|
684
591
|
return { kind: "lazyRef", name: reserved };
|
|
685
592
|
}
|
|
593
|
+
if (ctx.depth >= 8) {
|
|
594
|
+
if (!ctx.warnedDecorators.has(`deep:${className}`)) {
|
|
595
|
+
ctx.warnedDecorators.add(`deep:${className}`);
|
|
596
|
+
const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
|
|
597
|
+
ctx.warnings.push(msg);
|
|
598
|
+
console.warn(`[nestjs-codegen] ${msg}`);
|
|
599
|
+
}
|
|
600
|
+
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
601
|
+
}
|
|
686
602
|
const existing = ctx.emittedClasses.get(className);
|
|
687
603
|
if (existing) return { kind: "ref", name: existing };
|
|
688
604
|
const schemaName = aliasFor(className, ctx);
|
|
@@ -801,344 +717,14 @@ function inSchemaFromDecorator(decorator) {
|
|
|
801
717
|
return null;
|
|
802
718
|
}
|
|
803
719
|
|
|
804
|
-
// src/discovery/dto-to-zod.ts
|
|
805
|
-
import {
|
|
806
|
-
Node as Node3
|
|
807
|
-
} from "ts-morph";
|
|
808
|
-
var KNOWN_DECORATORS2 = /* @__PURE__ */ new Set([
|
|
809
|
-
"IsString",
|
|
810
|
-
"IsNumber",
|
|
811
|
-
"IsInt",
|
|
812
|
-
"IsBoolean",
|
|
813
|
-
"IsDate",
|
|
814
|
-
"IsEmail",
|
|
815
|
-
"IsUrl",
|
|
816
|
-
"IsUUID",
|
|
817
|
-
"MinLength",
|
|
818
|
-
"MaxLength",
|
|
819
|
-
"Length",
|
|
820
|
-
"Min",
|
|
821
|
-
"Max",
|
|
822
|
-
"IsPositive",
|
|
823
|
-
"IsNegative",
|
|
824
|
-
"Matches",
|
|
825
|
-
"IsEnum",
|
|
826
|
-
"IsIn",
|
|
827
|
-
"IsOptional",
|
|
828
|
-
"IsNotEmpty",
|
|
829
|
-
"IsArray",
|
|
830
|
-
"ValidateNested",
|
|
831
|
-
"Type",
|
|
832
|
-
"IsObject",
|
|
833
|
-
"Allow",
|
|
834
|
-
"IsDefined"
|
|
835
|
-
]);
|
|
836
|
-
function extractZodFromDto(classDecl, sourceFile, project) {
|
|
837
|
-
const ctx = {
|
|
838
|
-
sourceFile,
|
|
839
|
-
project,
|
|
840
|
-
namedNestedSchemas: /* @__PURE__ */ new Map(),
|
|
841
|
-
warnings: [],
|
|
842
|
-
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
843
|
-
emittedClasses: /* @__PURE__ */ new Map(),
|
|
844
|
-
visiting: /* @__PURE__ */ new Set(),
|
|
845
|
-
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
846
|
-
depth: 0
|
|
847
|
-
};
|
|
848
|
-
const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
|
|
849
|
-
for (const schemaName of ctx.recursiveSchemas) {
|
|
850
|
-
ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
|
|
851
|
-
}
|
|
852
|
-
return {
|
|
853
|
-
schemaText,
|
|
854
|
-
namedNestedSchemas: ctx.namedNestedSchemas,
|
|
855
|
-
warnings: ctx.warnings
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
function buildObjectSchema(classDecl, classFile, ctx) {
|
|
859
|
-
const props = classDecl.getProperties();
|
|
860
|
-
if (props.length === 0) {
|
|
861
|
-
return "z.object({}).passthrough()";
|
|
862
|
-
}
|
|
863
|
-
const fields = [];
|
|
864
|
-
for (const prop of props) {
|
|
865
|
-
const name = prop.getName();
|
|
866
|
-
const expr = buildPropertySchema(prop, classFile, ctx);
|
|
867
|
-
fields.push(`${toObjectKey2(name)}: ${expr}`);
|
|
868
|
-
}
|
|
869
|
-
return `z.object({ ${fields.join(", ")} })`;
|
|
870
|
-
}
|
|
871
|
-
function toObjectKey2(name) {
|
|
872
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
873
|
-
}
|
|
874
|
-
function buildPropertySchema(prop, classFile, ctx) {
|
|
875
|
-
const decorators = /* @__PURE__ */ new Map();
|
|
876
|
-
for (const d of prop.getDecorators()) decorators.set(d.getName(), d);
|
|
877
|
-
const has = (n) => decorators.has(n);
|
|
878
|
-
const dec = (n) => decorators.get(n);
|
|
879
|
-
const typeNode = prop.getTypeNode();
|
|
880
|
-
const typeText = typeNode?.getText() ?? "unknown";
|
|
881
|
-
const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
|
|
882
|
-
const comments = [];
|
|
883
|
-
const typeRefName = resolveTypeFactoryName2(dec("Type"));
|
|
884
|
-
if (has("ValidateNested") || typeRefName) {
|
|
885
|
-
const childName = typeRefName ?? singularClassName2(typeText);
|
|
886
|
-
if (childName) {
|
|
887
|
-
const childExpr = buildNestedReference2(childName, classFile, ctx);
|
|
888
|
-
const wrapArray = has("IsArray") || isArrayType;
|
|
889
|
-
let expr2 = wrapArray ? `z.array(${childExpr})` : childExpr;
|
|
890
|
-
expr2 = applyPresence2(expr2, decorators);
|
|
891
|
-
return expr2;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
let base = baseFromType2(typeText, isArrayType, ctx, classFile);
|
|
895
|
-
const refinements = [];
|
|
896
|
-
if (has("IsString")) base = "z.string()";
|
|
897
|
-
if (has("IsBoolean")) base = "z.boolean()";
|
|
898
|
-
if (has("IsDate")) base = "z.coerce.date()";
|
|
899
|
-
if (has("IsNumber")) base = "z.number()";
|
|
900
|
-
if (has("IsInt")) base = "z.number().int()";
|
|
901
|
-
if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
|
|
902
|
-
if (has("Allow")) base = "z.unknown()";
|
|
903
|
-
if (has("IsEmail")) {
|
|
904
|
-
base = ensureStringBase(base);
|
|
905
|
-
refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
|
|
906
|
-
}
|
|
907
|
-
if (has("IsUrl")) {
|
|
908
|
-
base = ensureStringBase(base);
|
|
909
|
-
refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
|
|
910
|
-
}
|
|
911
|
-
if (has("IsUUID")) {
|
|
912
|
-
base = ensureStringBase(base);
|
|
913
|
-
refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
|
|
914
|
-
}
|
|
915
|
-
if (has("Matches")) {
|
|
916
|
-
const re = firstArgText2(dec("Matches"));
|
|
917
|
-
if (re) {
|
|
918
|
-
base = ensureStringBase(base);
|
|
919
|
-
refinements.push(`.regex(${re})`);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
if (has("MinLength")) {
|
|
923
|
-
const n = numericArg2(dec("MinLength"));
|
|
924
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
925
|
-
}
|
|
926
|
-
if (has("MaxLength")) {
|
|
927
|
-
const n = numericArg2(dec("MaxLength"));
|
|
928
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
929
|
-
}
|
|
930
|
-
if (has("Length")) {
|
|
931
|
-
const [min, max] = numericArgs2(dec("Length"));
|
|
932
|
-
if (min !== null) refinements.push(`.min(${min})`);
|
|
933
|
-
if (max !== null) refinements.push(`.max(${max})`);
|
|
934
|
-
}
|
|
935
|
-
if (has("Min")) {
|
|
936
|
-
const n = numericArg2(dec("Min"));
|
|
937
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
938
|
-
}
|
|
939
|
-
if (has("Max")) {
|
|
940
|
-
const n = numericArg2(dec("Max"));
|
|
941
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
942
|
-
}
|
|
943
|
-
if (has("IsPositive")) refinements.push(".positive()");
|
|
944
|
-
if (has("IsNegative")) refinements.push(".negative()");
|
|
945
|
-
if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
|
|
946
|
-
if (has("IsEnum")) {
|
|
947
|
-
const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
|
|
948
|
-
if (enumExpr) base = enumExpr;
|
|
949
|
-
}
|
|
950
|
-
if (has("IsIn")) {
|
|
951
|
-
const inExpr = inSchemaFromDecorator2(dec("IsIn"));
|
|
952
|
-
if (inExpr) base = inExpr;
|
|
953
|
-
}
|
|
954
|
-
for (const name of decorators.keys()) {
|
|
955
|
-
if (!KNOWN_DECORATORS2.has(name)) {
|
|
956
|
-
comments.push(`/* @${name}: not translatable to zod (server-only) */`);
|
|
957
|
-
if (!ctx.warnedDecorators.has(name)) {
|
|
958
|
-
ctx.warnedDecorators.add(name);
|
|
959
|
-
const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
|
|
960
|
-
ctx.warnings.push(msg);
|
|
961
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
let expr = base + refinements.join("");
|
|
966
|
-
if (isArrayType && !expr.startsWith("z.array(")) {
|
|
967
|
-
expr = `z.array(${expr})`;
|
|
968
|
-
}
|
|
969
|
-
expr = applyPresence2(expr, decorators);
|
|
970
|
-
if (comments.length > 0) {
|
|
971
|
-
expr = `${expr} ${comments.join(" ")}`;
|
|
972
|
-
}
|
|
973
|
-
return expr;
|
|
974
|
-
}
|
|
975
|
-
function applyPresence2(expr, decorators) {
|
|
976
|
-
if (decorators.has("IsDefined")) return expr;
|
|
977
|
-
if (decorators.has("IsOptional")) return `${expr}.optional()`;
|
|
978
|
-
return expr;
|
|
979
|
-
}
|
|
980
|
-
function baseFromType2(typeText, isArrayType, ctx, classFile) {
|
|
981
|
-
const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
|
|
982
|
-
switch (inner) {
|
|
983
|
-
case "string":
|
|
984
|
-
return "z.string()";
|
|
985
|
-
case "number":
|
|
986
|
-
return "z.number()";
|
|
987
|
-
case "boolean":
|
|
988
|
-
return "z.boolean()";
|
|
989
|
-
case "Date":
|
|
990
|
-
return "z.coerce.date()";
|
|
991
|
-
case "File":
|
|
992
|
-
case "Express.Multer.File":
|
|
993
|
-
return "z.instanceof(File)";
|
|
994
|
-
default:
|
|
995
|
-
return "z.unknown()";
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
function ensureStringBase(base) {
|
|
999
|
-
return isStringBase(base) ? base : "z.string()";
|
|
1000
|
-
}
|
|
1001
|
-
function isStringBase(base) {
|
|
1002
|
-
return base.startsWith("z.string(");
|
|
1003
|
-
}
|
|
1004
|
-
function buildNestedReference2(className, fromFile, ctx) {
|
|
1005
|
-
if (ctx.visiting.has(className) || ctx.depth >= 8) {
|
|
1006
|
-
const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
|
|
1007
|
-
ctx.emittedClasses.set(className, reserved);
|
|
1008
|
-
ctx.recursiveSchemas.add(reserved);
|
|
1009
|
-
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
1010
|
-
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
1011
|
-
const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
|
|
1012
|
-
ctx.warnings.push(msg);
|
|
1013
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
1014
|
-
}
|
|
1015
|
-
return `z.lazy(() => ${reserved})`;
|
|
1016
|
-
}
|
|
1017
|
-
const existing = ctx.emittedClasses.get(className);
|
|
1018
|
-
if (existing) return existing;
|
|
1019
|
-
const schemaName = aliasFor2(className, ctx);
|
|
1020
|
-
const resolved = findType(className, fromFile, ctx.project);
|
|
1021
|
-
if (!resolved || resolved.kind !== "class") {
|
|
1022
|
-
return "z.object({}).passthrough()";
|
|
1023
|
-
}
|
|
1024
|
-
ctx.emittedClasses.set(className, schemaName);
|
|
1025
|
-
ctx.visiting.add(className);
|
|
1026
|
-
ctx.depth += 1;
|
|
1027
|
-
const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
|
|
1028
|
-
ctx.depth -= 1;
|
|
1029
|
-
ctx.visiting.delete(className);
|
|
1030
|
-
ctx.namedNestedSchemas.set(schemaName, childText);
|
|
1031
|
-
return schemaName;
|
|
1032
|
-
}
|
|
1033
|
-
function aliasFor2(className, ctx) {
|
|
1034
|
-
const baseName = `${className}Schema`;
|
|
1035
|
-
let candidate = baseName;
|
|
1036
|
-
let i = 1;
|
|
1037
|
-
const used = new Set(ctx.namedNestedSchemas.keys());
|
|
1038
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
1039
|
-
while (used.has(candidate)) {
|
|
1040
|
-
candidate = `${baseName}_${i}`;
|
|
1041
|
-
i += 1;
|
|
1042
|
-
}
|
|
1043
|
-
return candidate;
|
|
1044
|
-
}
|
|
1045
|
-
function firstArg2(decorator) {
|
|
1046
|
-
return decorator?.getArguments()[0];
|
|
1047
|
-
}
|
|
1048
|
-
function firstArgText2(decorator) {
|
|
1049
|
-
const arg = firstArg2(decorator);
|
|
1050
|
-
return arg ? arg.getText() : null;
|
|
1051
|
-
}
|
|
1052
|
-
function numericArg2(decorator) {
|
|
1053
|
-
const arg = firstArg2(decorator);
|
|
1054
|
-
if (arg && Node3.isNumericLiteral(arg)) return arg.getText();
|
|
1055
|
-
return null;
|
|
1056
|
-
}
|
|
1057
|
-
function numericArgs2(decorator) {
|
|
1058
|
-
const args = decorator?.getArguments() ?? [];
|
|
1059
|
-
const num = (n) => n && Node3.isNumericLiteral(n) ? n.getText() : null;
|
|
1060
|
-
return [num(args[0]), num(args[1])];
|
|
1061
|
-
}
|
|
1062
|
-
function messageArg(decorator) {
|
|
1063
|
-
const args = decorator?.getArguments() ?? [];
|
|
1064
|
-
for (const arg of args) {
|
|
1065
|
-
if (Node3.isObjectLiteralExpression(arg)) {
|
|
1066
|
-
for (const prop of arg.getProperties()) {
|
|
1067
|
-
if (Node3.isPropertyAssignment(prop) && prop.getName() === "message") {
|
|
1068
|
-
const init = prop.getInitializer();
|
|
1069
|
-
if (init && Node3.isStringLiteral(init)) {
|
|
1070
|
-
return `{ message: ${init.getText()} }`;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
return "";
|
|
1077
|
-
}
|
|
1078
|
-
function resolveTypeFactoryName2(decorator) {
|
|
1079
|
-
const arg = firstArg2(decorator);
|
|
1080
|
-
if (!arg) return null;
|
|
1081
|
-
if (Node3.isArrowFunction(arg)) {
|
|
1082
|
-
const body = arg.getBody();
|
|
1083
|
-
if (Node3.isIdentifier(body)) return body.getText();
|
|
1084
|
-
}
|
|
1085
|
-
return null;
|
|
1086
|
-
}
|
|
1087
|
-
function singularClassName2(typeText) {
|
|
1088
|
-
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
1089
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
1090
|
-
}
|
|
1091
|
-
function enumSchemaFromDecorator2(decorator, classFile, ctx) {
|
|
1092
|
-
const arg = firstArg2(decorator);
|
|
1093
|
-
if (!arg) return null;
|
|
1094
|
-
if (Node3.isIdentifier(arg)) {
|
|
1095
|
-
const name = arg.getText();
|
|
1096
|
-
const resolved = findType(name, classFile, ctx.project);
|
|
1097
|
-
if (resolved && resolved.kind === "enum") {
|
|
1098
|
-
return `z.enum([${resolved.members.join(", ")}])`;
|
|
1099
|
-
}
|
|
1100
|
-
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().`;
|
|
1101
|
-
if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
|
|
1102
|
-
ctx.warnedDecorators.add(`IsEnum:${name}`);
|
|
1103
|
-
ctx.warnings.push(msg);
|
|
1104
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
1105
|
-
}
|
|
1106
|
-
return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
|
|
1107
|
-
}
|
|
1108
|
-
if (Node3.isObjectLiteralExpression(arg)) {
|
|
1109
|
-
const values = [];
|
|
1110
|
-
for (const p of arg.getProperties()) {
|
|
1111
|
-
if (!Node3.isPropertyAssignment(p)) continue;
|
|
1112
|
-
const init = p.getInitializer();
|
|
1113
|
-
if (init && Node3.isStringLiteral(init)) values.push(init.getText());
|
|
1114
|
-
}
|
|
1115
|
-
if (values.length > 0) return `z.enum([${values.join(", ")}])`;
|
|
1116
|
-
}
|
|
1117
|
-
return null;
|
|
1118
|
-
}
|
|
1119
|
-
function inSchemaFromDecorator2(decorator) {
|
|
1120
|
-
const arg = firstArg2(decorator);
|
|
1121
|
-
if (arg && Node3.isArrayLiteralExpression(arg)) {
|
|
1122
|
-
const elements = arg.getElements();
|
|
1123
|
-
const allStrings = elements.every((e) => Node3.isStringLiteral(e));
|
|
1124
|
-
if (allStrings && elements.length > 0) {
|
|
1125
|
-
return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
|
|
1126
|
-
}
|
|
1127
|
-
if (elements.length > 0) {
|
|
1128
|
-
return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
return null;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
720
|
// src/discovery/filter-for.ts
|
|
1135
721
|
import {
|
|
1136
|
-
Node as
|
|
722
|
+
Node as Node4
|
|
1137
723
|
} from "ts-morph";
|
|
1138
724
|
|
|
1139
725
|
// src/discovery/filter-field-types.ts
|
|
1140
726
|
import {
|
|
1141
|
-
Node as
|
|
727
|
+
Node as Node3,
|
|
1142
728
|
SyntaxKind
|
|
1143
729
|
} from "ts-morph";
|
|
1144
730
|
|
|
@@ -1175,7 +761,7 @@ function markNullable(r, nullable) {
|
|
|
1175
761
|
return nullable ? { ...r, nullable: true } : r;
|
|
1176
762
|
}
|
|
1177
763
|
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
1178
|
-
if (
|
|
764
|
+
if (Node3.isUnionTypeNode(typeNode)) {
|
|
1179
765
|
let nullable = false;
|
|
1180
766
|
const stringLits = [];
|
|
1181
767
|
const numberLits = [];
|
|
@@ -1186,13 +772,13 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
1186
772
|
nullable = true;
|
|
1187
773
|
continue;
|
|
1188
774
|
}
|
|
1189
|
-
if (
|
|
775
|
+
if (Node3.isLiteralTypeNode(member)) {
|
|
1190
776
|
const lit = member.getLiteral();
|
|
1191
|
-
if (
|
|
777
|
+
if (Node3.isStringLiteral(lit)) {
|
|
1192
778
|
stringLits.push(lit.getLiteralValue());
|
|
1193
779
|
continue;
|
|
1194
780
|
}
|
|
1195
|
-
if (
|
|
781
|
+
if (Node3.isNumericLiteral(lit)) {
|
|
1196
782
|
numberLits.push(lit.getText());
|
|
1197
783
|
continue;
|
|
1198
784
|
}
|
|
@@ -1228,7 +814,7 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
1228
814
|
default:
|
|
1229
815
|
break;
|
|
1230
816
|
}
|
|
1231
|
-
if (
|
|
817
|
+
if (Node3.isTypeReference(typeNode)) {
|
|
1232
818
|
const refName = typeNode.getTypeName().getText();
|
|
1233
819
|
if (refName === "Date") return { kind: "date" };
|
|
1234
820
|
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
@@ -1241,25 +827,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
1241
827
|
if (typeRef) return { kind: "unknown", typeRef };
|
|
1242
828
|
return { kind: "unknown" };
|
|
1243
829
|
}
|
|
1244
|
-
if (
|
|
830
|
+
if (Node3.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
1245
831
|
return { kind: "unknown" };
|
|
1246
832
|
}
|
|
1247
833
|
function enumFromDecoratorArgs(args, sourceFile, project) {
|
|
1248
834
|
for (const arg of args) {
|
|
1249
|
-
if (
|
|
835
|
+
if (Node3.isArrowFunction(arg)) {
|
|
1250
836
|
const body = arg.getBody();
|
|
1251
|
-
if (
|
|
837
|
+
if (Node3.isIdentifier(body)) {
|
|
1252
838
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
1253
839
|
if (en) return en;
|
|
1254
840
|
}
|
|
1255
841
|
}
|
|
1256
|
-
if (
|
|
842
|
+
if (Node3.isObjectLiteralExpression(arg)) {
|
|
1257
843
|
const itemsProp = arg.getProperty("items");
|
|
1258
|
-
if (itemsProp &&
|
|
844
|
+
if (itemsProp && Node3.isPropertyAssignment(itemsProp)) {
|
|
1259
845
|
const init = itemsProp.getInitializer();
|
|
1260
|
-
if (init &&
|
|
846
|
+
if (init && Node3.isArrowFunction(init)) {
|
|
1261
847
|
const body = init.getBody();
|
|
1262
|
-
if (
|
|
848
|
+
if (Node3.isIdentifier(body)) {
|
|
1263
849
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
1264
850
|
if (en) return en;
|
|
1265
851
|
}
|
|
@@ -1282,7 +868,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1282
868
|
return { kind: "string" };
|
|
1283
869
|
}
|
|
1284
870
|
for (const arg of args) {
|
|
1285
|
-
if (
|
|
871
|
+
if (Node3.isStringLiteral(arg)) {
|
|
1286
872
|
const raw = arg.getLiteralValue();
|
|
1287
873
|
const kind = classifyTypeKeyword(raw);
|
|
1288
874
|
if (kind) {
|
|
@@ -1290,11 +876,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1290
876
|
return { kind };
|
|
1291
877
|
}
|
|
1292
878
|
}
|
|
1293
|
-
if (
|
|
879
|
+
if (Node3.isObjectLiteralExpression(arg)) {
|
|
1294
880
|
const enumProp = arg.getProperty("enum");
|
|
1295
|
-
if (enumProp &&
|
|
881
|
+
if (enumProp && Node3.isPropertyAssignment(enumProp)) {
|
|
1296
882
|
const init = enumProp.getInitializer();
|
|
1297
|
-
if (init &&
|
|
883
|
+
if (init && Node3.isIdentifier(init)) {
|
|
1298
884
|
const en = resolveEnumValues(init.getText(), sourceFile, project);
|
|
1299
885
|
if (en) {
|
|
1300
886
|
return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
@@ -1303,9 +889,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1303
889
|
}
|
|
1304
890
|
}
|
|
1305
891
|
const typeProp = arg.getProperty("type");
|
|
1306
|
-
if (typeProp &&
|
|
892
|
+
if (typeProp && Node3.isPropertyAssignment(typeProp)) {
|
|
1307
893
|
const init = typeProp.getInitializer();
|
|
1308
|
-
if (init &&
|
|
894
|
+
if (init && Node3.isStringLiteral(init)) {
|
|
1309
895
|
const kind = classifyTypeKeyword(init.getLiteralValue());
|
|
1310
896
|
if (kind) return { kind };
|
|
1311
897
|
}
|
|
@@ -1341,7 +927,7 @@ function toFilterFieldType(name, r) {
|
|
|
1341
927
|
|
|
1342
928
|
// src/discovery/filter-for.ts
|
|
1343
929
|
function classifyFilterForHint(typeInit) {
|
|
1344
|
-
if (
|
|
930
|
+
if (Node4.isStringLiteral(typeInit)) {
|
|
1345
931
|
switch (typeInit.getLiteralValue()) {
|
|
1346
932
|
case "string":
|
|
1347
933
|
return { kind: "string" };
|
|
@@ -1355,10 +941,10 @@ function classifyFilterForHint(typeInit) {
|
|
|
1355
941
|
return null;
|
|
1356
942
|
}
|
|
1357
943
|
}
|
|
1358
|
-
if (
|
|
944
|
+
if (Node4.isArrayLiteralExpression(typeInit)) {
|
|
1359
945
|
const values = [];
|
|
1360
946
|
for (const el of typeInit.getElements()) {
|
|
1361
|
-
if (!
|
|
947
|
+
if (!Node4.isStringLiteral(el)) return null;
|
|
1362
948
|
values.push(el.getLiteralValue());
|
|
1363
949
|
}
|
|
1364
950
|
if (values.length === 0) return null;
|
|
@@ -1393,11 +979,11 @@ function extractFilterForHints(classDecl, project) {
|
|
|
1393
979
|
if (!filterForDec) continue;
|
|
1394
980
|
const args = filterForDec.getArguments();
|
|
1395
981
|
const keyArg = args[0];
|
|
1396
|
-
const inputKey = keyArg &&
|
|
982
|
+
const inputKey = keyArg && Node4.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
|
|
1397
983
|
const optsArg = args[1];
|
|
1398
|
-
if (optsArg &&
|
|
984
|
+
if (optsArg && Node4.isObjectLiteralExpression(optsArg)) {
|
|
1399
985
|
const typeProp = optsArg.getProperty("type");
|
|
1400
|
-
if (typeProp &&
|
|
986
|
+
if (typeProp && Node4.isPropertyAssignment(typeProp)) {
|
|
1401
987
|
const typeInit = typeProp.getInitializer();
|
|
1402
988
|
if (typeInit) {
|
|
1403
989
|
const classified = classifyFilterForHint(typeInit);
|
|
@@ -1420,14 +1006,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
|
|
|
1420
1006
|
const args = filterDecorator.getArguments();
|
|
1421
1007
|
if (args.length === 0) continue;
|
|
1422
1008
|
const filterClassArg = args[0];
|
|
1423
|
-
if (!filterClassArg || !
|
|
1009
|
+
if (!filterClassArg || !Node4.isIdentifier(filterClassArg)) continue;
|
|
1424
1010
|
let source = "query";
|
|
1425
1011
|
const optionsArg = args[1];
|
|
1426
|
-
if (optionsArg &&
|
|
1012
|
+
if (optionsArg && Node4.isObjectLiteralExpression(optionsArg)) {
|
|
1427
1013
|
const sourceProp = optionsArg.getProperty("source");
|
|
1428
|
-
if (sourceProp &&
|
|
1014
|
+
if (sourceProp && Node4.isPropertyAssignment(sourceProp)) {
|
|
1429
1015
|
const init = sourceProp.getInitializer();
|
|
1430
|
-
if (init &&
|
|
1016
|
+
if (init && Node4.isStringLiteral(init) && init.getLiteralValue() === "body") {
|
|
1431
1017
|
source = "body";
|
|
1432
1018
|
}
|
|
1433
1019
|
}
|
|
@@ -1490,22 +1076,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
|
|
|
1490
1076
|
const args = dec.getArguments();
|
|
1491
1077
|
if (args.length === 0) continue;
|
|
1492
1078
|
const arg = args[0];
|
|
1493
|
-
if (
|
|
1079
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
1494
1080
|
const entityProp = arg.getProperty("entity");
|
|
1495
|
-
if (entityProp &&
|
|
1081
|
+
if (entityProp && Node4.isPropertyAssignment(entityProp)) {
|
|
1496
1082
|
const init = entityProp.getInitializer();
|
|
1497
|
-
if (init &&
|
|
1083
|
+
if (init && Node4.isArrowFunction(init)) {
|
|
1498
1084
|
const body = init.getBody();
|
|
1499
|
-
if (
|
|
1085
|
+
if (Node4.isIdentifier(body)) {
|
|
1500
1086
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
1501
1087
|
if (resolved?.kind === "class") return resolved.decl;
|
|
1502
1088
|
}
|
|
1503
1089
|
}
|
|
1504
1090
|
}
|
|
1505
1091
|
}
|
|
1506
|
-
if (
|
|
1092
|
+
if (Node4.isArrowFunction(arg)) {
|
|
1507
1093
|
const body = arg.getBody();
|
|
1508
|
-
if (
|
|
1094
|
+
if (Node4.isIdentifier(body)) {
|
|
1509
1095
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
1510
1096
|
if (resolved?.kind === "class") return resolved.decl;
|
|
1511
1097
|
}
|
|
@@ -1529,11 +1115,11 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1529
1115
|
const args = filterableDecorator.getArguments();
|
|
1530
1116
|
if (args.length === 0) return [];
|
|
1531
1117
|
const optionsArg = args[0];
|
|
1532
|
-
if (!
|
|
1118
|
+
if (!Node4.isObjectLiteralExpression(optionsArg)) return [];
|
|
1533
1119
|
const entityProp = optionsArg.getProperty("entity");
|
|
1534
|
-
if (!entityProp || !
|
|
1120
|
+
if (!entityProp || !Node4.isPropertyAssignment(entityProp)) return [];
|
|
1535
1121
|
const entityInit = entityProp.getInitializer();
|
|
1536
|
-
if (!entityInit || !
|
|
1122
|
+
if (!entityInit || !Node4.isIdentifier(entityInit)) return [];
|
|
1537
1123
|
const entityName = entityInit.getText();
|
|
1538
1124
|
const filterSourceFile = filterClass.getSourceFile();
|
|
1539
1125
|
const resolvedEntity = findType(entityName, filterSourceFile, project);
|
|
@@ -1549,17 +1135,17 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1549
1135
|
const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
|
|
1550
1136
|
if (relationsDecorator) {
|
|
1551
1137
|
const relArgs = relationsDecorator.getArguments();
|
|
1552
|
-
if (relArgs.length > 0 &&
|
|
1138
|
+
if (relArgs.length > 0 && Node4.isObjectLiteralExpression(relArgs[0])) {
|
|
1553
1139
|
for (const relProp of relArgs[0].getProperties()) {
|
|
1554
|
-
if (!
|
|
1140
|
+
if (!Node4.isPropertyAssignment(relProp)) continue;
|
|
1555
1141
|
const relInit = relProp.getInitializer();
|
|
1556
|
-
if (!relInit || !
|
|
1142
|
+
if (!relInit || !Node4.isObjectLiteralExpression(relInit)) continue;
|
|
1557
1143
|
const keysProp = relInit.getProperty("keys");
|
|
1558
|
-
if (!keysProp || !
|
|
1144
|
+
if (!keysProp || !Node4.isPropertyAssignment(keysProp)) continue;
|
|
1559
1145
|
const keysInit = keysProp.getInitializer();
|
|
1560
|
-
if (!keysInit || !
|
|
1146
|
+
if (!keysInit || !Node4.isArrayLiteralExpression(keysInit)) continue;
|
|
1561
1147
|
for (const el of keysInit.getElements()) {
|
|
1562
|
-
if (
|
|
1148
|
+
if (Node4.isStringLiteral(el)) {
|
|
1563
1149
|
fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
|
|
1564
1150
|
}
|
|
1565
1151
|
}
|
|
@@ -1569,267 +1155,65 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1569
1155
|
return fields;
|
|
1570
1156
|
}
|
|
1571
1157
|
|
|
1572
|
-
// src/discovery/
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
const prevCtx = setDiscoveryContext({
|
|
1602
|
-
projectRoot: cwd,
|
|
1603
|
-
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
1604
|
-
});
|
|
1605
|
-
try {
|
|
1606
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
1607
|
-
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
1608
|
-
}
|
|
1609
|
-
} finally {
|
|
1610
|
-
restoreDiscoveryContext(prevCtx);
|
|
1611
|
-
}
|
|
1612
|
-
return routes;
|
|
1613
|
-
}
|
|
1614
|
-
function zodAstToTs(node) {
|
|
1615
|
-
if (!Node6.isCallExpression(node)) return "unknown";
|
|
1616
|
-
const expr = node.getExpression();
|
|
1617
|
-
if (Node6.isPropertyAccessExpression(expr)) {
|
|
1618
|
-
const methodName = expr.getName();
|
|
1619
|
-
const receiver = expr.getExpression();
|
|
1620
|
-
if (methodName === "optional") {
|
|
1621
|
-
return `${zodAstToTs(receiver)} | undefined`;
|
|
1622
|
-
}
|
|
1623
|
-
if (methodName === "nullable") {
|
|
1624
|
-
return `${zodAstToTs(receiver)} | null`;
|
|
1625
|
-
}
|
|
1626
|
-
const args = node.getArguments();
|
|
1627
|
-
switch (methodName) {
|
|
1628
|
-
case "string":
|
|
1629
|
-
return "string";
|
|
1630
|
-
case "number":
|
|
1631
|
-
return "number";
|
|
1632
|
-
case "boolean":
|
|
1633
|
-
return "boolean";
|
|
1634
|
-
case "unknown":
|
|
1635
|
-
return "unknown";
|
|
1636
|
-
case "any":
|
|
1637
|
-
return "unknown";
|
|
1638
|
-
case "literal": {
|
|
1639
|
-
const lit = args[0];
|
|
1640
|
-
if (!lit) return "unknown";
|
|
1641
|
-
if (Node6.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
1642
|
-
if (Node6.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
1643
|
-
if (lit.getKind() === SyntaxKind2.TrueKeyword) return "true";
|
|
1644
|
-
if (lit.getKind() === SyntaxKind2.FalseKeyword) return "false";
|
|
1645
|
-
return "unknown";
|
|
1646
|
-
}
|
|
1647
|
-
case "enum": {
|
|
1648
|
-
const arrArg = args[0];
|
|
1649
|
-
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1650
|
-
const members = arrArg.getElements().map(
|
|
1651
|
-
(el) => Node6.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
1652
|
-
);
|
|
1653
|
-
return members.join(" | ");
|
|
1654
|
-
}
|
|
1655
|
-
case "array": {
|
|
1656
|
-
const inner = args[0];
|
|
1657
|
-
if (!inner) return "unknown";
|
|
1658
|
-
return `Array<${zodAstToTs(inner)}>`;
|
|
1659
|
-
}
|
|
1660
|
-
case "object": {
|
|
1661
|
-
const objArg = args[0];
|
|
1662
|
-
if (!objArg || !Node6.isObjectLiteralExpression(objArg)) return "unknown";
|
|
1663
|
-
const lines = [];
|
|
1664
|
-
for (const prop of objArg.getProperties()) {
|
|
1665
|
-
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
1666
|
-
const key = prop.getName();
|
|
1667
|
-
const valNode = prop.getInitializer();
|
|
1668
|
-
if (!valNode) continue;
|
|
1669
|
-
const tsType = zodAstToTs(valNode);
|
|
1670
|
-
const isOpt = isOptionalChain(valNode);
|
|
1671
|
-
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
1672
|
-
}
|
|
1673
|
-
return `{ ${lines.join("; ")} }`;
|
|
1674
|
-
}
|
|
1675
|
-
case "union": {
|
|
1676
|
-
const arrArg = args[0];
|
|
1677
|
-
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1678
|
-
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
1679
|
-
}
|
|
1680
|
-
case "record": {
|
|
1681
|
-
const valArg = args.length === 1 ? args[0] : args[1];
|
|
1682
|
-
if (!valArg) return "unknown";
|
|
1683
|
-
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
1684
|
-
}
|
|
1685
|
-
case "tuple": {
|
|
1686
|
-
const arrArg = args[0];
|
|
1687
|
-
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1688
|
-
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
1689
|
-
}
|
|
1690
|
-
default:
|
|
1691
|
-
return "unknown";
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
return "unknown";
|
|
1695
|
-
}
|
|
1696
|
-
function isOptionalChain(node) {
|
|
1697
|
-
if (!Node6.isCallExpression(node)) return false;
|
|
1698
|
-
const expr = node.getExpression();
|
|
1699
|
-
return Node6.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
1700
|
-
}
|
|
1701
|
-
function decoratorStringArg(decoratorExpr) {
|
|
1702
|
-
if (!decoratorExpr) return void 0;
|
|
1703
|
-
if (Node6.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
1704
|
-
if (Node6.isArrayLiteralExpression(decoratorExpr)) {
|
|
1705
|
-
const first = decoratorExpr.getElements()[0];
|
|
1706
|
-
if (first && Node6.isStringLiteral(first)) return first.getLiteralValue();
|
|
1707
|
-
}
|
|
1708
|
-
return void 0;
|
|
1709
|
-
}
|
|
1710
|
-
function parseDefineContractCall(callExpr) {
|
|
1711
|
-
if (!Node6.isCallExpression(callExpr)) return null;
|
|
1712
|
-
const callee = callExpr.getExpression();
|
|
1713
|
-
const calleeName = Node6.isIdentifier(callee) ? callee.getText() : Node6.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
1714
|
-
if (calleeName !== "defineContract") return null;
|
|
1715
|
-
const args = callExpr.getArguments();
|
|
1716
|
-
const optsArg = args[0];
|
|
1717
|
-
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
1718
|
-
let query = null;
|
|
1719
|
-
let body = null;
|
|
1720
|
-
let response = "unknown";
|
|
1721
|
-
let bodyZodText = null;
|
|
1722
|
-
let queryZodText = null;
|
|
1723
|
-
for (const prop of optsArg.getProperties()) {
|
|
1724
|
-
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
1725
|
-
const propName = prop.getName();
|
|
1726
|
-
const val = prop.getInitializer();
|
|
1727
|
-
if (!val) continue;
|
|
1728
|
-
if (propName === "query") {
|
|
1729
|
-
query = zodAstToTs(val);
|
|
1730
|
-
queryZodText = val.getText();
|
|
1731
|
-
} else if (propName === "body") {
|
|
1732
|
-
body = zodAstToTs(val);
|
|
1733
|
-
bodyZodText = val.getText();
|
|
1734
|
-
} else if (propName === "response") {
|
|
1735
|
-
response = zodAstToTs(val);
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
1739
|
-
}
|
|
1740
|
-
function deriveClassSegment(className) {
|
|
1741
|
-
const noSuffix = className.replace(/Controller$/, "");
|
|
1742
|
-
if (!noSuffix) {
|
|
1743
|
-
throw new Error(
|
|
1744
|
-
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
1745
|
-
);
|
|
1746
|
-
}
|
|
1747
|
-
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
1748
|
-
}
|
|
1749
|
-
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
1750
|
-
const classPortion = classAs ?? deriveClassSegment(className);
|
|
1751
|
-
const methodPortion = methodAs ?? methodName;
|
|
1752
|
-
return `${classPortion}.${methodPortion}`;
|
|
1753
|
-
}
|
|
1754
|
-
function joinPaths(prefix, suffix) {
|
|
1755
|
-
if (!prefix && !suffix) return "/";
|
|
1756
|
-
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1757
|
-
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1758
|
-
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1759
|
-
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1760
|
-
const combined = p + s;
|
|
1761
|
-
return combined === "" ? "/" : combined;
|
|
1762
|
-
}
|
|
1763
|
-
function extractParams(path) {
|
|
1764
|
-
const matches = path.matchAll(/:(\w+)/g);
|
|
1765
|
-
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
1766
|
-
}
|
|
1158
|
+
// src/discovery/dto-type-resolver.ts
|
|
1159
|
+
var WRAPPER_TYPES = {
|
|
1160
|
+
// MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
|
|
1161
|
+
// wrappers around related entities; the wire shape is just the referenced
|
|
1162
|
+
// entity. Unwrap to the type argument.
|
|
1163
|
+
Ref: "unwrap",
|
|
1164
|
+
Reference: "unwrap",
|
|
1165
|
+
LoadedReference: "unwrap",
|
|
1166
|
+
IdentifiedReference: "unwrap",
|
|
1167
|
+
// MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
|
|
1168
|
+
Opt: "unwrap",
|
|
1169
|
+
Loaded: "unwrap",
|
|
1170
|
+
// Promise<T> — unwrap
|
|
1171
|
+
Promise: "unwrap",
|
|
1172
|
+
// MikroORM Collection<T> serializes as an array of T on the wire.
|
|
1173
|
+
Collection: "arrayOf",
|
|
1174
|
+
// Array<T> generic form
|
|
1175
|
+
Array: "arrayOf"
|
|
1176
|
+
};
|
|
1177
|
+
var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
1178
|
+
"Record",
|
|
1179
|
+
"Omit",
|
|
1180
|
+
"Pick",
|
|
1181
|
+
"Partial",
|
|
1182
|
+
"Required",
|
|
1183
|
+
"Readonly",
|
|
1184
|
+
"Map",
|
|
1185
|
+
"Set"
|
|
1186
|
+
]);
|
|
1767
1187
|
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
1768
1188
|
if (depth <= 0) return "unknown";
|
|
1769
|
-
if (
|
|
1189
|
+
if (Node5.isArrayTypeNode(typeNode)) {
|
|
1770
1190
|
const elementType = typeNode.getElementTypeNode();
|
|
1771
1191
|
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
1772
1192
|
}
|
|
1773
|
-
if (
|
|
1193
|
+
if (Node5.isUnionTypeNode(typeNode)) {
|
|
1774
1194
|
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
1775
1195
|
}
|
|
1776
|
-
if (
|
|
1196
|
+
if (Node5.isIntersectionTypeNode(typeNode)) {
|
|
1777
1197
|
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
1778
1198
|
}
|
|
1779
|
-
if (
|
|
1199
|
+
if (Node5.isParenthesizedTypeNode(typeNode)) {
|
|
1780
1200
|
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
1781
1201
|
}
|
|
1782
|
-
if (
|
|
1202
|
+
if (Node5.isTypeReference(typeNode)) {
|
|
1783
1203
|
const typeName = typeNode.getTypeName();
|
|
1784
|
-
const name =
|
|
1204
|
+
const name = Node5.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
1785
1205
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
1786
1206
|
if (name === "Date") return "string";
|
|
1787
1207
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
1788
1208
|
if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
|
|
1789
1209
|
return "unknown";
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1794
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1795
|
-
}
|
|
1796
|
-
return "unknown";
|
|
1797
|
-
}
|
|
1798
|
-
if (name === "Collection") {
|
|
1799
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
1800
|
-
const firstTypeArg = typeArgs[0];
|
|
1801
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1802
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
1803
|
-
}
|
|
1804
|
-
return "Array<unknown>";
|
|
1805
|
-
}
|
|
1806
|
-
if (name === "Opt" || name === "Loaded") {
|
|
1807
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
1808
|
-
const firstTypeArg = typeArgs[0];
|
|
1809
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1810
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1811
|
-
}
|
|
1812
|
-
return "unknown";
|
|
1210
|
+
const wrapperMode = WRAPPER_TYPES[name];
|
|
1211
|
+
if (wrapperMode) {
|
|
1212
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
1813
1213
|
}
|
|
1814
|
-
if (name
|
|
1815
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
1816
|
-
const firstTypeArg = typeArgs[0];
|
|
1817
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1818
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
1819
|
-
}
|
|
1820
|
-
return "Array<unknown>";
|
|
1821
|
-
}
|
|
1822
|
-
if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
|
|
1214
|
+
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
1823
1215
|
return typeNode.getText();
|
|
1824
1216
|
}
|
|
1825
|
-
if (name === "Promise") {
|
|
1826
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
1827
|
-
const firstTypeArg = typeArgs[0];
|
|
1828
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1829
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1830
|
-
}
|
|
1831
|
-
return "unknown";
|
|
1832
|
-
}
|
|
1833
1217
|
const resolved = findType(name, sourceFile, project);
|
|
1834
1218
|
if (resolved) {
|
|
1835
1219
|
return expandTypeDecl(resolved, project, depth - 1);
|
|
@@ -1845,6 +1229,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1845
1229
|
if (kind === SyntaxKind2.AnyKeyword) return "unknown";
|
|
1846
1230
|
return typeNode.getText();
|
|
1847
1231
|
}
|
|
1232
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
1233
|
+
const typeArgs = typeNode.getTypeArguments();
|
|
1234
|
+
const firstTypeArg = typeArgs[0];
|
|
1235
|
+
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1236
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1237
|
+
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
1238
|
+
}
|
|
1239
|
+
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
1240
|
+
}
|
|
1848
1241
|
function expandTypeDecl(result, project, depth) {
|
|
1849
1242
|
if (depth < 0) return "unknown";
|
|
1850
1243
|
switch (result.kind) {
|
|
@@ -1910,7 +1303,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
1910
1303
|
const paramArgs = paramDecorator.getArguments();
|
|
1911
1304
|
if (paramArgs.length === 0) continue;
|
|
1912
1305
|
const nameArg = paramArgs[0];
|
|
1913
|
-
if (!
|
|
1306
|
+
if (!Node5.isStringLiteral(nameArg)) continue;
|
|
1914
1307
|
const paramName = nameArg.getLiteralValue();
|
|
1915
1308
|
const typeNode = param.getTypeNode();
|
|
1916
1309
|
const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
|
|
@@ -1923,13 +1316,13 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1923
1316
|
if (apiResponseDecorator) {
|
|
1924
1317
|
const args = apiResponseDecorator.getArguments();
|
|
1925
1318
|
const optsArg = args[0];
|
|
1926
|
-
if (optsArg &&
|
|
1319
|
+
if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
|
|
1927
1320
|
for (const prop of optsArg.getProperties()) {
|
|
1928
|
-
if (!
|
|
1321
|
+
if (!Node5.isPropertyAssignment(prop)) continue;
|
|
1929
1322
|
if (prop.getName() !== "type") continue;
|
|
1930
1323
|
const val = prop.getInitializer();
|
|
1931
1324
|
if (!val) continue;
|
|
1932
|
-
if (
|
|
1325
|
+
if (Node5.isArrayLiteralExpression(val)) {
|
|
1933
1326
|
const elements = val.getElements();
|
|
1934
1327
|
const firstEl = elements[0];
|
|
1935
1328
|
if (elements.length > 0 && firstEl !== void 0) {
|
|
@@ -1949,7 +1342,7 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1949
1342
|
return "unknown";
|
|
1950
1343
|
}
|
|
1951
1344
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
1952
|
-
if (!
|
|
1345
|
+
if (!Node5.isIdentifier(node)) return "unknown";
|
|
1953
1346
|
const name = node.getText();
|
|
1954
1347
|
const resolved = findType(name, sourceFile, project);
|
|
1955
1348
|
if (resolved) {
|
|
@@ -1996,11 +1389,11 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
1996
1389
|
if (apiResp) {
|
|
1997
1390
|
const args = apiResp.getArguments();
|
|
1998
1391
|
const optsArg = args[0];
|
|
1999
|
-
if (optsArg &&
|
|
1392
|
+
if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
|
|
2000
1393
|
for (const prop of optsArg.getProperties()) {
|
|
2001
|
-
if (
|
|
1394
|
+
if (Node5.isPropertyAssignment(prop) && prop.getName() === "type") {
|
|
2002
1395
|
const val = prop.getInitializer();
|
|
2003
|
-
if (val &&
|
|
1396
|
+
if (val && Node5.isIdentifier(val)) {
|
|
2004
1397
|
const name = val.getText();
|
|
2005
1398
|
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
2006
1399
|
if (localDecl?.isExported()) {
|
|
@@ -2017,27 +1410,18 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2017
1410
|
}
|
|
2018
1411
|
}
|
|
2019
1412
|
}
|
|
2020
|
-
let bodyZodText = null;
|
|
2021
|
-
let queryZodText = null;
|
|
2022
1413
|
let bodySchema = null;
|
|
2023
1414
|
let querySchema = null;
|
|
2024
|
-
const formNested = {};
|
|
2025
1415
|
const formWarnings = [];
|
|
2026
1416
|
const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
|
|
2027
1417
|
if (bodyClass) {
|
|
2028
|
-
const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
|
|
2029
|
-
bodyZodText = result.schemaText;
|
|
2030
|
-
for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
|
|
2031
|
-
formWarnings.push(...result.warnings);
|
|
2032
1418
|
bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
|
|
1419
|
+
formWarnings.push(...bodySchema.warnings);
|
|
2033
1420
|
}
|
|
2034
1421
|
const queryClass = resolveParamClass(method, "Query", sourceFile, project);
|
|
2035
1422
|
if (queryClass) {
|
|
2036
|
-
const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
|
|
2037
|
-
queryZodText = result.schemaText;
|
|
2038
|
-
for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
|
|
2039
|
-
formWarnings.push(...result.warnings);
|
|
2040
1423
|
querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
|
|
1424
|
+
formWarnings.push(...querySchema.warnings);
|
|
2041
1425
|
}
|
|
2042
1426
|
return {
|
|
2043
1427
|
query,
|
|
@@ -2050,9 +1434,6 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2050
1434
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2051
1435
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2052
1436
|
filterSource: filterInfo?.source ?? null,
|
|
2053
|
-
bodyZodText,
|
|
2054
|
-
queryZodText,
|
|
2055
|
-
formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
|
|
2056
1437
|
formWarnings,
|
|
2057
1438
|
bodySchema,
|
|
2058
1439
|
querySchema
|
|
@@ -2072,6 +1453,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
|
2072
1453
|
}
|
|
2073
1454
|
return null;
|
|
2074
1455
|
}
|
|
1456
|
+
|
|
1457
|
+
// src/discovery/zod-ast-to-ts.ts
|
|
1458
|
+
import { Node as Node6, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
1459
|
+
function zodAstToTs(node) {
|
|
1460
|
+
if (!Node6.isCallExpression(node)) return "unknown";
|
|
1461
|
+
const expr = node.getExpression();
|
|
1462
|
+
if (Node6.isPropertyAccessExpression(expr)) {
|
|
1463
|
+
const methodName = expr.getName();
|
|
1464
|
+
const receiver = expr.getExpression();
|
|
1465
|
+
if (methodName === "optional") {
|
|
1466
|
+
return `${zodAstToTs(receiver)} | undefined`;
|
|
1467
|
+
}
|
|
1468
|
+
if (methodName === "nullable") {
|
|
1469
|
+
return `${zodAstToTs(receiver)} | null`;
|
|
1470
|
+
}
|
|
1471
|
+
const args = node.getArguments();
|
|
1472
|
+
switch (methodName) {
|
|
1473
|
+
case "string":
|
|
1474
|
+
return "string";
|
|
1475
|
+
case "number":
|
|
1476
|
+
return "number";
|
|
1477
|
+
case "boolean":
|
|
1478
|
+
return "boolean";
|
|
1479
|
+
case "unknown":
|
|
1480
|
+
return "unknown";
|
|
1481
|
+
case "any":
|
|
1482
|
+
return "unknown";
|
|
1483
|
+
case "literal": {
|
|
1484
|
+
const lit = args[0];
|
|
1485
|
+
if (!lit) return "unknown";
|
|
1486
|
+
if (Node6.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
1487
|
+
if (Node6.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
1488
|
+
if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
|
|
1489
|
+
if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
|
|
1490
|
+
return "unknown";
|
|
1491
|
+
}
|
|
1492
|
+
case "enum": {
|
|
1493
|
+
const arrArg = args[0];
|
|
1494
|
+
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1495
|
+
const members = arrArg.getElements().map(
|
|
1496
|
+
(el) => Node6.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
1497
|
+
);
|
|
1498
|
+
return members.join(" | ");
|
|
1499
|
+
}
|
|
1500
|
+
case "array": {
|
|
1501
|
+
const inner = args[0];
|
|
1502
|
+
if (!inner) return "unknown";
|
|
1503
|
+
return `Array<${zodAstToTs(inner)}>`;
|
|
1504
|
+
}
|
|
1505
|
+
case "object": {
|
|
1506
|
+
const objArg = args[0];
|
|
1507
|
+
if (!objArg || !Node6.isObjectLiteralExpression(objArg)) return "unknown";
|
|
1508
|
+
const lines = [];
|
|
1509
|
+
for (const prop of objArg.getProperties()) {
|
|
1510
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
1511
|
+
const key = prop.getName();
|
|
1512
|
+
const valNode = prop.getInitializer();
|
|
1513
|
+
if (!valNode) continue;
|
|
1514
|
+
const tsType = zodAstToTs(valNode);
|
|
1515
|
+
const isOpt = isOptionalChain(valNode);
|
|
1516
|
+
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
1517
|
+
}
|
|
1518
|
+
return `{ ${lines.join("; ")} }`;
|
|
1519
|
+
}
|
|
1520
|
+
case "union": {
|
|
1521
|
+
const arrArg = args[0];
|
|
1522
|
+
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1523
|
+
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
1524
|
+
}
|
|
1525
|
+
case "record": {
|
|
1526
|
+
const valArg = args.length === 1 ? args[0] : args[1];
|
|
1527
|
+
if (!valArg) return "unknown";
|
|
1528
|
+
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
1529
|
+
}
|
|
1530
|
+
case "tuple": {
|
|
1531
|
+
const arrArg = args[0];
|
|
1532
|
+
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1533
|
+
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
1534
|
+
}
|
|
1535
|
+
default:
|
|
1536
|
+
return "unknown";
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return "unknown";
|
|
1540
|
+
}
|
|
1541
|
+
function isOptionalChain(node) {
|
|
1542
|
+
if (!Node6.isCallExpression(node)) return false;
|
|
1543
|
+
const expr = node.getExpression();
|
|
1544
|
+
return Node6.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
1545
|
+
}
|
|
1546
|
+
function parseDefineContractCall(callExpr) {
|
|
1547
|
+
if (!Node6.isCallExpression(callExpr)) return null;
|
|
1548
|
+
const callee = callExpr.getExpression();
|
|
1549
|
+
const calleeName = Node6.isIdentifier(callee) ? callee.getText() : Node6.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
1550
|
+
if (calleeName !== "defineContract") return null;
|
|
1551
|
+
const args = callExpr.getArguments();
|
|
1552
|
+
const optsArg = args[0];
|
|
1553
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
1554
|
+
let query = null;
|
|
1555
|
+
let body = null;
|
|
1556
|
+
let response = "unknown";
|
|
1557
|
+
let bodyZodText = null;
|
|
1558
|
+
let queryZodText = null;
|
|
1559
|
+
for (const prop of optsArg.getProperties()) {
|
|
1560
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
1561
|
+
const propName = prop.getName();
|
|
1562
|
+
const val = prop.getInitializer();
|
|
1563
|
+
if (!val) continue;
|
|
1564
|
+
if (propName === "query") {
|
|
1565
|
+
query = zodAstToTs(val);
|
|
1566
|
+
queryZodText = val.getText();
|
|
1567
|
+
} else if (propName === "body") {
|
|
1568
|
+
body = zodAstToTs(val);
|
|
1569
|
+
bodyZodText = val.getText();
|
|
1570
|
+
} else if (propName === "response") {
|
|
1571
|
+
response = zodAstToTs(val);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return { query, body, response, bodyZodText, queryZodText };
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/discovery/contracts-fast.ts
|
|
1578
|
+
async function discoverContractsFast(opts) {
|
|
1579
|
+
const { cwd, glob, tsconfig } = opts;
|
|
1580
|
+
const tsconfigPath = tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
|
|
1581
|
+
let project;
|
|
1582
|
+
try {
|
|
1583
|
+
project = new Project({
|
|
1584
|
+
tsConfigFilePath: tsconfigPath,
|
|
1585
|
+
skipAddingFilesFromTsConfig: true,
|
|
1586
|
+
skipLoadingLibFiles: true,
|
|
1587
|
+
skipFileDependencyResolution: true
|
|
1588
|
+
});
|
|
1589
|
+
} catch {
|
|
1590
|
+
project = new Project({
|
|
1591
|
+
skipAddingFilesFromTsConfig: true,
|
|
1592
|
+
skipLoadingLibFiles: true,
|
|
1593
|
+
skipFileDependencyResolution: true,
|
|
1594
|
+
compilerOptions: {
|
|
1595
|
+
allowJs: true,
|
|
1596
|
+
resolveJsonModule: false,
|
|
1597
|
+
strict: false
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
const files = await fg(glob, { cwd, absolute: true, onlyFiles: true });
|
|
1602
|
+
for (const f of files) {
|
|
1603
|
+
project.addSourceFileAtPath(f);
|
|
1604
|
+
}
|
|
1605
|
+
const routes = [];
|
|
1606
|
+
setDiscoveryContext(project, {
|
|
1607
|
+
projectRoot: cwd,
|
|
1608
|
+
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
1609
|
+
});
|
|
1610
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
1611
|
+
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
1612
|
+
}
|
|
1613
|
+
return routes;
|
|
1614
|
+
}
|
|
1615
|
+
function decoratorStringArg(decoratorExpr) {
|
|
1616
|
+
if (!decoratorExpr) return void 0;
|
|
1617
|
+
if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
1618
|
+
if (Node7.isArrayLiteralExpression(decoratorExpr)) {
|
|
1619
|
+
const first = decoratorExpr.getElements()[0];
|
|
1620
|
+
if (first && Node7.isStringLiteral(first)) return first.getLiteralValue();
|
|
1621
|
+
}
|
|
1622
|
+
return void 0;
|
|
1623
|
+
}
|
|
1624
|
+
function deriveClassSegment(className) {
|
|
1625
|
+
const noSuffix = className.replace(/Controller$/, "");
|
|
1626
|
+
if (!noSuffix) {
|
|
1627
|
+
throw new Error(
|
|
1628
|
+
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
1632
|
+
}
|
|
1633
|
+
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
1634
|
+
const classPortion = classAs ?? deriveClassSegment(className);
|
|
1635
|
+
const methodPortion = methodAs ?? methodName;
|
|
1636
|
+
return `${classPortion}.${methodPortion}`;
|
|
1637
|
+
}
|
|
1638
|
+
function joinPaths(prefix, suffix) {
|
|
1639
|
+
if (!prefix && !suffix) return "/";
|
|
1640
|
+
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1641
|
+
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1642
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1643
|
+
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1644
|
+
const combined = p + s;
|
|
1645
|
+
return combined === "" ? "/" : combined;
|
|
1646
|
+
}
|
|
1647
|
+
function extractParams(path) {
|
|
1648
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
1649
|
+
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
1650
|
+
}
|
|
2075
1651
|
var HTTP_METHOD_DECORATORS = {
|
|
2076
1652
|
Get: "GET",
|
|
2077
1653
|
Post: "POST",
|
|
@@ -2082,176 +1658,186 @@ var HTTP_METHOD_DECORATORS = {
|
|
|
2082
1658
|
Head: "HEAD",
|
|
2083
1659
|
All: "ALL"
|
|
2084
1660
|
};
|
|
1661
|
+
function resolveVerb(method) {
|
|
1662
|
+
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
1663
|
+
const httpDecorator = method.getDecorator(decoratorName);
|
|
1664
|
+
if (httpDecorator) {
|
|
1665
|
+
const httpArgs = httpDecorator.getArguments();
|
|
1666
|
+
const pathArg = httpArgs[0];
|
|
1667
|
+
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
function readAsDecorator(node, label) {
|
|
1673
|
+
const asDecorator = node.getDecorator("As");
|
|
1674
|
+
if (!asDecorator) return void 0;
|
|
1675
|
+
const asName = decoratorStringArg(asDecorator.getArguments()[0]);
|
|
1676
|
+
if (!asName) {
|
|
1677
|
+
throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
|
|
1678
|
+
}
|
|
1679
|
+
return asName;
|
|
1680
|
+
}
|
|
1681
|
+
function buildRoute(args) {
|
|
1682
|
+
const {
|
|
1683
|
+
className,
|
|
1684
|
+
methodName,
|
|
1685
|
+
resolvedMethod,
|
|
1686
|
+
combinedPath,
|
|
1687
|
+
classAs,
|
|
1688
|
+
methodAs,
|
|
1689
|
+
sourceFile,
|
|
1690
|
+
seenNames,
|
|
1691
|
+
contractSource
|
|
1692
|
+
} = args;
|
|
1693
|
+
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
1694
|
+
const qualifiedRef = `${className}.${methodName}`;
|
|
1695
|
+
const existing = seenNames.get(routeName);
|
|
1696
|
+
if (existing !== void 0) {
|
|
1697
|
+
throw new Error(
|
|
1698
|
+
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
seenNames.set(routeName, qualifiedRef);
|
|
1702
|
+
return {
|
|
1703
|
+
method: resolvedMethod,
|
|
1704
|
+
path: combinedPath,
|
|
1705
|
+
name: routeName,
|
|
1706
|
+
params: extractParams(combinedPath),
|
|
1707
|
+
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
1708
|
+
contract: { contractSource }
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
function extractContractRoute(args) {
|
|
1712
|
+
const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
|
|
1713
|
+
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
1714
|
+
if (!firstDecoratorArg) return null;
|
|
1715
|
+
let contractDef = null;
|
|
1716
|
+
let bodyZodRef = null;
|
|
1717
|
+
let queryZodRef = null;
|
|
1718
|
+
if (Node7.isCallExpression(firstDecoratorArg)) {
|
|
1719
|
+
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
1720
|
+
} else if (Node7.isIdentifier(firstDecoratorArg)) {
|
|
1721
|
+
const identName = firstDecoratorArg.getText();
|
|
1722
|
+
const varDecl = sourceFile.getVariableDeclaration(identName);
|
|
1723
|
+
if (!varDecl) {
|
|
1724
|
+
console.warn(
|
|
1725
|
+
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
|
|
1726
|
+
);
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
const initializer = varDecl.getInitializer();
|
|
1730
|
+
if (!initializer) return null;
|
|
1731
|
+
contractDef = parseDefineContractCall(initializer);
|
|
1732
|
+
if (contractDef && varDecl.isExported()) {
|
|
1733
|
+
const filePath = sourceFile.getFilePath();
|
|
1734
|
+
if (contractDef.body !== null) {
|
|
1735
|
+
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
1736
|
+
}
|
|
1737
|
+
if (contractDef.query !== null) {
|
|
1738
|
+
queryZodRef = { name: `${identName}.query`, filePath };
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
} else {
|
|
1742
|
+
console.warn(
|
|
1743
|
+
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
1744
|
+
);
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
if (!contractDef) return null;
|
|
1748
|
+
if (!verb) return null;
|
|
1749
|
+
const resolvedPath = joinPaths(prefix, verb.handlerPath);
|
|
1750
|
+
const methodName = method.getName();
|
|
1751
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
1752
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
1753
|
+
return buildRoute({
|
|
1754
|
+
className,
|
|
1755
|
+
methodName,
|
|
1756
|
+
resolvedMethod: verb.httpMethod,
|
|
1757
|
+
combinedPath: resolvedPath,
|
|
1758
|
+
classAs,
|
|
1759
|
+
methodAs,
|
|
1760
|
+
sourceFile,
|
|
1761
|
+
seenNames,
|
|
1762
|
+
contractSource: {
|
|
1763
|
+
query: contractDef.query,
|
|
1764
|
+
body: contractDef.body,
|
|
1765
|
+
response: contractDef.response,
|
|
1766
|
+
// Path A: capture both the importable ref and the raw text. The emitter
|
|
1767
|
+
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
1768
|
+
// would drag server-only deps into the client bundle).
|
|
1769
|
+
bodyZodRef,
|
|
1770
|
+
bodyZodText: contractDef.bodyZodText,
|
|
1771
|
+
queryZodRef,
|
|
1772
|
+
queryZodText: contractDef.queryZodText
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
function extractDtoRoute(args) {
|
|
1777
|
+
const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
|
|
1778
|
+
if (!verb) return null;
|
|
1779
|
+
const combined = joinPaths(prefix, verb.handlerPath);
|
|
1780
|
+
const methodName = method.getName();
|
|
1781
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
1782
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
1783
|
+
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
1784
|
+
return buildRoute({
|
|
1785
|
+
className,
|
|
1786
|
+
methodName,
|
|
1787
|
+
resolvedMethod: verb.httpMethod,
|
|
1788
|
+
combinedPath: combined,
|
|
1789
|
+
classAs,
|
|
1790
|
+
methodAs,
|
|
1791
|
+
sourceFile,
|
|
1792
|
+
seenNames,
|
|
1793
|
+
contractSource: {
|
|
1794
|
+
query: dtoContract?.query ?? null,
|
|
1795
|
+
body: dtoContract?.body ?? null,
|
|
1796
|
+
response: dtoContract?.response ?? "unknown",
|
|
1797
|
+
queryRef: dtoContract?.queryRef ?? null,
|
|
1798
|
+
bodyRef: dtoContract?.bodyRef ?? null,
|
|
1799
|
+
responseRef: dtoContract?.responseRef ?? null,
|
|
1800
|
+
filterFields: dtoContract?.filterFields ?? null,
|
|
1801
|
+
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
1802
|
+
filterSource: dtoContract?.filterSource ?? null,
|
|
1803
|
+
formWarnings: dtoContract?.formWarnings ?? [],
|
|
1804
|
+
bodySchema: dtoContract?.bodySchema ?? null,
|
|
1805
|
+
querySchema: dtoContract?.querySchema ?? null
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
2085
1809
|
function extractFromSourceFile(sourceFile, project) {
|
|
2086
1810
|
const routes = [];
|
|
2087
1811
|
const seenNames = /* @__PURE__ */ new Map();
|
|
2088
|
-
const
|
|
2089
|
-
for (const cls of classes) {
|
|
1812
|
+
for (const cls of sourceFile.getClasses()) {
|
|
2090
1813
|
const controllerDecorator = cls.getDecorator("Controller");
|
|
2091
1814
|
if (!controllerDecorator) continue;
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2094
|
-
const prefix = decoratorStringArg(firstArg3) ?? "";
|
|
1815
|
+
const firstArg2 = controllerDecorator.getArguments()[0];
|
|
1816
|
+
const prefix = decoratorStringArg(firstArg2) ?? "";
|
|
2095
1817
|
const className = cls.getName() ?? "Unknown";
|
|
2096
1818
|
for (const method of cls.getMethods()) {
|
|
2097
|
-
|
|
2098
|
-
let handlerPath = "";
|
|
2099
|
-
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
2100
|
-
const httpDecorator = method.getDecorator(decoratorName);
|
|
2101
|
-
if (httpDecorator) {
|
|
2102
|
-
httpMethod = verb;
|
|
2103
|
-
const httpArgs = httpDecorator.getArguments();
|
|
2104
|
-
const pathArg = httpArgs[0];
|
|
2105
|
-
handlerPath = decoratorStringArg(pathArg) ?? "";
|
|
2106
|
-
break;
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
1819
|
+
const verb = resolveVerb(method);
|
|
2109
1820
|
const applyContractDecorator = method.getDecorator("ApplyContract");
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
contractDef = parseDefineContractCall(initializer);
|
|
2131
|
-
if (contractDef && varDecl.isExported()) {
|
|
2132
|
-
const filePath = sourceFile.getFilePath();
|
|
2133
|
-
if (contractDef.body !== null) {
|
|
2134
|
-
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
2135
|
-
}
|
|
2136
|
-
if (contractDef.query !== null) {
|
|
2137
|
-
queryZodRef = { name: `${identName}.query`, filePath };
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
} else {
|
|
2141
|
-
console.warn(
|
|
2142
|
-
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
2143
|
-
);
|
|
2144
|
-
continue;
|
|
2145
|
-
}
|
|
2146
|
-
if (!contractDef) continue;
|
|
2147
|
-
if (!httpMethod) continue;
|
|
2148
|
-
const resolvedMethod = httpMethod;
|
|
2149
|
-
const resolvedPath = joinPaths(prefix, handlerPath);
|
|
2150
|
-
const combined = resolvedPath;
|
|
2151
|
-
const params = extractParams(combined);
|
|
2152
|
-
const methodName = method.getName();
|
|
2153
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
2154
|
-
let classAs;
|
|
2155
|
-
if (classAsDecorator) {
|
|
2156
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
2157
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
2158
|
-
if (!classAsName) {
|
|
2159
|
-
throw new Error(
|
|
2160
|
-
`@As decorator on class ${className} must have a non-empty string argument.`
|
|
2161
|
-
);
|
|
2162
|
-
}
|
|
2163
|
-
classAs = classAsName;
|
|
2164
|
-
}
|
|
2165
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
2166
|
-
let methodAs;
|
|
2167
|
-
if (methodAsDecorator) {
|
|
2168
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
2169
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
2170
|
-
if (!methodAsName) {
|
|
2171
|
-
throw new Error(
|
|
2172
|
-
`@As decorator on ${className}.${methodName} must have a non-empty string argument.`
|
|
2173
|
-
);
|
|
2174
|
-
}
|
|
2175
|
-
methodAs = methodAsName;
|
|
2176
|
-
}
|
|
2177
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
2178
|
-
const qualifiedRef = `${className}.${methodName}`;
|
|
2179
|
-
const existing = seenNames.get(routeName);
|
|
2180
|
-
if (existing !== void 0) {
|
|
2181
|
-
throw new Error(
|
|
2182
|
-
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
2183
|
-
);
|
|
2184
|
-
}
|
|
2185
|
-
seenNames.set(routeName, qualifiedRef);
|
|
2186
|
-
routes.push({
|
|
2187
|
-
method: resolvedMethod,
|
|
2188
|
-
path: combined,
|
|
2189
|
-
name: routeName,
|
|
2190
|
-
params,
|
|
2191
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
2192
|
-
contract: {
|
|
2193
|
-
contractSource: {
|
|
2194
|
-
query: contractDef.query,
|
|
2195
|
-
body: contractDef.body,
|
|
2196
|
-
response: contractDef.response,
|
|
2197
|
-
// Path A: capture both the importable ref and the raw text. The
|
|
2198
|
-
// emitter prefers inlining the text (client-safe — re-exporting from
|
|
2199
|
-
// a controller would drag server-only deps into the client bundle).
|
|
2200
|
-
bodyZodRef,
|
|
2201
|
-
bodyZodText: contractDef.bodyZodText,
|
|
2202
|
-
queryZodRef,
|
|
2203
|
-
queryZodText: contractDef.queryZodText
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
});
|
|
2207
|
-
} else {
|
|
2208
|
-
if (!httpMethod) continue;
|
|
2209
|
-
const combined = joinPaths(prefix, handlerPath);
|
|
2210
|
-
const params = extractParams(combined);
|
|
2211
|
-
const methodName = method.getName();
|
|
2212
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
2213
|
-
let classAs;
|
|
2214
|
-
if (classAsDecorator) {
|
|
2215
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
2216
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
2217
|
-
if (classAsName) classAs = classAsName;
|
|
2218
|
-
}
|
|
2219
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
2220
|
-
let methodAs;
|
|
2221
|
-
if (methodAsDecorator) {
|
|
2222
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
2223
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
2224
|
-
if (methodAsName) methodAs = methodAsName;
|
|
2225
|
-
}
|
|
2226
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
2227
|
-
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
2228
|
-
routes.push({
|
|
2229
|
-
method: httpMethod,
|
|
2230
|
-
path: combined,
|
|
2231
|
-
name: routeName,
|
|
2232
|
-
params,
|
|
2233
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
2234
|
-
contract: {
|
|
2235
|
-
contractSource: {
|
|
2236
|
-
query: dtoContract?.query ?? null,
|
|
2237
|
-
body: dtoContract?.body ?? null,
|
|
2238
|
-
response: dtoContract?.response ?? "unknown",
|
|
2239
|
-
queryRef: dtoContract?.queryRef ?? null,
|
|
2240
|
-
bodyRef: dtoContract?.bodyRef ?? null,
|
|
2241
|
-
responseRef: dtoContract?.responseRef ?? null,
|
|
2242
|
-
filterFields: dtoContract?.filterFields ?? null,
|
|
2243
|
-
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
2244
|
-
filterSource: dtoContract?.filterSource ?? null,
|
|
2245
|
-
bodyZodText: dtoContract?.bodyZodText ?? null,
|
|
2246
|
-
queryZodText: dtoContract?.queryZodText ?? null,
|
|
2247
|
-
formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
|
|
2248
|
-
formWarnings: dtoContract?.formWarnings ?? [],
|
|
2249
|
-
bodySchema: dtoContract?.bodySchema ?? null,
|
|
2250
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
});
|
|
2254
|
-
}
|
|
1821
|
+
const route = applyContractDecorator ? extractContractRoute({
|
|
1822
|
+
cls,
|
|
1823
|
+
method,
|
|
1824
|
+
applyContractDecorator,
|
|
1825
|
+
verb,
|
|
1826
|
+
prefix,
|
|
1827
|
+
className,
|
|
1828
|
+
sourceFile,
|
|
1829
|
+
seenNames
|
|
1830
|
+
}) : extractDtoRoute({
|
|
1831
|
+
cls,
|
|
1832
|
+
method,
|
|
1833
|
+
verb,
|
|
1834
|
+
prefix,
|
|
1835
|
+
className,
|
|
1836
|
+
sourceFile,
|
|
1837
|
+
project,
|
|
1838
|
+
seenNames
|
|
1839
|
+
});
|
|
1840
|
+
if (route) routes.push(route);
|
|
2255
1841
|
}
|
|
2256
1842
|
}
|
|
2257
1843
|
return routes;
|
|
@@ -2259,8 +1845,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
2259
1845
|
|
|
2260
1846
|
// src/generate.ts
|
|
2261
1847
|
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
2262
|
-
import { dirname as dirname3, join as
|
|
2263
|
-
import { Project as Project3 } from "ts-morph";
|
|
1848
|
+
import { dirname as dirname3, join as join11 } from "path";
|
|
2264
1849
|
|
|
2265
1850
|
// src/discovery/pages.ts
|
|
2266
1851
|
import { readFile } from "fs/promises";
|
|
@@ -2315,7 +1900,8 @@ function extractPropsSource(source, exportName) {
|
|
|
2315
1900
|
}
|
|
2316
1901
|
|
|
2317
1902
|
// src/discovery/shared-props.ts
|
|
2318
|
-
import {
|
|
1903
|
+
import { join as join4 } from "path";
|
|
1904
|
+
import { Node as Node8, Project as Project2, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
2319
1905
|
function discoverSharedProps(project, moduleEntry) {
|
|
2320
1906
|
try {
|
|
2321
1907
|
let sourceFile = project.getSourceFile(moduleEntry);
|
|
@@ -2326,23 +1912,48 @@ function discoverSharedProps(project, moduleEntry) {
|
|
|
2326
1912
|
return null;
|
|
2327
1913
|
}
|
|
2328
1914
|
}
|
|
2329
|
-
const forRootCall = findForRootCall(sourceFile);
|
|
2330
|
-
if (!forRootCall) return null;
|
|
2331
|
-
const initializer = findShareInitializer(forRootCall);
|
|
2332
|
-
if (!initializer) return null;
|
|
2333
|
-
return extractShareType(initializer, sourceFile, project);
|
|
1915
|
+
const forRootCall = findForRootCall(sourceFile);
|
|
1916
|
+
if (!forRootCall) return null;
|
|
1917
|
+
const initializer = findShareInitializer(forRootCall);
|
|
1918
|
+
if (!initializer) return null;
|
|
1919
|
+
return extractShareType(initializer, sourceFile, project);
|
|
1920
|
+
} catch {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function discoverSharedPropsFromConfig(config) {
|
|
1925
|
+
if (!config.app?.moduleEntry) return null;
|
|
1926
|
+
try {
|
|
1927
|
+
const tsconfigPath = config.app.tsconfig ?? join4(config.codegen.cwd, "tsconfig.json");
|
|
1928
|
+
let project;
|
|
1929
|
+
try {
|
|
1930
|
+
project = new Project2({
|
|
1931
|
+
tsConfigFilePath: tsconfigPath,
|
|
1932
|
+
skipAddingFilesFromTsConfig: true,
|
|
1933
|
+
skipLoadingLibFiles: true,
|
|
1934
|
+
skipFileDependencyResolution: true
|
|
1935
|
+
});
|
|
1936
|
+
} catch {
|
|
1937
|
+
project = new Project2({
|
|
1938
|
+
skipAddingFilesFromTsConfig: true,
|
|
1939
|
+
skipLoadingLibFiles: true,
|
|
1940
|
+
skipFileDependencyResolution: true,
|
|
1941
|
+
compilerOptions: { allowJs: true, strict: false }
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
return discoverSharedProps(project, config.app.moduleEntry);
|
|
2334
1945
|
} catch {
|
|
2335
1946
|
return null;
|
|
2336
1947
|
}
|
|
2337
1948
|
}
|
|
2338
1949
|
function findForRootCall(sourceFile) {
|
|
2339
|
-
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
1950
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression);
|
|
2340
1951
|
for (const call of callExpressions) {
|
|
2341
1952
|
const expr = call.getExpression();
|
|
2342
|
-
if (!
|
|
1953
|
+
if (!Node8.isPropertyAccessExpression(expr)) continue;
|
|
2343
1954
|
const methodName = expr.getName();
|
|
2344
1955
|
const objectExpr = expr.getExpression();
|
|
2345
|
-
if (methodName === "forRoot" &&
|
|
1956
|
+
if (methodName === "forRoot" && Node8.isIdentifier(objectExpr)) {
|
|
2346
1957
|
const name = objectExpr.getText();
|
|
2347
1958
|
if (name === "InertiaModule") {
|
|
2348
1959
|
return call;
|
|
@@ -2352,19 +1963,19 @@ function findForRootCall(sourceFile) {
|
|
|
2352
1963
|
return null;
|
|
2353
1964
|
}
|
|
2354
1965
|
function findShareInitializer(forRootCall) {
|
|
2355
|
-
if (!
|
|
1966
|
+
if (!Node8.isCallExpression(forRootCall)) return null;
|
|
2356
1967
|
const args = forRootCall.getArguments();
|
|
2357
|
-
const
|
|
2358
|
-
if (!
|
|
2359
|
-
for (const prop of
|
|
2360
|
-
if (
|
|
1968
|
+
const firstArg2 = args[0];
|
|
1969
|
+
if (!firstArg2 || !Node8.isObjectLiteralExpression(firstArg2)) return null;
|
|
1970
|
+
for (const prop of firstArg2.getProperties()) {
|
|
1971
|
+
if (Node8.isPropertyAssignment(prop) && prop.getName() === "share") {
|
|
2361
1972
|
return prop.getInitializer() ?? null;
|
|
2362
1973
|
}
|
|
2363
1974
|
}
|
|
2364
1975
|
return null;
|
|
2365
1976
|
}
|
|
2366
1977
|
function extractShareType(node, sourceFile, project) {
|
|
2367
|
-
if (
|
|
1978
|
+
if (Node8.isIdentifier(node)) {
|
|
2368
1979
|
const ref = resolveIdentifierToImportRef(node, sourceFile, project);
|
|
2369
1980
|
if (ref) {
|
|
2370
1981
|
return {
|
|
@@ -2374,22 +1985,22 @@ function extractShareType(node, sourceFile, project) {
|
|
|
2374
1985
|
};
|
|
2375
1986
|
}
|
|
2376
1987
|
}
|
|
2377
|
-
if (
|
|
1988
|
+
if (Node8.isArrowFunction(node)) {
|
|
2378
1989
|
const result = extractFromFunctionLike(node, sourceFile);
|
|
2379
1990
|
return result ? { ...result, isImportRef: false } : null;
|
|
2380
1991
|
}
|
|
2381
|
-
if (
|
|
1992
|
+
if (Node8.isFunctionExpression(node)) {
|
|
2382
1993
|
const result = extractFromFunctionLike(node, sourceFile);
|
|
2383
1994
|
return result ? { ...result, isImportRef: false } : null;
|
|
2384
1995
|
}
|
|
2385
|
-
if (
|
|
1996
|
+
if (Node8.isObjectLiteralExpression(node)) {
|
|
2386
1997
|
const result = extractFromObjectLiteral(node);
|
|
2387
1998
|
return result ? { ...result, isImportRef: false } : null;
|
|
2388
1999
|
}
|
|
2389
2000
|
return null;
|
|
2390
2001
|
}
|
|
2391
2002
|
function resolveIdentifierToImportRef(id, sourceFile, project) {
|
|
2392
|
-
if (!
|
|
2003
|
+
if (!Node8.isIdentifier(id)) return null;
|
|
2393
2004
|
const name = id.getText();
|
|
2394
2005
|
const localFunc = sourceFile.getFunction(name);
|
|
2395
2006
|
if (localFunc?.isExported()) {
|
|
@@ -2423,49 +2034,49 @@ function resolveIdentifierToImportRef(id, sourceFile, project) {
|
|
|
2423
2034
|
return null;
|
|
2424
2035
|
}
|
|
2425
2036
|
function extractFromFunctionLike(node, _sourceFile) {
|
|
2426
|
-
const returnTypeNode =
|
|
2037
|
+
const returnTypeNode = Node8.isArrowFunction(node) || Node8.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
|
|
2427
2038
|
if (returnTypeNode) {
|
|
2428
2039
|
return extractFromReturnTypeAnnotation(returnTypeNode);
|
|
2429
2040
|
}
|
|
2430
|
-
if (
|
|
2041
|
+
if (Node8.isArrowFunction(node)) {
|
|
2431
2042
|
const body = node.getBody();
|
|
2432
|
-
if (
|
|
2043
|
+
if (Node8.isParenthesizedExpression(body)) {
|
|
2433
2044
|
const inner = body.getExpression();
|
|
2434
|
-
if (
|
|
2045
|
+
if (Node8.isObjectLiteralExpression(inner)) {
|
|
2435
2046
|
return extractFromObjectLiteral(inner);
|
|
2436
2047
|
}
|
|
2437
2048
|
}
|
|
2438
|
-
if (
|
|
2049
|
+
if (Node8.isObjectLiteralExpression(body)) {
|
|
2439
2050
|
return extractFromObjectLiteral(body);
|
|
2440
2051
|
}
|
|
2441
|
-
if (
|
|
2052
|
+
if (Node8.isBlock(body)) {
|
|
2442
2053
|
return extractFromBlockReturn(body);
|
|
2443
2054
|
}
|
|
2444
2055
|
}
|
|
2445
|
-
if (
|
|
2056
|
+
if (Node8.isFunctionExpression(node)) {
|
|
2446
2057
|
const body = node.getBody();
|
|
2447
|
-
if (
|
|
2058
|
+
if (Node8.isBlock(body)) {
|
|
2448
2059
|
return extractFromBlockReturn(body);
|
|
2449
2060
|
}
|
|
2450
2061
|
}
|
|
2451
2062
|
return null;
|
|
2452
2063
|
}
|
|
2453
2064
|
function extractFromReturnTypeAnnotation(typeNode) {
|
|
2454
|
-
if (
|
|
2065
|
+
if (Node8.isTypeReference(typeNode)) {
|
|
2455
2066
|
const typeName = typeNode.getTypeName();
|
|
2456
|
-
if (
|
|
2067
|
+
if (Node8.isIdentifier(typeName) && typeName.getText() === "Promise") {
|
|
2457
2068
|
const typeArgs = typeNode.getTypeArguments();
|
|
2458
|
-
const
|
|
2459
|
-
if (
|
|
2460
|
-
return extractFromReturnTypeAnnotation(
|
|
2069
|
+
const firstArg2 = typeArgs[0];
|
|
2070
|
+
if (firstArg2) {
|
|
2071
|
+
return extractFromReturnTypeAnnotation(firstArg2);
|
|
2461
2072
|
}
|
|
2462
2073
|
return null;
|
|
2463
2074
|
}
|
|
2464
2075
|
}
|
|
2465
|
-
if (
|
|
2076
|
+
if (Node8.isTypeLiteral(typeNode)) {
|
|
2466
2077
|
const properties = [];
|
|
2467
2078
|
for (const member of typeNode.getMembers()) {
|
|
2468
|
-
if (
|
|
2079
|
+
if (Node8.isPropertySignature(member)) {
|
|
2469
2080
|
const name = member.getName();
|
|
2470
2081
|
const memberTypeNode = member.getTypeNode();
|
|
2471
2082
|
const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
|
|
@@ -2479,19 +2090,19 @@ function extractFromReturnTypeAnnotation(typeNode) {
|
|
|
2479
2090
|
return null;
|
|
2480
2091
|
}
|
|
2481
2092
|
function extractFromBlockReturn(block) {
|
|
2482
|
-
if (!
|
|
2093
|
+
if (!Node8.isBlock(block)) return null;
|
|
2483
2094
|
const statements = block.getStatements();
|
|
2484
2095
|
for (let i = statements.length - 1; i >= 0; i--) {
|
|
2485
2096
|
const stmt = statements[i];
|
|
2486
|
-
if (!
|
|
2097
|
+
if (!Node8.isReturnStatement(stmt)) continue;
|
|
2487
2098
|
const expr = stmt.getExpression();
|
|
2488
2099
|
if (!expr) continue;
|
|
2489
|
-
if (
|
|
2100
|
+
if (Node8.isObjectLiteralExpression(expr)) {
|
|
2490
2101
|
return extractFromObjectLiteral(expr);
|
|
2491
2102
|
}
|
|
2492
|
-
if (
|
|
2103
|
+
if (Node8.isParenthesizedExpression(expr)) {
|
|
2493
2104
|
const inner = expr.getExpression();
|
|
2494
|
-
if (
|
|
2105
|
+
if (Node8.isObjectLiteralExpression(inner)) {
|
|
2495
2106
|
return extractFromObjectLiteral(inner);
|
|
2496
2107
|
}
|
|
2497
2108
|
}
|
|
@@ -2500,10 +2111,10 @@ function extractFromBlockReturn(block) {
|
|
|
2500
2111
|
return null;
|
|
2501
2112
|
}
|
|
2502
2113
|
function extractFromObjectLiteral(objLiteral) {
|
|
2503
|
-
if (!
|
|
2114
|
+
if (!Node8.isObjectLiteralExpression(objLiteral)) return null;
|
|
2504
2115
|
const properties = [];
|
|
2505
2116
|
for (const prop of objLiteral.getProperties()) {
|
|
2506
|
-
if (!
|
|
2117
|
+
if (!Node8.isPropertyAssignment(prop)) continue;
|
|
2507
2118
|
const name = prop.getName();
|
|
2508
2119
|
const initializer = prop.getInitializer();
|
|
2509
2120
|
if (!initializer) continue;
|
|
@@ -2515,21 +2126,21 @@ function extractFromObjectLiteral(objLiteral) {
|
|
|
2515
2126
|
return { typeString, properties, isImportRef: false };
|
|
2516
2127
|
}
|
|
2517
2128
|
function inferExpressionType(node) {
|
|
2518
|
-
if (
|
|
2519
|
-
if (
|
|
2129
|
+
if (Node8.isStringLiteral(node)) return "string";
|
|
2130
|
+
if (Node8.isTemplateExpression(node) || Node8.isNoSubstitutionTemplateLiteral(node))
|
|
2520
2131
|
return "string";
|
|
2521
|
-
if (
|
|
2522
|
-
if (node.getKind() ===
|
|
2132
|
+
if (Node8.isNumericLiteral(node)) return "number";
|
|
2133
|
+
if (node.getKind() === SyntaxKind4.TrueKeyword || node.getKind() === SyntaxKind4.FalseKeyword) {
|
|
2523
2134
|
return "boolean";
|
|
2524
2135
|
}
|
|
2525
|
-
if (node.getKind() ===
|
|
2526
|
-
if (
|
|
2527
|
-
if (
|
|
2136
|
+
if (node.getKind() === SyntaxKind4.NullKeyword) return "null";
|
|
2137
|
+
if (Node8.isIdentifier(node) && node.getText() === "undefined") return "undefined";
|
|
2138
|
+
if (Node8.isObjectLiteralExpression(node)) {
|
|
2528
2139
|
const props = node.getProperties();
|
|
2529
2140
|
if (props.length === 0) return "Record<string, unknown>";
|
|
2530
2141
|
const entries = [];
|
|
2531
2142
|
for (const prop of props) {
|
|
2532
|
-
if (!
|
|
2143
|
+
if (!Node8.isPropertyAssignment(prop)) continue;
|
|
2533
2144
|
const key = prop.getName();
|
|
2534
2145
|
const init = prop.getInitializer();
|
|
2535
2146
|
if (!init) continue;
|
|
@@ -2538,23 +2149,23 @@ function inferExpressionType(node) {
|
|
|
2538
2149
|
if (entries.length === 0) return "Record<string, unknown>";
|
|
2539
2150
|
return `{ ${entries.join("; ")} }`;
|
|
2540
2151
|
}
|
|
2541
|
-
if (
|
|
2152
|
+
if (Node8.isArrayLiteralExpression(node)) {
|
|
2542
2153
|
const elements = node.getElements();
|
|
2543
2154
|
if (elements.length === 0) return "Array<unknown>";
|
|
2544
2155
|
const first = elements[0];
|
|
2545
2156
|
if (first) return `Array<${inferExpressionType(first)}>`;
|
|
2546
2157
|
return "Array<unknown>";
|
|
2547
2158
|
}
|
|
2548
|
-
if (
|
|
2159
|
+
if (Node8.isConditionalExpression(node)) {
|
|
2549
2160
|
const whenTrue = inferExpressionType(node.getWhenTrue());
|
|
2550
2161
|
const whenFalse = inferExpressionType(node.getWhenFalse());
|
|
2551
2162
|
if (whenTrue === whenFalse) return whenTrue;
|
|
2552
2163
|
return `${whenTrue} | ${whenFalse}`;
|
|
2553
2164
|
}
|
|
2554
|
-
if (
|
|
2165
|
+
if (Node8.isParenthesizedExpression(node)) {
|
|
2555
2166
|
return inferExpressionType(node.getExpression());
|
|
2556
2167
|
}
|
|
2557
|
-
if (
|
|
2168
|
+
if (Node8.isAsExpression(node)) {
|
|
2558
2169
|
const typeNode = node.getTypeNode();
|
|
2559
2170
|
if (typeNode) return typeNode.getText();
|
|
2560
2171
|
}
|
|
@@ -2563,25 +2174,14 @@ function inferExpressionType(node) {
|
|
|
2563
2174
|
|
|
2564
2175
|
// src/emit/emit-api.ts
|
|
2565
2176
|
import { mkdir, writeFile } from "fs/promises";
|
|
2566
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2177
|
+
import { isAbsolute as isAbsolute2, join as join5, relative as relative3 } from "path";
|
|
2567
2178
|
|
|
2568
2179
|
// src/extension/registry.ts
|
|
2569
|
-
import { Project as
|
|
2180
|
+
import { Project as Project3 } from "ts-morph";
|
|
2570
2181
|
function resolveApiSlots(extensions) {
|
|
2571
|
-
let transport;
|
|
2572
|
-
let transportOwner;
|
|
2573
2182
|
let layer;
|
|
2574
2183
|
let layerOwner;
|
|
2575
2184
|
for (const ext of extensions) {
|
|
2576
|
-
if (ext.apiTransport) {
|
|
2577
|
-
if (transport) {
|
|
2578
|
-
throw new CodegenError(
|
|
2579
|
-
`api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
|
|
2580
|
-
);
|
|
2581
|
-
}
|
|
2582
|
-
transport = ext.apiTransport;
|
|
2583
|
-
transportOwner = ext.name;
|
|
2584
|
-
}
|
|
2585
2185
|
if (ext.apiClientLayer) {
|
|
2586
2186
|
if (layer) {
|
|
2587
2187
|
throw new CodegenError(
|
|
@@ -2593,11 +2193,22 @@ function resolveApiSlots(extensions) {
|
|
|
2593
2193
|
}
|
|
2594
2194
|
}
|
|
2595
2195
|
return {
|
|
2596
|
-
...transport ? { transport } : {},
|
|
2597
2196
|
...layer ? { layer } : {}
|
|
2598
2197
|
};
|
|
2599
2198
|
}
|
|
2600
2199
|
var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
|
|
2200
|
+
function mergeExclusive(target, incoming, {
|
|
2201
|
+
owner,
|
|
2202
|
+
describe
|
|
2203
|
+
}) {
|
|
2204
|
+
for (const [key, value] of incoming) {
|
|
2205
|
+
const prev = target.get(key);
|
|
2206
|
+
if (prev !== void 0) {
|
|
2207
|
+
throw new CodegenError(describe(key, prev.owner, owner));
|
|
2208
|
+
}
|
|
2209
|
+
target.set(key, { value, owner });
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2601
2212
|
function createExtensionContext(config, getRoutes) {
|
|
2602
2213
|
let project;
|
|
2603
2214
|
return {
|
|
@@ -2609,7 +2220,7 @@ function createExtensionContext(config, getRoutes) {
|
|
|
2609
2220
|
},
|
|
2610
2221
|
project() {
|
|
2611
2222
|
if (!project) {
|
|
2612
|
-
project = new
|
|
2223
|
+
project = new Project3({
|
|
2613
2224
|
skipAddingFilesFromTsConfig: true,
|
|
2614
2225
|
skipLoadingLibFiles: true,
|
|
2615
2226
|
skipFileDependencyResolution: true,
|
|
@@ -2642,29 +2253,36 @@ async function collectEmittedFiles(extensions, ctx) {
|
|
|
2642
2253
|
`Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
|
|
2643
2254
|
);
|
|
2644
2255
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
);
|
|
2650
|
-
}
|
|
2651
|
-
owners.set(key, ext.name);
|
|
2256
|
+
mergeExclusive(owners, [[key, file]], {
|
|
2257
|
+
owner: ext.name,
|
|
2258
|
+
describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
|
|
2259
|
+
});
|
|
2652
2260
|
files.push(file);
|
|
2653
2261
|
}
|
|
2654
2262
|
}
|
|
2655
2263
|
return files;
|
|
2656
2264
|
}
|
|
2657
2265
|
|
|
2266
|
+
// src/extension/types.ts
|
|
2267
|
+
function requestShape(route) {
|
|
2268
|
+
const cs = route.contract?.contractSource;
|
|
2269
|
+
const isGet = route.method.toUpperCase() === "GET";
|
|
2270
|
+
const isQuery = isGet || !!cs?.filterFields?.length;
|
|
2271
|
+
const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
|
|
2272
|
+
const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
|
|
2273
|
+
return { isGet, isQuery, hasBody, hasQuery };
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2658
2276
|
// src/emit/emit-api.ts
|
|
2659
2277
|
async function emitApi(routes, outDir, opts = {}) {
|
|
2660
2278
|
await mkdir(outDir, { recursive: true });
|
|
2661
2279
|
const content = buildApiFile(routes, outDir, opts);
|
|
2662
|
-
await writeFile(
|
|
2280
|
+
await writeFile(join5(outDir, "api.ts"), content, "utf8");
|
|
2663
2281
|
}
|
|
2664
2282
|
function splitName(name) {
|
|
2665
2283
|
return name.split(".");
|
|
2666
2284
|
}
|
|
2667
|
-
function
|
|
2285
|
+
function toObjectKey(segment) {
|
|
2668
2286
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
|
|
2669
2287
|
return segment;
|
|
2670
2288
|
}
|
|
@@ -2771,7 +2389,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2771
2389
|
const pad = " ".repeat(indent);
|
|
2772
2390
|
const lines = [];
|
|
2773
2391
|
for (const [key, node] of tree) {
|
|
2774
|
-
const objKey =
|
|
2392
|
+
const objKey = toObjectKey(key);
|
|
2775
2393
|
if (node.kind === "leaf") {
|
|
2776
2394
|
const c = node;
|
|
2777
2395
|
const method = c.method.toUpperCase();
|
|
@@ -2797,15 +2415,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2797
2415
|
return lines;
|
|
2798
2416
|
}
|
|
2799
2417
|
function buildRequestModel(c) {
|
|
2800
|
-
const isGet = c.method.toUpperCase() === "GET";
|
|
2801
2418
|
const m = c.method.toLowerCase();
|
|
2802
2419
|
const flat = JSON.stringify(c.name);
|
|
2803
2420
|
const path = JSON.stringify(c.path);
|
|
2804
2421
|
const TA = buildRouterTypeAccess(c.name);
|
|
2805
2422
|
const withParams = hasPathParams(c.params);
|
|
2806
|
-
const
|
|
2807
|
-
const isQuery = isGet || !!c.contractSource.filterFields?.length;
|
|
2808
|
-
const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
|
|
2423
|
+
const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
|
|
2809
2424
|
const fields = [];
|
|
2810
2425
|
if (withParams) fields.push(`params: ${TA}['params']`);
|
|
2811
2426
|
if (hasQuery) fields.push(`query?: ${TA}['query']`);
|
|
@@ -2827,7 +2442,6 @@ function buildRequestModel(c) {
|
|
|
2827
2442
|
urlExpr,
|
|
2828
2443
|
optsExpr,
|
|
2829
2444
|
responseType: `${TA}['response']`,
|
|
2830
|
-
bodyType: `${TA}['body']`,
|
|
2831
2445
|
queryKeyExpr: `[${flat}, input] as const`
|
|
2832
2446
|
};
|
|
2833
2447
|
}
|
|
@@ -2875,7 +2489,7 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2875
2489
|
const pad = " ".repeat(indent);
|
|
2876
2490
|
const lines = [];
|
|
2877
2491
|
for (const [key, node] of tree) {
|
|
2878
|
-
const objKey =
|
|
2492
|
+
const objKey = toObjectKey(key);
|
|
2879
2493
|
if (node.kind === "branch") {
|
|
2880
2494
|
lines.push(`${pad}${objKey}: {`);
|
|
2881
2495
|
lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
|
|
@@ -2883,30 +2497,28 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2883
2497
|
continue;
|
|
2884
2498
|
}
|
|
2885
2499
|
const req = buildRequestModel(node);
|
|
2886
|
-
const
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
params: node.params,
|
|
2891
|
-
contract: { contractSource: node.contractSource },
|
|
2892
|
-
...node.controllerRef ? { controllerRef: node.controllerRef } : {}
|
|
2500
|
+
const leaf = {
|
|
2501
|
+
route: node.route,
|
|
2502
|
+
request: req,
|
|
2503
|
+
requestExpr: renderFetcherRequest(req)
|
|
2893
2504
|
};
|
|
2894
|
-
const
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2505
|
+
const owned = /* @__PURE__ */ new Map();
|
|
2506
|
+
if (p.layer) {
|
|
2507
|
+
mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
|
|
2508
|
+
owner: p.layer.name,
|
|
2509
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2898
2512
|
for (const ext of p.memberExts) {
|
|
2899
2513
|
const extra = ext.apiMembers?.(leaf, p.ctx);
|
|
2900
2514
|
if (!extra) continue;
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
);
|
|
2906
|
-
}
|
|
2907
|
-
members[name] = value;
|
|
2908
|
-
}
|
|
2515
|
+
mergeExclusive(owned, Object.entries(extra), {
|
|
2516
|
+
owner: ext.name,
|
|
2517
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
2518
|
+
});
|
|
2909
2519
|
}
|
|
2520
|
+
const members = {};
|
|
2521
|
+
for (const [name, { value }] of owned) members[name] = value;
|
|
2910
2522
|
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
|
|
2911
2523
|
}
|
|
2912
2524
|
return lines;
|
|
@@ -2915,10 +2527,82 @@ function buildRouterTypeAccess(name) {
|
|
|
2915
2527
|
const segments = splitName(name);
|
|
2916
2528
|
return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
|
|
2917
2529
|
}
|
|
2530
|
+
var RESOLVER_HELPERS = [
|
|
2531
|
+
// --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
|
|
2532
|
+
"type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
|
|
2533
|
+
" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
|
|
2534
|
+
" : P extends keyof R ? R[P] : never;",
|
|
2535
|
+
"",
|
|
2536
|
+
// --- ResolveByName: resolve a field from a dot-path name ---
|
|
2537
|
+
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
|
|
2538
|
+
"",
|
|
2539
|
+
// --- ResolveByPath: scan all leaves for matching method + url ---
|
|
2540
|
+
// Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
|
|
2541
|
+
"type _LeafValues<T> = T extends { method: string; url: string }",
|
|
2542
|
+
" ? T",
|
|
2543
|
+
" : T extends object ? _LeafValues<T[keyof T]> : never;",
|
|
2544
|
+
"",
|
|
2545
|
+
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
|
|
2546
|
+
" ? L extends { method: M; url: U }",
|
|
2547
|
+
" ? Field extends keyof L ? L[Field] : never",
|
|
2548
|
+
" : never",
|
|
2549
|
+
" : never;",
|
|
2550
|
+
""
|
|
2551
|
+
];
|
|
2552
|
+
var ROUTE_NAMESPACE = [
|
|
2553
|
+
"export namespace Route {",
|
|
2554
|
+
' export type Response<K extends string> = ResolveByName<K, "response">;',
|
|
2555
|
+
' export type Body<K extends string> = ResolveByName<K, "body">;',
|
|
2556
|
+
' export type Query<K extends string> = ResolveByName<K, "query">;',
|
|
2557
|
+
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
2558
|
+
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
2559
|
+
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
2560
|
+
" export type Request<K extends string> = {",
|
|
2561
|
+
" body: Body<K>;",
|
|
2562
|
+
" query: Query<K>;",
|
|
2563
|
+
" params: Params<K>;",
|
|
2564
|
+
" };",
|
|
2565
|
+
"}",
|
|
2566
|
+
""
|
|
2567
|
+
];
|
|
2568
|
+
var PATH_NAMESPACE = [
|
|
2569
|
+
"export namespace Path {",
|
|
2570
|
+
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
2571
|
+
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
|
|
2572
|
+
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
|
|
2573
|
+
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
2574
|
+
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
2575
|
+
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
2576
|
+
"}",
|
|
2577
|
+
""
|
|
2578
|
+
];
|
|
2579
|
+
var EMPTY_ROUTE_NAMESPACE = [
|
|
2580
|
+
"export namespace Route {",
|
|
2581
|
+
" export type Response<K extends string> = never;",
|
|
2582
|
+
" export type Body<K extends string> = never;",
|
|
2583
|
+
" export type Query<K extends string> = never;",
|
|
2584
|
+
" export type Params<K extends string> = never;",
|
|
2585
|
+
" export type Error<K extends string> = never;",
|
|
2586
|
+
" export type FilterFields<K extends string> = never;",
|
|
2587
|
+
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
2588
|
+
"}",
|
|
2589
|
+
""
|
|
2590
|
+
];
|
|
2591
|
+
var EMPTY_PATH_NAMESPACE = [
|
|
2592
|
+
"export namespace Path {",
|
|
2593
|
+
" export type Response<M extends string, U extends string> = never;",
|
|
2594
|
+
" export type Body<M extends string, U extends string> = never;",
|
|
2595
|
+
" export type Query<M extends string, U extends string> = never;",
|
|
2596
|
+
" export type Params<M extends string, U extends string> = never;",
|
|
2597
|
+
" export type Error<M extends string, U extends string> = never;",
|
|
2598
|
+
" export type FilterFields<M extends string, U extends string> = never;",
|
|
2599
|
+
"}",
|
|
2600
|
+
""
|
|
2601
|
+
];
|
|
2918
2602
|
function buildApiFile(routes, outDir, opts = {}) {
|
|
2919
2603
|
const fetcherImportPath = opts.fetcherImportPath;
|
|
2920
2604
|
const extensions = opts.extensions ?? [];
|
|
2921
|
-
const {
|
|
2605
|
+
const { layer } = resolveApiSlots(extensions);
|
|
2922
2606
|
const memberExts = extensions.filter((e) => e.apiMembers);
|
|
2923
2607
|
const headerExts = extensions.filter((e) => e.apiHeader);
|
|
2924
2608
|
const contracted = routes.filter((r) => r.contract);
|
|
@@ -2963,7 +2647,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
2963
2647
|
seenImports.add(imp);
|
|
2964
2648
|
extImports.push(imp);
|
|
2965
2649
|
};
|
|
2966
|
-
for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
|
|
2967
2650
|
for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
|
|
2968
2651
|
for (const ext of headerExts) {
|
|
2969
2652
|
for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
|
|
@@ -3008,27 +2691,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3008
2691
|
lines.push("}");
|
|
3009
2692
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
3010
2693
|
lines.push("");
|
|
3011
|
-
lines.push(
|
|
3012
|
-
lines.push(
|
|
3013
|
-
lines.push(" export type Body<K extends string> = never;");
|
|
3014
|
-
lines.push(" export type Query<K extends string> = never;");
|
|
3015
|
-
lines.push(" export type Params<K extends string> = never;");
|
|
3016
|
-
lines.push(" export type Error<K extends string> = never;");
|
|
3017
|
-
lines.push(" export type FilterFields<K extends string> = never;");
|
|
3018
|
-
lines.push(
|
|
3019
|
-
" export type Request<K extends string> = { body: never; query: never; params: never };"
|
|
3020
|
-
);
|
|
3021
|
-
lines.push("}");
|
|
3022
|
-
lines.push("");
|
|
3023
|
-
lines.push("export namespace Path {");
|
|
3024
|
-
lines.push(" export type Response<M extends string, U extends string> = never;");
|
|
3025
|
-
lines.push(" export type Body<M extends string, U extends string> = never;");
|
|
3026
|
-
lines.push(" export type Query<M extends string, U extends string> = never;");
|
|
3027
|
-
lines.push(" export type Params<M extends string, U extends string> = never;");
|
|
3028
|
-
lines.push(" export type Error<M extends string, U extends string> = never;");
|
|
3029
|
-
lines.push(" export type FilterFields<M extends string, U extends string> = never;");
|
|
3030
|
-
lines.push("}");
|
|
3031
|
-
lines.push("");
|
|
2694
|
+
lines.push(...EMPTY_ROUTE_NAMESPACE);
|
|
2695
|
+
lines.push(...EMPTY_PATH_NAMESPACE);
|
|
3032
2696
|
return lines.join("\n");
|
|
3033
2697
|
}
|
|
3034
2698
|
const tree = /* @__PURE__ */ new Map();
|
|
@@ -3046,7 +2710,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3046
2710
|
path: r.path,
|
|
3047
2711
|
params: r.params,
|
|
3048
2712
|
controllerRef: r.controllerRef,
|
|
3049
|
-
contractSource: c.contractSource
|
|
2713
|
+
contractSource: c.contractSource,
|
|
2714
|
+
route: r
|
|
3050
2715
|
};
|
|
3051
2716
|
insertIntoTree(tree, segments, leaf, name);
|
|
3052
2717
|
}
|
|
@@ -3059,7 +2724,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3059
2724
|
lines.push(" return {");
|
|
3060
2725
|
lines.push(
|
|
3061
2726
|
...emitApiObjectBlock(tree, 4, {
|
|
3062
|
-
...transport ? { transport } : {},
|
|
3063
2727
|
...layer ? { layer } : {},
|
|
3064
2728
|
memberExts,
|
|
3065
2729
|
ctx
|
|
@@ -3070,61 +2734,9 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3070
2734
|
lines.push("");
|
|
3071
2735
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
3072
2736
|
lines.push("");
|
|
3073
|
-
lines.push(
|
|
3074
|
-
lines.push(
|
|
3075
|
-
lines.push(
|
|
3076
|
-
lines.push("");
|
|
3077
|
-
lines.push(
|
|
3078
|
-
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
|
|
3079
|
-
);
|
|
3080
|
-
lines.push("");
|
|
3081
|
-
lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
|
|
3082
|
-
lines.push(" ? T");
|
|
3083
|
-
lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
|
|
3084
|
-
lines.push("");
|
|
3085
|
-
lines.push(
|
|
3086
|
-
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
|
|
3087
|
-
);
|
|
3088
|
-
lines.push(" ? L extends { method: M; url: U }");
|
|
3089
|
-
lines.push(" ? Field extends keyof L ? L[Field] : never");
|
|
3090
|
-
lines.push(" : never");
|
|
3091
|
-
lines.push(" : never;");
|
|
3092
|
-
lines.push("");
|
|
3093
|
-
lines.push("export namespace Route {");
|
|
3094
|
-
lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
|
|
3095
|
-
lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
|
|
3096
|
-
lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
|
|
3097
|
-
lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
|
|
3098
|
-
lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
|
|
3099
|
-
lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
|
|
3100
|
-
lines.push(" export type Request<K extends string> = {");
|
|
3101
|
-
lines.push(" body: Body<K>;");
|
|
3102
|
-
lines.push(" query: Query<K>;");
|
|
3103
|
-
lines.push(" params: Params<K>;");
|
|
3104
|
-
lines.push(" };");
|
|
3105
|
-
lines.push("}");
|
|
3106
|
-
lines.push("");
|
|
3107
|
-
lines.push("export namespace Path {");
|
|
3108
|
-
lines.push(
|
|
3109
|
-
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
|
|
3110
|
-
);
|
|
3111
|
-
lines.push(
|
|
3112
|
-
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
|
|
3113
|
-
);
|
|
3114
|
-
lines.push(
|
|
3115
|
-
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
|
|
3116
|
-
);
|
|
3117
|
-
lines.push(
|
|
3118
|
-
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
|
|
3119
|
-
);
|
|
3120
|
-
lines.push(
|
|
3121
|
-
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
|
|
3122
|
-
);
|
|
3123
|
-
lines.push(
|
|
3124
|
-
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
|
|
3125
|
-
);
|
|
3126
|
-
lines.push("}");
|
|
3127
|
-
lines.push("");
|
|
2737
|
+
lines.push(...RESOLVER_HELPERS);
|
|
2738
|
+
lines.push(...ROUTE_NAMESPACE);
|
|
2739
|
+
lines.push(...PATH_NAMESPACE);
|
|
3128
2740
|
for (const ext of headerExts) {
|
|
3129
2741
|
const statements = ext.apiHeader?.(ctx)?.statements;
|
|
3130
2742
|
if (statements?.length) {
|
|
@@ -3136,7 +2748,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3136
2748
|
|
|
3137
2749
|
// src/emit/emit-cache.ts
|
|
3138
2750
|
import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
3139
|
-
import { join as
|
|
2751
|
+
import { join as join6 } from "path";
|
|
3140
2752
|
async function emitCache(pages, outDir) {
|
|
3141
2753
|
await mkdir2(outDir, { recursive: true });
|
|
3142
2754
|
const entries = await Promise.all(
|
|
@@ -3150,95 +2762,21 @@ async function emitCache(pages, outDir) {
|
|
|
3150
2762
|
})
|
|
3151
2763
|
);
|
|
3152
2764
|
const cache = { pages: entries };
|
|
3153
|
-
await writeFile2(
|
|
2765
|
+
await writeFile2(join6(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
|
|
3154
2766
|
`, "utf8");
|
|
3155
2767
|
}
|
|
3156
2768
|
|
|
3157
2769
|
// src/emit/emit-forms.ts
|
|
3158
2770
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
3159
|
-
import { join as
|
|
2771
|
+
import { join as join7, relative as relative4 } from "path";
|
|
3160
2772
|
async function emitForms(routes, outDir, config, adapter) {
|
|
3161
2773
|
if (config && config.enabled === false) return false;
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
if (content2 === null) return false;
|
|
3165
|
-
await mkdir3(outDir, { recursive: true });
|
|
3166
|
-
await writeFile3(join6(outDir, "forms.ts"), content2, "utf8");
|
|
3167
|
-
return true;
|
|
3168
|
-
}
|
|
3169
|
-
const entries = collectFormEntries(routes);
|
|
3170
|
-
if (entries.length === 0) return false;
|
|
2774
|
+
const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
|
|
2775
|
+
if (content === null) return false;
|
|
3171
2776
|
await mkdir3(outDir, { recursive: true });
|
|
3172
|
-
|
|
3173
|
-
await writeFile3(join6(outDir, "forms.ts"), content, "utf8");
|
|
2777
|
+
await writeFile3(join7(outDir, "forms.ts"), content, "utf8");
|
|
3174
2778
|
return true;
|
|
3175
2779
|
}
|
|
3176
|
-
function buildFormsFileWithAdapter(routes, adapter) {
|
|
3177
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
3178
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
3179
|
-
for (const route of sorted) {
|
|
3180
|
-
const cs = route.contract.contractSource;
|
|
3181
|
-
if (!cs.bodySchema && !cs.querySchema) continue;
|
|
3182
|
-
methodNameCounts.set(
|
|
3183
|
-
deriveBaseName(route.name).method,
|
|
3184
|
-
(methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
|
|
3185
|
-
);
|
|
3186
|
-
}
|
|
3187
|
-
const named = /* @__PURE__ */ new Map();
|
|
3188
|
-
const decls = [];
|
|
3189
|
-
const mapEntries = [];
|
|
3190
|
-
let used = false;
|
|
3191
|
-
for (const route of sorted) {
|
|
3192
|
-
const cs = route.contract.contractSource;
|
|
3193
|
-
const { method, full } = deriveBaseName(route.name);
|
|
3194
|
-
const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
|
|
3195
|
-
const block = [];
|
|
3196
|
-
if (cs.bodyZodText && !cs.bodySchema) {
|
|
3197
|
-
block.push(
|
|
3198
|
-
`// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
3199
|
-
);
|
|
3200
|
-
}
|
|
3201
|
-
let bodyConst;
|
|
3202
|
-
if (cs.bodySchema) {
|
|
3203
|
-
used = true;
|
|
3204
|
-
const r = adapter.renderModule(cs.bodySchema);
|
|
3205
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
3206
|
-
bodyConst = `${base}BodySchema`;
|
|
3207
|
-
block.push(`export const ${bodyConst} = ${r.schemaText};`);
|
|
3208
|
-
block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
|
|
3209
|
-
}
|
|
3210
|
-
if (cs.querySchema) {
|
|
3211
|
-
used = true;
|
|
3212
|
-
const r = adapter.renderModule(cs.querySchema);
|
|
3213
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
3214
|
-
const queryConst = `${base}QuerySchema`;
|
|
3215
|
-
block.push(`export const ${queryConst} = ${r.schemaText};`);
|
|
3216
|
-
block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
|
|
3217
|
-
}
|
|
3218
|
-
if (block.length === 0) continue;
|
|
3219
|
-
decls.push(`// ${route.name}`, ...block, "");
|
|
3220
|
-
if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
|
|
3221
|
-
}
|
|
3222
|
-
if (!used) return null;
|
|
3223
|
-
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
3224
|
-
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
3225
|
-
lines.push("");
|
|
3226
|
-
if (named.size > 0) {
|
|
3227
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
3228
|
-
for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
|
|
3229
|
-
lines.push("");
|
|
3230
|
-
}
|
|
3231
|
-
lines.push(...decls);
|
|
3232
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
3233
|
-
lines.push("export const formSchemas = {");
|
|
3234
|
-
lines.push(...mapEntries);
|
|
3235
|
-
lines.push("} as const;");
|
|
3236
|
-
lines.push("");
|
|
3237
|
-
return lines.join("\n");
|
|
3238
|
-
}
|
|
3239
|
-
function hasSchema(src) {
|
|
3240
|
-
return !!src && (src.ref !== null || src.text !== null);
|
|
3241
|
-
}
|
|
3242
2780
|
function pascal(segment) {
|
|
3243
2781
|
return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
3244
2782
|
}
|
|
@@ -3248,37 +2786,6 @@ function deriveBaseName(routeName) {
|
|
|
3248
2786
|
const full = segments.map(pascal).join("");
|
|
3249
2787
|
return { method, full };
|
|
3250
2788
|
}
|
|
3251
|
-
function collectFormEntries(routes) {
|
|
3252
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
3253
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
3254
|
-
const candidates = [];
|
|
3255
|
-
for (const route of sorted) {
|
|
3256
|
-
const cs = route.contract.contractSource;
|
|
3257
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
3258
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
3259
|
-
if (!hasSchema(body) && !hasSchema(query)) continue;
|
|
3260
|
-
const { method, full } = deriveBaseName(route.name);
|
|
3261
|
-
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
3262
|
-
candidates.push({ route, method, full });
|
|
3263
|
-
}
|
|
3264
|
-
const entries = [];
|
|
3265
|
-
for (const { route, method, full } of candidates) {
|
|
3266
|
-
const cs = route.contract.contractSource;
|
|
3267
|
-
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
3268
|
-
const baseName = collision ? full : method;
|
|
3269
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
3270
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
3271
|
-
entries.push({
|
|
3272
|
-
routeName: route.name,
|
|
3273
|
-
baseName,
|
|
3274
|
-
body: hasSchema(body) ? body : void 0,
|
|
3275
|
-
query: hasSchema(query) ? query : void 0,
|
|
3276
|
-
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
3277
|
-
warnings: cs.formWarnings ?? []
|
|
3278
|
-
});
|
|
3279
|
-
}
|
|
3280
|
-
return entries;
|
|
3281
|
-
}
|
|
3282
2789
|
function relImport(outDir, filePath) {
|
|
3283
2790
|
let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
|
|
3284
2791
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
@@ -3287,85 +2794,8 @@ function relImport(outDir, filePath) {
|
|
|
3287
2794
|
function refRootIdentifier(refName) {
|
|
3288
2795
|
return refName.split(".")[0] ?? refName;
|
|
3289
2796
|
}
|
|
3290
|
-
function
|
|
3291
|
-
|
|
3292
|
-
const lines = [
|
|
3293
|
-
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
3294
|
-
`import { z } from '${zodImport}';`
|
|
3295
|
-
];
|
|
3296
|
-
const importsByFile = /* @__PURE__ */ new Map();
|
|
3297
|
-
const refAlias = /* @__PURE__ */ new Map();
|
|
3298
|
-
for (const entry of entries) {
|
|
3299
|
-
for (const src of [entry.body, entry.query]) {
|
|
3300
|
-
if (src?.ref && !src.text) {
|
|
3301
|
-
const root = refRootIdentifier(src.ref.name);
|
|
3302
|
-
const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
|
|
3303
|
-
set.add(root);
|
|
3304
|
-
importsByFile.set(src.ref.filePath, set);
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
if (importsByFile.size > 0) {
|
|
3309
|
-
const emitted = /* @__PURE__ */ new Set();
|
|
3310
|
-
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
3311
|
-
const relPath = relImport(outDir, filePath);
|
|
3312
|
-
const specifiers = [];
|
|
3313
|
-
for (const root of [...roots].sort()) {
|
|
3314
|
-
if (emitted.has(root)) {
|
|
3315
|
-
const alias = `${root}_${emitted.size}`;
|
|
3316
|
-
specifiers.push(`${root} as ${alias}`);
|
|
3317
|
-
emitted.add(alias);
|
|
3318
|
-
refAlias.set(`${filePath}\0${root}`, alias);
|
|
3319
|
-
} else {
|
|
3320
|
-
specifiers.push(root);
|
|
3321
|
-
emitted.add(root);
|
|
3322
|
-
refAlias.set(`${filePath}\0${root}`, root);
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
lines.push("");
|
|
3329
|
-
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
3330
|
-
if (globalSchemas.size > 0) {
|
|
3331
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
3332
|
-
for (const [name, text] of globalSchemas) {
|
|
3333
|
-
lines.push(`const ${name} = ${text};`);
|
|
3334
|
-
}
|
|
3335
|
-
lines.push("");
|
|
3336
|
-
}
|
|
3337
|
-
const mapEntries = [];
|
|
3338
|
-
for (const entry of entries) {
|
|
3339
|
-
lines.push(`// ${entry.routeName}`);
|
|
3340
|
-
if (entry.warnings && entry.warnings.length > 0) {
|
|
3341
|
-
for (const w of entry.warnings) {
|
|
3342
|
-
lines.push(`// warning: ${w}`);
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
const rename = renamesByEntry.get(entry) ?? null;
|
|
3346
|
-
if (entry.body) {
|
|
3347
|
-
const schemaName = `${entry.baseName}BodySchema`;
|
|
3348
|
-
const typeName = `${entry.baseName}Body`;
|
|
3349
|
-
const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
|
|
3350
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
3351
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
3352
|
-
mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
|
|
3353
|
-
}
|
|
3354
|
-
if (entry.query) {
|
|
3355
|
-
const schemaName = `${entry.baseName}QuerySchema`;
|
|
3356
|
-
const typeName = `${entry.baseName}Query`;
|
|
3357
|
-
const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
|
|
3358
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
3359
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
3360
|
-
}
|
|
3361
|
-
lines.push("");
|
|
3362
|
-
}
|
|
3363
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
3364
|
-
lines.push("export const formSchemas = {");
|
|
3365
|
-
lines.push(...mapEntries);
|
|
3366
|
-
lines.push("} as const;");
|
|
3367
|
-
lines.push("");
|
|
3368
|
-
return lines.join("\n");
|
|
2797
|
+
function hasSource(src) {
|
|
2798
|
+
return !!(src.schema || src.zodText || src.zodRef);
|
|
3369
2799
|
}
|
|
3370
2800
|
function applyRenames(text, renames) {
|
|
3371
2801
|
if (!renames || renames.size === 0) return text;
|
|
@@ -3431,20 +2861,182 @@ function planNestedSchemas(entries) {
|
|
|
3431
2861
|
}
|
|
3432
2862
|
return { globalSchemas, renamesByEntry };
|
|
3433
2863
|
}
|
|
3434
|
-
function
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
2864
|
+
function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
|
|
2865
|
+
const acceptsRawZod = adapter.acceptsRawZodSource === true;
|
|
2866
|
+
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
2867
|
+
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
2868
|
+
const candidates = [];
|
|
2869
|
+
for (const route of sorted) {
|
|
2870
|
+
const cs = route.contract.contractSource;
|
|
2871
|
+
const body = {
|
|
2872
|
+
schema: cs.bodySchema ?? null,
|
|
2873
|
+
zodText: cs.bodyZodText ?? null,
|
|
2874
|
+
zodRef: cs.bodyZodRef ?? null
|
|
2875
|
+
};
|
|
2876
|
+
const query = {
|
|
2877
|
+
schema: cs.querySchema ?? null,
|
|
2878
|
+
zodText: cs.queryZodText ?? null,
|
|
2879
|
+
zodRef: cs.queryZodRef ?? null
|
|
2880
|
+
};
|
|
2881
|
+
if (!hasSource(body) && !hasSource(query)) continue;
|
|
2882
|
+
const { method, full } = deriveBaseName(route.name);
|
|
2883
|
+
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
2884
|
+
candidates.push({
|
|
2885
|
+
routeName: route.name,
|
|
2886
|
+
baseName: full,
|
|
2887
|
+
// resolved below
|
|
2888
|
+
body: hasSource(body) ? body : void 0,
|
|
2889
|
+
query: hasSource(query) ? query : void 0,
|
|
2890
|
+
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
2891
|
+
warnings: cs.formWarnings ?? []
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
const entries = candidates.map((c) => {
|
|
2895
|
+
const { method, full } = deriveBaseName(c.routeName);
|
|
2896
|
+
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
2897
|
+
return { ...c, baseName: collision ? full : method };
|
|
2898
|
+
});
|
|
2899
|
+
if (entries.length === 0) return null;
|
|
2900
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
2901
|
+
const refAlias = /* @__PURE__ */ new Map();
|
|
2902
|
+
for (const entry of entries) {
|
|
2903
|
+
for (const src of [entry.body, entry.query]) {
|
|
2904
|
+
if (src?.zodRef && !src.zodText && !src.schema) {
|
|
2905
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
2906
|
+
const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
|
|
2907
|
+
set.add(root);
|
|
2908
|
+
importsByFile.set(src.zodRef.filePath, set);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
const importLines = [];
|
|
2913
|
+
if (importsByFile.size > 0) {
|
|
2914
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2915
|
+
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
2916
|
+
const relPath = relImport(outDir, filePath);
|
|
2917
|
+
const specifiers = [];
|
|
2918
|
+
for (const root of [...roots].sort()) {
|
|
2919
|
+
if (emitted.has(root)) {
|
|
2920
|
+
const alias = `${root}_${emitted.size}`;
|
|
2921
|
+
specifiers.push(`${root} as ${alias}`);
|
|
2922
|
+
emitted.add(alias);
|
|
2923
|
+
refAlias.set(`${filePath}\0${root}`, alias);
|
|
2924
|
+
} else {
|
|
2925
|
+
specifiers.push(root);
|
|
2926
|
+
emitted.add(root);
|
|
2927
|
+
refAlias.set(`${filePath}\0${root}`, root);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
2934
|
+
const irNamed = /* @__PURE__ */ new Map();
|
|
2935
|
+
const irTypeAliases = /* @__PURE__ */ new Map();
|
|
2936
|
+
const irAnnotations = /* @__PURE__ */ new Map();
|
|
2937
|
+
const decls = [];
|
|
2938
|
+
const mapEntries = [];
|
|
2939
|
+
let used = false;
|
|
2940
|
+
const renderSource = (src, rename) => {
|
|
2941
|
+
if (src.schema) {
|
|
2942
|
+
const r = adapter.renderModule(src.schema);
|
|
2943
|
+
for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
|
|
2944
|
+
if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
|
|
2945
|
+
if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
|
|
2946
|
+
return { text: r.schemaText };
|
|
2947
|
+
}
|
|
2948
|
+
if (src.zodText) {
|
|
2949
|
+
if (!acceptsRawZod) {
|
|
2950
|
+
return {
|
|
2951
|
+
text: "",
|
|
2952
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
return { text: applyRenames(src.zodText, rename) };
|
|
2956
|
+
}
|
|
2957
|
+
if (src.zodRef) {
|
|
2958
|
+
if (!acceptsRawZod) {
|
|
2959
|
+
return {
|
|
2960
|
+
text: "",
|
|
2961
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
2965
|
+
const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
|
|
2966
|
+
const member = src.zodRef.name.slice(root.length);
|
|
2967
|
+
return { text: `${alias}${member}` };
|
|
2968
|
+
}
|
|
2969
|
+
return null;
|
|
2970
|
+
};
|
|
2971
|
+
for (const entry of entries) {
|
|
2972
|
+
const block = [];
|
|
2973
|
+
const rename = renamesByEntry.get(entry) ?? null;
|
|
2974
|
+
let bodyConst;
|
|
2975
|
+
if (entry.warnings && entry.warnings.length > 0) {
|
|
2976
|
+
for (const w of entry.warnings) block.push(`// warning: ${w}`);
|
|
2977
|
+
}
|
|
2978
|
+
if (entry.body) {
|
|
2979
|
+
const rendered = renderSource(entry.body, rename);
|
|
2980
|
+
if (rendered?.warn) {
|
|
2981
|
+
block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
|
|
2982
|
+
} else if (rendered) {
|
|
2983
|
+
used = true;
|
|
2984
|
+
bodyConst = `${entry.baseName}BodySchema`;
|
|
2985
|
+
block.push(`export const ${bodyConst} = ${rendered.text};`);
|
|
2986
|
+
block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (entry.query) {
|
|
2990
|
+
const rendered = renderSource(entry.query, rename);
|
|
2991
|
+
if (rendered?.warn) {
|
|
2992
|
+
block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
|
|
2993
|
+
} else if (rendered) {
|
|
2994
|
+
used = true;
|
|
2995
|
+
const queryConst = `${entry.baseName}QuerySchema`;
|
|
2996
|
+
block.push(`export const ${queryConst} = ${rendered.text};`);
|
|
2997
|
+
block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
if (block.length === 0) continue;
|
|
3001
|
+
decls.push(`// ${entry.routeName}`, ...block, "");
|
|
3002
|
+
if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
|
|
3003
|
+
}
|
|
3004
|
+
if (!used) return null;
|
|
3005
|
+
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
3006
|
+
if (acceptsRawZod) {
|
|
3007
|
+
const zodImport = config?.zodImport ?? "zod";
|
|
3008
|
+
lines.push(`import { z } from '${zodImport}';`);
|
|
3009
|
+
} else {
|
|
3010
|
+
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
3011
|
+
}
|
|
3012
|
+
lines.push(...importLines);
|
|
3013
|
+
lines.push("");
|
|
3014
|
+
const allNested = /* @__PURE__ */ new Map();
|
|
3015
|
+
for (const [n, t] of globalSchemas) allNested.set(n, t);
|
|
3016
|
+
for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
|
|
3017
|
+
if (allNested.size > 0) {
|
|
3018
|
+
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
3019
|
+
for (const [n, alias] of irTypeAliases) {
|
|
3020
|
+
if (allNested.has(n)) lines.push(`${alias};`);
|
|
3021
|
+
}
|
|
3022
|
+
for (const [n, t] of allNested) {
|
|
3023
|
+
const annotation = irAnnotations.get(n);
|
|
3024
|
+
lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
|
|
3025
|
+
}
|
|
3026
|
+
lines.push("");
|
|
3441
3027
|
}
|
|
3442
|
-
|
|
3028
|
+
lines.push(...decls);
|
|
3029
|
+
lines.push("/** Route name \u2192 body schema map. */");
|
|
3030
|
+
lines.push("export const formSchemas = {");
|
|
3031
|
+
lines.push(...mapEntries);
|
|
3032
|
+
lines.push("} as const;");
|
|
3033
|
+
lines.push("");
|
|
3034
|
+
return lines.join("\n");
|
|
3443
3035
|
}
|
|
3444
3036
|
|
|
3445
3037
|
// src/emit/emit-index.ts
|
|
3446
3038
|
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
3447
|
-
import { join as
|
|
3039
|
+
import { join as join8 } from "path";
|
|
3448
3040
|
async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
3449
3041
|
await mkdir4(outDir, { recursive: true });
|
|
3450
3042
|
const exports = ["export * from './pages.js';", "export * from './routes.js';"];
|
|
@@ -3457,12 +3049,12 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
3457
3049
|
const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
|
|
3458
3050
|
"\n"
|
|
3459
3051
|
);
|
|
3460
|
-
await writeFile4(
|
|
3052
|
+
await writeFile4(join8(outDir, "index.d.ts"), content, "utf8");
|
|
3461
3053
|
}
|
|
3462
3054
|
|
|
3463
3055
|
// src/emit/emit-pages.ts
|
|
3464
3056
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
3465
|
-
import { join as
|
|
3057
|
+
import { join as join9, relative as relative5 } from "path";
|
|
3466
3058
|
async function emitPages(pages, outDir, _options = {}) {
|
|
3467
3059
|
await mkdir5(outDir, { recursive: true });
|
|
3468
3060
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
@@ -3483,7 +3075,7 @@ ${augBody}
|
|
|
3483
3075
|
}
|
|
3484
3076
|
${sharedPropsBlock}}
|
|
3485
3077
|
`;
|
|
3486
|
-
await writeFile5(
|
|
3078
|
+
await writeFile5(join9(outDir, "pages.d.ts"), content, "utf8");
|
|
3487
3079
|
}
|
|
3488
3080
|
function buildSharedPropsBlock(sharedProps) {
|
|
3489
3081
|
if (!sharedProps) return "";
|
|
@@ -3514,11 +3106,11 @@ function needsQuotes(name) {
|
|
|
3514
3106
|
|
|
3515
3107
|
// src/emit/emit-routes.ts
|
|
3516
3108
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
3517
|
-
import { join as
|
|
3109
|
+
import { join as join10 } from "path";
|
|
3518
3110
|
async function emitRoutes(routes, outDir) {
|
|
3519
3111
|
await mkdir6(outDir, { recursive: true });
|
|
3520
3112
|
const content = buildRoutesFile(routes);
|
|
3521
|
-
await writeFile6(
|
|
3113
|
+
await writeFile6(join10(outDir, "routes.ts"), content, "utf8");
|
|
3522
3114
|
}
|
|
3523
3115
|
function buildRoutesFile(routes) {
|
|
3524
3116
|
if (routes.length === 0) {
|
|
@@ -3646,30 +3238,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
3646
3238
|
propsExport: pagesConfig.propsExport,
|
|
3647
3239
|
componentNameStrategy: pagesConfig.componentNameStrategy
|
|
3648
3240
|
});
|
|
3649
|
-
|
|
3650
|
-
if (config.app?.moduleEntry) {
|
|
3651
|
-
try {
|
|
3652
|
-
const tsconfigPath = config.app.tsconfig ?? join10(config.codegen.cwd, "tsconfig.json");
|
|
3653
|
-
let project;
|
|
3654
|
-
try {
|
|
3655
|
-
project = new Project3({
|
|
3656
|
-
tsConfigFilePath: tsconfigPath,
|
|
3657
|
-
skipAddingFilesFromTsConfig: true,
|
|
3658
|
-
skipLoadingLibFiles: true,
|
|
3659
|
-
skipFileDependencyResolution: true
|
|
3660
|
-
});
|
|
3661
|
-
} catch {
|
|
3662
|
-
project = new Project3({
|
|
3663
|
-
skipAddingFilesFromTsConfig: true,
|
|
3664
|
-
skipLoadingLibFiles: true,
|
|
3665
|
-
skipFileDependencyResolution: true,
|
|
3666
|
-
compilerOptions: { allowJs: true, strict: false }
|
|
3667
|
-
});
|
|
3668
|
-
}
|
|
3669
|
-
sharedProps = discoverSharedProps(project, config.app.moduleEntry);
|
|
3670
|
-
} catch {
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3241
|
+
const sharedProps = discoverSharedPropsFromConfig(config);
|
|
3673
3242
|
await emitPages(pages, config.codegen.outDir, {
|
|
3674
3243
|
propsExport: pagesConfig.propsExport,
|
|
3675
3244
|
sharedProps
|
|
@@ -3693,7 +3262,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
3693
3262
|
if (extensions.length > 0) {
|
|
3694
3263
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
3695
3264
|
for (const file of extraFiles) {
|
|
3696
|
-
const dest =
|
|
3265
|
+
const dest = join11(config.codegen.outDir, file.path);
|
|
3697
3266
|
await mkdir7(dirname3(dest), { recursive: true });
|
|
3698
3267
|
await writeFile7(dest, file.contents, "utf8");
|
|
3699
3268
|
}
|
|
@@ -3703,7 +3272,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
3703
3272
|
// src/watch/lock-file.ts
|
|
3704
3273
|
import { open } from "fs/promises";
|
|
3705
3274
|
import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
|
|
3706
|
-
import { join as
|
|
3275
|
+
import { join as join12 } from "path";
|
|
3707
3276
|
var LOCK_FILE = ".watcher.lock";
|
|
3708
3277
|
function isProcessAlive(pid) {
|
|
3709
3278
|
try {
|
|
@@ -3715,7 +3284,7 @@ function isProcessAlive(pid) {
|
|
|
3715
3284
|
}
|
|
3716
3285
|
async function acquireLock(outDir) {
|
|
3717
3286
|
await mkdir8(outDir, { recursive: true });
|
|
3718
|
-
const lockPath =
|
|
3287
|
+
const lockPath = join12(outDir, LOCK_FILE);
|
|
3719
3288
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3720
3289
|
try {
|
|
3721
3290
|
const fd = await open(lockPath, "wx");
|
|
@@ -3755,7 +3324,7 @@ async function watch(config, onChange) {
|
|
|
3755
3324
|
if (lock === null) {
|
|
3756
3325
|
let holderPid = "unknown";
|
|
3757
3326
|
try {
|
|
3758
|
-
const raw = await readFile3(
|
|
3327
|
+
const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3759
3328
|
const data = JSON.parse(raw);
|
|
3760
3329
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3761
3330
|
} catch {
|
|
@@ -3783,7 +3352,7 @@ async function watch(config, onChange) {
|
|
|
3783
3352
|
}
|
|
3784
3353
|
let pagesDebounceTimer;
|
|
3785
3354
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3786
|
-
const pagesWatcher = chokidar.watch(
|
|
3355
|
+
const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
|
|
3787
3356
|
ignoreInitial: true,
|
|
3788
3357
|
persistent: true,
|
|
3789
3358
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3809,7 +3378,7 @@ async function watch(config, onChange) {
|
|
|
3809
3378
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3810
3379
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3811
3380
|
let contractsDebounceTimer;
|
|
3812
|
-
const contractsWatcher = chokidar.watch(
|
|
3381
|
+
const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
|
|
3813
3382
|
ignoreInitial: true,
|
|
3814
3383
|
persistent: true,
|
|
3815
3384
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3839,7 +3408,7 @@ async function watch(config, onChange) {
|
|
|
3839
3408
|
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3840
3409
|
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3841
3410
|
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3842
|
-
const formsWatcher = chokidar.watch(
|
|
3411
|
+
const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
|
|
3843
3412
|
ignoreInitial: true,
|
|
3844
3413
|
persistent: true,
|
|
3845
3414
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|