@dudousxd/nestjs-codegen 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/cli/main.cjs +1214 -1613
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1188 -1587
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.cjs +12 -2
- package/dist/extension/index.cjs.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js +10 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/{index-BwIRjOQA.d.cts → index-oH5t7x4G.d.cts} +56 -41
- package/dist/{index-BwIRjOQA.d.ts → index-oH5t7x4G.d.ts} +56 -41
- package/dist/index.cjs +1003 -1457
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -16
- package/dist/index.d.ts +32 -16
- package/dist/index.js +977 -1430
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +908 -1355
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +9 -2
- package/dist/nest/index.d.ts +9 -2
- package/dist/nest/index.js +893 -1340
- package/dist/nest/index.js.map +1 -1
- package/package.json +3 -2
package/dist/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);
|
|
@@ -801,344 +711,14 @@ function inSchemaFromDecorator(decorator) {
|
|
|
801
711
|
return null;
|
|
802
712
|
}
|
|
803
713
|
|
|
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
714
|
// src/discovery/filter-for.ts
|
|
1135
715
|
import {
|
|
1136
|
-
Node as
|
|
716
|
+
Node as Node4
|
|
1137
717
|
} from "ts-morph";
|
|
1138
718
|
|
|
1139
719
|
// src/discovery/filter-field-types.ts
|
|
1140
720
|
import {
|
|
1141
|
-
Node as
|
|
721
|
+
Node as Node3,
|
|
1142
722
|
SyntaxKind
|
|
1143
723
|
} from "ts-morph";
|
|
1144
724
|
|
|
@@ -1175,7 +755,7 @@ function markNullable(r, nullable) {
|
|
|
1175
755
|
return nullable ? { ...r, nullable: true } : r;
|
|
1176
756
|
}
|
|
1177
757
|
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
1178
|
-
if (
|
|
758
|
+
if (Node3.isUnionTypeNode(typeNode)) {
|
|
1179
759
|
let nullable = false;
|
|
1180
760
|
const stringLits = [];
|
|
1181
761
|
const numberLits = [];
|
|
@@ -1186,13 +766,13 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
1186
766
|
nullable = true;
|
|
1187
767
|
continue;
|
|
1188
768
|
}
|
|
1189
|
-
if (
|
|
769
|
+
if (Node3.isLiteralTypeNode(member)) {
|
|
1190
770
|
const lit = member.getLiteral();
|
|
1191
|
-
if (
|
|
771
|
+
if (Node3.isStringLiteral(lit)) {
|
|
1192
772
|
stringLits.push(lit.getLiteralValue());
|
|
1193
773
|
continue;
|
|
1194
774
|
}
|
|
1195
|
-
if (
|
|
775
|
+
if (Node3.isNumericLiteral(lit)) {
|
|
1196
776
|
numberLits.push(lit.getText());
|
|
1197
777
|
continue;
|
|
1198
778
|
}
|
|
@@ -1228,7 +808,7 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
1228
808
|
default:
|
|
1229
809
|
break;
|
|
1230
810
|
}
|
|
1231
|
-
if (
|
|
811
|
+
if (Node3.isTypeReference(typeNode)) {
|
|
1232
812
|
const refName = typeNode.getTypeName().getText();
|
|
1233
813
|
if (refName === "Date") return { kind: "date" };
|
|
1234
814
|
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
@@ -1241,25 +821,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
1241
821
|
if (typeRef) return { kind: "unknown", typeRef };
|
|
1242
822
|
return { kind: "unknown" };
|
|
1243
823
|
}
|
|
1244
|
-
if (
|
|
824
|
+
if (Node3.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
1245
825
|
return { kind: "unknown" };
|
|
1246
826
|
}
|
|
1247
827
|
function enumFromDecoratorArgs(args, sourceFile, project) {
|
|
1248
828
|
for (const arg of args) {
|
|
1249
|
-
if (
|
|
829
|
+
if (Node3.isArrowFunction(arg)) {
|
|
1250
830
|
const body = arg.getBody();
|
|
1251
|
-
if (
|
|
831
|
+
if (Node3.isIdentifier(body)) {
|
|
1252
832
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
1253
833
|
if (en) return en;
|
|
1254
834
|
}
|
|
1255
835
|
}
|
|
1256
|
-
if (
|
|
836
|
+
if (Node3.isObjectLiteralExpression(arg)) {
|
|
1257
837
|
const itemsProp = arg.getProperty("items");
|
|
1258
|
-
if (itemsProp &&
|
|
838
|
+
if (itemsProp && Node3.isPropertyAssignment(itemsProp)) {
|
|
1259
839
|
const init = itemsProp.getInitializer();
|
|
1260
|
-
if (init &&
|
|
840
|
+
if (init && Node3.isArrowFunction(init)) {
|
|
1261
841
|
const body = init.getBody();
|
|
1262
|
-
if (
|
|
842
|
+
if (Node3.isIdentifier(body)) {
|
|
1263
843
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
1264
844
|
if (en) return en;
|
|
1265
845
|
}
|
|
@@ -1282,7 +862,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1282
862
|
return { kind: "string" };
|
|
1283
863
|
}
|
|
1284
864
|
for (const arg of args) {
|
|
1285
|
-
if (
|
|
865
|
+
if (Node3.isStringLiteral(arg)) {
|
|
1286
866
|
const raw = arg.getLiteralValue();
|
|
1287
867
|
const kind = classifyTypeKeyword(raw);
|
|
1288
868
|
if (kind) {
|
|
@@ -1290,11 +870,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1290
870
|
return { kind };
|
|
1291
871
|
}
|
|
1292
872
|
}
|
|
1293
|
-
if (
|
|
873
|
+
if (Node3.isObjectLiteralExpression(arg)) {
|
|
1294
874
|
const enumProp = arg.getProperty("enum");
|
|
1295
|
-
if (enumProp &&
|
|
875
|
+
if (enumProp && Node3.isPropertyAssignment(enumProp)) {
|
|
1296
876
|
const init = enumProp.getInitializer();
|
|
1297
|
-
if (init &&
|
|
877
|
+
if (init && Node3.isIdentifier(init)) {
|
|
1298
878
|
const en = resolveEnumValues(init.getText(), sourceFile, project);
|
|
1299
879
|
if (en) {
|
|
1300
880
|
return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
@@ -1303,9 +883,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1303
883
|
}
|
|
1304
884
|
}
|
|
1305
885
|
const typeProp = arg.getProperty("type");
|
|
1306
|
-
if (typeProp &&
|
|
886
|
+
if (typeProp && Node3.isPropertyAssignment(typeProp)) {
|
|
1307
887
|
const init = typeProp.getInitializer();
|
|
1308
|
-
if (init &&
|
|
888
|
+
if (init && Node3.isStringLiteral(init)) {
|
|
1309
889
|
const kind = classifyTypeKeyword(init.getLiteralValue());
|
|
1310
890
|
if (kind) return { kind };
|
|
1311
891
|
}
|
|
@@ -1341,7 +921,7 @@ function toFilterFieldType(name, r) {
|
|
|
1341
921
|
|
|
1342
922
|
// src/discovery/filter-for.ts
|
|
1343
923
|
function classifyFilterForHint(typeInit) {
|
|
1344
|
-
if (
|
|
924
|
+
if (Node4.isStringLiteral(typeInit)) {
|
|
1345
925
|
switch (typeInit.getLiteralValue()) {
|
|
1346
926
|
case "string":
|
|
1347
927
|
return { kind: "string" };
|
|
@@ -1355,10 +935,10 @@ function classifyFilterForHint(typeInit) {
|
|
|
1355
935
|
return null;
|
|
1356
936
|
}
|
|
1357
937
|
}
|
|
1358
|
-
if (
|
|
938
|
+
if (Node4.isArrayLiteralExpression(typeInit)) {
|
|
1359
939
|
const values = [];
|
|
1360
940
|
for (const el of typeInit.getElements()) {
|
|
1361
|
-
if (!
|
|
941
|
+
if (!Node4.isStringLiteral(el)) return null;
|
|
1362
942
|
values.push(el.getLiteralValue());
|
|
1363
943
|
}
|
|
1364
944
|
if (values.length === 0) return null;
|
|
@@ -1393,11 +973,11 @@ function extractFilterForHints(classDecl, project) {
|
|
|
1393
973
|
if (!filterForDec) continue;
|
|
1394
974
|
const args = filterForDec.getArguments();
|
|
1395
975
|
const keyArg = args[0];
|
|
1396
|
-
const inputKey = keyArg &&
|
|
976
|
+
const inputKey = keyArg && Node4.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
|
|
1397
977
|
const optsArg = args[1];
|
|
1398
|
-
if (optsArg &&
|
|
978
|
+
if (optsArg && Node4.isObjectLiteralExpression(optsArg)) {
|
|
1399
979
|
const typeProp = optsArg.getProperty("type");
|
|
1400
|
-
if (typeProp &&
|
|
980
|
+
if (typeProp && Node4.isPropertyAssignment(typeProp)) {
|
|
1401
981
|
const typeInit = typeProp.getInitializer();
|
|
1402
982
|
if (typeInit) {
|
|
1403
983
|
const classified = classifyFilterForHint(typeInit);
|
|
@@ -1420,14 +1000,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
|
|
|
1420
1000
|
const args = filterDecorator.getArguments();
|
|
1421
1001
|
if (args.length === 0) continue;
|
|
1422
1002
|
const filterClassArg = args[0];
|
|
1423
|
-
if (!filterClassArg || !
|
|
1003
|
+
if (!filterClassArg || !Node4.isIdentifier(filterClassArg)) continue;
|
|
1424
1004
|
let source = "query";
|
|
1425
1005
|
const optionsArg = args[1];
|
|
1426
|
-
if (optionsArg &&
|
|
1006
|
+
if (optionsArg && Node4.isObjectLiteralExpression(optionsArg)) {
|
|
1427
1007
|
const sourceProp = optionsArg.getProperty("source");
|
|
1428
|
-
if (sourceProp &&
|
|
1008
|
+
if (sourceProp && Node4.isPropertyAssignment(sourceProp)) {
|
|
1429
1009
|
const init = sourceProp.getInitializer();
|
|
1430
|
-
if (init &&
|
|
1010
|
+
if (init && Node4.isStringLiteral(init) && init.getLiteralValue() === "body") {
|
|
1431
1011
|
source = "body";
|
|
1432
1012
|
}
|
|
1433
1013
|
}
|
|
@@ -1490,22 +1070,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
|
|
|
1490
1070
|
const args = dec.getArguments();
|
|
1491
1071
|
if (args.length === 0) continue;
|
|
1492
1072
|
const arg = args[0];
|
|
1493
|
-
if (
|
|
1073
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
1494
1074
|
const entityProp = arg.getProperty("entity");
|
|
1495
|
-
if (entityProp &&
|
|
1075
|
+
if (entityProp && Node4.isPropertyAssignment(entityProp)) {
|
|
1496
1076
|
const init = entityProp.getInitializer();
|
|
1497
|
-
if (init &&
|
|
1077
|
+
if (init && Node4.isArrowFunction(init)) {
|
|
1498
1078
|
const body = init.getBody();
|
|
1499
|
-
if (
|
|
1079
|
+
if (Node4.isIdentifier(body)) {
|
|
1500
1080
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
1501
1081
|
if (resolved?.kind === "class") return resolved.decl;
|
|
1502
1082
|
}
|
|
1503
1083
|
}
|
|
1504
1084
|
}
|
|
1505
1085
|
}
|
|
1506
|
-
if (
|
|
1086
|
+
if (Node4.isArrowFunction(arg)) {
|
|
1507
1087
|
const body = arg.getBody();
|
|
1508
|
-
if (
|
|
1088
|
+
if (Node4.isIdentifier(body)) {
|
|
1509
1089
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
1510
1090
|
if (resolved?.kind === "class") return resolved.decl;
|
|
1511
1091
|
}
|
|
@@ -1529,11 +1109,11 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1529
1109
|
const args = filterableDecorator.getArguments();
|
|
1530
1110
|
if (args.length === 0) return [];
|
|
1531
1111
|
const optionsArg = args[0];
|
|
1532
|
-
if (!
|
|
1112
|
+
if (!Node4.isObjectLiteralExpression(optionsArg)) return [];
|
|
1533
1113
|
const entityProp = optionsArg.getProperty("entity");
|
|
1534
|
-
if (!entityProp || !
|
|
1114
|
+
if (!entityProp || !Node4.isPropertyAssignment(entityProp)) return [];
|
|
1535
1115
|
const entityInit = entityProp.getInitializer();
|
|
1536
|
-
if (!entityInit || !
|
|
1116
|
+
if (!entityInit || !Node4.isIdentifier(entityInit)) return [];
|
|
1537
1117
|
const entityName = entityInit.getText();
|
|
1538
1118
|
const filterSourceFile = filterClass.getSourceFile();
|
|
1539
1119
|
const resolvedEntity = findType(entityName, filterSourceFile, project);
|
|
@@ -1549,17 +1129,17 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1549
1129
|
const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
|
|
1550
1130
|
if (relationsDecorator) {
|
|
1551
1131
|
const relArgs = relationsDecorator.getArguments();
|
|
1552
|
-
if (relArgs.length > 0 &&
|
|
1132
|
+
if (relArgs.length > 0 && Node4.isObjectLiteralExpression(relArgs[0])) {
|
|
1553
1133
|
for (const relProp of relArgs[0].getProperties()) {
|
|
1554
|
-
if (!
|
|
1134
|
+
if (!Node4.isPropertyAssignment(relProp)) continue;
|
|
1555
1135
|
const relInit = relProp.getInitializer();
|
|
1556
|
-
if (!relInit || !
|
|
1136
|
+
if (!relInit || !Node4.isObjectLiteralExpression(relInit)) continue;
|
|
1557
1137
|
const keysProp = relInit.getProperty("keys");
|
|
1558
|
-
if (!keysProp || !
|
|
1138
|
+
if (!keysProp || !Node4.isPropertyAssignment(keysProp)) continue;
|
|
1559
1139
|
const keysInit = keysProp.getInitializer();
|
|
1560
|
-
if (!keysInit || !
|
|
1140
|
+
if (!keysInit || !Node4.isArrayLiteralExpression(keysInit)) continue;
|
|
1561
1141
|
for (const el of keysInit.getElements()) {
|
|
1562
|
-
if (
|
|
1142
|
+
if (Node4.isStringLiteral(el)) {
|
|
1563
1143
|
fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
|
|
1564
1144
|
}
|
|
1565
1145
|
}
|
|
@@ -1569,267 +1149,65 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1569
1149
|
return fields;
|
|
1570
1150
|
}
|
|
1571
1151
|
|
|
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
|
-
}
|
|
1152
|
+
// src/discovery/dto-type-resolver.ts
|
|
1153
|
+
var WRAPPER_TYPES = {
|
|
1154
|
+
// MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
|
|
1155
|
+
// wrappers around related entities; the wire shape is just the referenced
|
|
1156
|
+
// entity. Unwrap to the type argument.
|
|
1157
|
+
Ref: "unwrap",
|
|
1158
|
+
Reference: "unwrap",
|
|
1159
|
+
LoadedReference: "unwrap",
|
|
1160
|
+
IdentifiedReference: "unwrap",
|
|
1161
|
+
// MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
|
|
1162
|
+
Opt: "unwrap",
|
|
1163
|
+
Loaded: "unwrap",
|
|
1164
|
+
// Promise<T> — unwrap
|
|
1165
|
+
Promise: "unwrap",
|
|
1166
|
+
// MikroORM Collection<T> serializes as an array of T on the wire.
|
|
1167
|
+
Collection: "arrayOf",
|
|
1168
|
+
// Array<T> generic form
|
|
1169
|
+
Array: "arrayOf"
|
|
1170
|
+
};
|
|
1171
|
+
var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
1172
|
+
"Record",
|
|
1173
|
+
"Omit",
|
|
1174
|
+
"Pick",
|
|
1175
|
+
"Partial",
|
|
1176
|
+
"Required",
|
|
1177
|
+
"Readonly",
|
|
1178
|
+
"Map",
|
|
1179
|
+
"Set"
|
|
1180
|
+
]);
|
|
1767
1181
|
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
1768
1182
|
if (depth <= 0) return "unknown";
|
|
1769
|
-
if (
|
|
1183
|
+
if (Node5.isArrayTypeNode(typeNode)) {
|
|
1770
1184
|
const elementType = typeNode.getElementTypeNode();
|
|
1771
1185
|
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
1772
1186
|
}
|
|
1773
|
-
if (
|
|
1187
|
+
if (Node5.isUnionTypeNode(typeNode)) {
|
|
1774
1188
|
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
1775
1189
|
}
|
|
1776
|
-
if (
|
|
1190
|
+
if (Node5.isIntersectionTypeNode(typeNode)) {
|
|
1777
1191
|
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
1778
1192
|
}
|
|
1779
|
-
if (
|
|
1193
|
+
if (Node5.isParenthesizedTypeNode(typeNode)) {
|
|
1780
1194
|
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
1781
1195
|
}
|
|
1782
|
-
if (
|
|
1196
|
+
if (Node5.isTypeReference(typeNode)) {
|
|
1783
1197
|
const typeName = typeNode.getTypeName();
|
|
1784
|
-
const name =
|
|
1198
|
+
const name = Node5.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
1785
1199
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
1786
1200
|
if (name === "Date") return "string";
|
|
1787
1201
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
1788
1202
|
if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
|
|
1789
1203
|
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";
|
|
1204
|
+
const wrapperMode = WRAPPER_TYPES[name];
|
|
1205
|
+
if (wrapperMode) {
|
|
1206
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
1813
1207
|
}
|
|
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)) {
|
|
1208
|
+
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
1823
1209
|
return typeNode.getText();
|
|
1824
1210
|
}
|
|
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
1211
|
const resolved = findType(name, sourceFile, project);
|
|
1834
1212
|
if (resolved) {
|
|
1835
1213
|
return expandTypeDecl(resolved, project, depth - 1);
|
|
@@ -1845,6 +1223,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1845
1223
|
if (kind === SyntaxKind2.AnyKeyword) return "unknown";
|
|
1846
1224
|
return typeNode.getText();
|
|
1847
1225
|
}
|
|
1226
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
1227
|
+
const typeArgs = typeNode.getTypeArguments();
|
|
1228
|
+
const firstTypeArg = typeArgs[0];
|
|
1229
|
+
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1230
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1231
|
+
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
1232
|
+
}
|
|
1233
|
+
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
1234
|
+
}
|
|
1848
1235
|
function expandTypeDecl(result, project, depth) {
|
|
1849
1236
|
if (depth < 0) return "unknown";
|
|
1850
1237
|
switch (result.kind) {
|
|
@@ -1910,7 +1297,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
1910
1297
|
const paramArgs = paramDecorator.getArguments();
|
|
1911
1298
|
if (paramArgs.length === 0) continue;
|
|
1912
1299
|
const nameArg = paramArgs[0];
|
|
1913
|
-
if (!
|
|
1300
|
+
if (!Node5.isStringLiteral(nameArg)) continue;
|
|
1914
1301
|
const paramName = nameArg.getLiteralValue();
|
|
1915
1302
|
const typeNode = param.getTypeNode();
|
|
1916
1303
|
const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
|
|
@@ -1923,13 +1310,13 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1923
1310
|
if (apiResponseDecorator) {
|
|
1924
1311
|
const args = apiResponseDecorator.getArguments();
|
|
1925
1312
|
const optsArg = args[0];
|
|
1926
|
-
if (optsArg &&
|
|
1313
|
+
if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
|
|
1927
1314
|
for (const prop of optsArg.getProperties()) {
|
|
1928
|
-
if (!
|
|
1315
|
+
if (!Node5.isPropertyAssignment(prop)) continue;
|
|
1929
1316
|
if (prop.getName() !== "type") continue;
|
|
1930
1317
|
const val = prop.getInitializer();
|
|
1931
1318
|
if (!val) continue;
|
|
1932
|
-
if (
|
|
1319
|
+
if (Node5.isArrayLiteralExpression(val)) {
|
|
1933
1320
|
const elements = val.getElements();
|
|
1934
1321
|
const firstEl = elements[0];
|
|
1935
1322
|
if (elements.length > 0 && firstEl !== void 0) {
|
|
@@ -1949,7 +1336,7 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1949
1336
|
return "unknown";
|
|
1950
1337
|
}
|
|
1951
1338
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
1952
|
-
if (!
|
|
1339
|
+
if (!Node5.isIdentifier(node)) return "unknown";
|
|
1953
1340
|
const name = node.getText();
|
|
1954
1341
|
const resolved = findType(name, sourceFile, project);
|
|
1955
1342
|
if (resolved) {
|
|
@@ -1996,11 +1383,11 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
1996
1383
|
if (apiResp) {
|
|
1997
1384
|
const args = apiResp.getArguments();
|
|
1998
1385
|
const optsArg = args[0];
|
|
1999
|
-
if (optsArg &&
|
|
1386
|
+
if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
|
|
2000
1387
|
for (const prop of optsArg.getProperties()) {
|
|
2001
|
-
if (
|
|
1388
|
+
if (Node5.isPropertyAssignment(prop) && prop.getName() === "type") {
|
|
2002
1389
|
const val = prop.getInitializer();
|
|
2003
|
-
if (val &&
|
|
1390
|
+
if (val && Node5.isIdentifier(val)) {
|
|
2004
1391
|
const name = val.getText();
|
|
2005
1392
|
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
2006
1393
|
if (localDecl?.isExported()) {
|
|
@@ -2017,27 +1404,18 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2017
1404
|
}
|
|
2018
1405
|
}
|
|
2019
1406
|
}
|
|
2020
|
-
let bodyZodText = null;
|
|
2021
|
-
let queryZodText = null;
|
|
2022
1407
|
let bodySchema = null;
|
|
2023
1408
|
let querySchema = null;
|
|
2024
|
-
const formNested = {};
|
|
2025
1409
|
const formWarnings = [];
|
|
2026
1410
|
const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
|
|
2027
1411
|
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
1412
|
bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
|
|
1413
|
+
formWarnings.push(...bodySchema.warnings);
|
|
2033
1414
|
}
|
|
2034
1415
|
const queryClass = resolveParamClass(method, "Query", sourceFile, project);
|
|
2035
1416
|
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
1417
|
querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
|
|
1418
|
+
formWarnings.push(...querySchema.warnings);
|
|
2041
1419
|
}
|
|
2042
1420
|
return {
|
|
2043
1421
|
query,
|
|
@@ -2050,9 +1428,6 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2050
1428
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2051
1429
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2052
1430
|
filterSource: filterInfo?.source ?? null,
|
|
2053
|
-
bodyZodText,
|
|
2054
|
-
queryZodText,
|
|
2055
|
-
formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
|
|
2056
1431
|
formWarnings,
|
|
2057
1432
|
bodySchema,
|
|
2058
1433
|
querySchema
|
|
@@ -2072,6 +1447,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
|
2072
1447
|
}
|
|
2073
1448
|
return null;
|
|
2074
1449
|
}
|
|
1450
|
+
|
|
1451
|
+
// src/discovery/zod-ast-to-ts.ts
|
|
1452
|
+
import { Node as Node6, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
1453
|
+
function zodAstToTs(node) {
|
|
1454
|
+
if (!Node6.isCallExpression(node)) return "unknown";
|
|
1455
|
+
const expr = node.getExpression();
|
|
1456
|
+
if (Node6.isPropertyAccessExpression(expr)) {
|
|
1457
|
+
const methodName = expr.getName();
|
|
1458
|
+
const receiver = expr.getExpression();
|
|
1459
|
+
if (methodName === "optional") {
|
|
1460
|
+
return `${zodAstToTs(receiver)} | undefined`;
|
|
1461
|
+
}
|
|
1462
|
+
if (methodName === "nullable") {
|
|
1463
|
+
return `${zodAstToTs(receiver)} | null`;
|
|
1464
|
+
}
|
|
1465
|
+
const args = node.getArguments();
|
|
1466
|
+
switch (methodName) {
|
|
1467
|
+
case "string":
|
|
1468
|
+
return "string";
|
|
1469
|
+
case "number":
|
|
1470
|
+
return "number";
|
|
1471
|
+
case "boolean":
|
|
1472
|
+
return "boolean";
|
|
1473
|
+
case "unknown":
|
|
1474
|
+
return "unknown";
|
|
1475
|
+
case "any":
|
|
1476
|
+
return "unknown";
|
|
1477
|
+
case "literal": {
|
|
1478
|
+
const lit = args[0];
|
|
1479
|
+
if (!lit) return "unknown";
|
|
1480
|
+
if (Node6.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
1481
|
+
if (Node6.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
1482
|
+
if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
|
|
1483
|
+
if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
|
|
1484
|
+
return "unknown";
|
|
1485
|
+
}
|
|
1486
|
+
case "enum": {
|
|
1487
|
+
const arrArg = args[0];
|
|
1488
|
+
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1489
|
+
const members = arrArg.getElements().map(
|
|
1490
|
+
(el) => Node6.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
1491
|
+
);
|
|
1492
|
+
return members.join(" | ");
|
|
1493
|
+
}
|
|
1494
|
+
case "array": {
|
|
1495
|
+
const inner = args[0];
|
|
1496
|
+
if (!inner) return "unknown";
|
|
1497
|
+
return `Array<${zodAstToTs(inner)}>`;
|
|
1498
|
+
}
|
|
1499
|
+
case "object": {
|
|
1500
|
+
const objArg = args[0];
|
|
1501
|
+
if (!objArg || !Node6.isObjectLiteralExpression(objArg)) return "unknown";
|
|
1502
|
+
const lines = [];
|
|
1503
|
+
for (const prop of objArg.getProperties()) {
|
|
1504
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
1505
|
+
const key = prop.getName();
|
|
1506
|
+
const valNode = prop.getInitializer();
|
|
1507
|
+
if (!valNode) continue;
|
|
1508
|
+
const tsType = zodAstToTs(valNode);
|
|
1509
|
+
const isOpt = isOptionalChain(valNode);
|
|
1510
|
+
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
1511
|
+
}
|
|
1512
|
+
return `{ ${lines.join("; ")} }`;
|
|
1513
|
+
}
|
|
1514
|
+
case "union": {
|
|
1515
|
+
const arrArg = args[0];
|
|
1516
|
+
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1517
|
+
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
1518
|
+
}
|
|
1519
|
+
case "record": {
|
|
1520
|
+
const valArg = args.length === 1 ? args[0] : args[1];
|
|
1521
|
+
if (!valArg) return "unknown";
|
|
1522
|
+
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
1523
|
+
}
|
|
1524
|
+
case "tuple": {
|
|
1525
|
+
const arrArg = args[0];
|
|
1526
|
+
if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1527
|
+
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
1528
|
+
}
|
|
1529
|
+
default:
|
|
1530
|
+
return "unknown";
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return "unknown";
|
|
1534
|
+
}
|
|
1535
|
+
function isOptionalChain(node) {
|
|
1536
|
+
if (!Node6.isCallExpression(node)) return false;
|
|
1537
|
+
const expr = node.getExpression();
|
|
1538
|
+
return Node6.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
1539
|
+
}
|
|
1540
|
+
function parseDefineContractCall(callExpr) {
|
|
1541
|
+
if (!Node6.isCallExpression(callExpr)) return null;
|
|
1542
|
+
const callee = callExpr.getExpression();
|
|
1543
|
+
const calleeName = Node6.isIdentifier(callee) ? callee.getText() : Node6.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
1544
|
+
if (calleeName !== "defineContract") return null;
|
|
1545
|
+
const args = callExpr.getArguments();
|
|
1546
|
+
const optsArg = args[0];
|
|
1547
|
+
if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
|
|
1548
|
+
let query = null;
|
|
1549
|
+
let body = null;
|
|
1550
|
+
let response = "unknown";
|
|
1551
|
+
let bodyZodText = null;
|
|
1552
|
+
let queryZodText = null;
|
|
1553
|
+
for (const prop of optsArg.getProperties()) {
|
|
1554
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
1555
|
+
const propName = prop.getName();
|
|
1556
|
+
const val = prop.getInitializer();
|
|
1557
|
+
if (!val) continue;
|
|
1558
|
+
if (propName === "query") {
|
|
1559
|
+
query = zodAstToTs(val);
|
|
1560
|
+
queryZodText = val.getText();
|
|
1561
|
+
} else if (propName === "body") {
|
|
1562
|
+
body = zodAstToTs(val);
|
|
1563
|
+
bodyZodText = val.getText();
|
|
1564
|
+
} else if (propName === "response") {
|
|
1565
|
+
response = zodAstToTs(val);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return { query, body, response, bodyZodText, queryZodText };
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/discovery/contracts-fast.ts
|
|
1572
|
+
async function discoverContractsFast(opts) {
|
|
1573
|
+
const { cwd, glob, tsconfig } = opts;
|
|
1574
|
+
const tsconfigPath = tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
|
|
1575
|
+
let project;
|
|
1576
|
+
try {
|
|
1577
|
+
project = new Project({
|
|
1578
|
+
tsConfigFilePath: tsconfigPath,
|
|
1579
|
+
skipAddingFilesFromTsConfig: true,
|
|
1580
|
+
skipLoadingLibFiles: true,
|
|
1581
|
+
skipFileDependencyResolution: true
|
|
1582
|
+
});
|
|
1583
|
+
} catch {
|
|
1584
|
+
project = new Project({
|
|
1585
|
+
skipAddingFilesFromTsConfig: true,
|
|
1586
|
+
skipLoadingLibFiles: true,
|
|
1587
|
+
skipFileDependencyResolution: true,
|
|
1588
|
+
compilerOptions: {
|
|
1589
|
+
allowJs: true,
|
|
1590
|
+
resolveJsonModule: false,
|
|
1591
|
+
strict: false
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
const files = await fg(glob, { cwd, absolute: true, onlyFiles: true });
|
|
1596
|
+
for (const f of files) {
|
|
1597
|
+
project.addSourceFileAtPath(f);
|
|
1598
|
+
}
|
|
1599
|
+
const routes = [];
|
|
1600
|
+
setDiscoveryContext(project, {
|
|
1601
|
+
projectRoot: cwd,
|
|
1602
|
+
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
1603
|
+
});
|
|
1604
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
1605
|
+
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
1606
|
+
}
|
|
1607
|
+
return routes;
|
|
1608
|
+
}
|
|
1609
|
+
function decoratorStringArg(decoratorExpr) {
|
|
1610
|
+
if (!decoratorExpr) return void 0;
|
|
1611
|
+
if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
1612
|
+
if (Node7.isArrayLiteralExpression(decoratorExpr)) {
|
|
1613
|
+
const first = decoratorExpr.getElements()[0];
|
|
1614
|
+
if (first && Node7.isStringLiteral(first)) return first.getLiteralValue();
|
|
1615
|
+
}
|
|
1616
|
+
return void 0;
|
|
1617
|
+
}
|
|
1618
|
+
function deriveClassSegment(className) {
|
|
1619
|
+
const noSuffix = className.replace(/Controller$/, "");
|
|
1620
|
+
if (!noSuffix) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
1626
|
+
}
|
|
1627
|
+
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
1628
|
+
const classPortion = classAs ?? deriveClassSegment(className);
|
|
1629
|
+
const methodPortion = methodAs ?? methodName;
|
|
1630
|
+
return `${classPortion}.${methodPortion}`;
|
|
1631
|
+
}
|
|
1632
|
+
function joinPaths(prefix, suffix) {
|
|
1633
|
+
if (!prefix && !suffix) return "/";
|
|
1634
|
+
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1635
|
+
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1636
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1637
|
+
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1638
|
+
const combined = p + s;
|
|
1639
|
+
return combined === "" ? "/" : combined;
|
|
1640
|
+
}
|
|
1641
|
+
function extractParams(path) {
|
|
1642
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
1643
|
+
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
1644
|
+
}
|
|
2075
1645
|
var HTTP_METHOD_DECORATORS = {
|
|
2076
1646
|
Get: "GET",
|
|
2077
1647
|
Post: "POST",
|
|
@@ -2082,176 +1652,186 @@ var HTTP_METHOD_DECORATORS = {
|
|
|
2082
1652
|
Head: "HEAD",
|
|
2083
1653
|
All: "ALL"
|
|
2084
1654
|
};
|
|
1655
|
+
function resolveVerb(method) {
|
|
1656
|
+
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
1657
|
+
const httpDecorator = method.getDecorator(decoratorName);
|
|
1658
|
+
if (httpDecorator) {
|
|
1659
|
+
const httpArgs = httpDecorator.getArguments();
|
|
1660
|
+
const pathArg = httpArgs[0];
|
|
1661
|
+
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
function readAsDecorator(node, label) {
|
|
1667
|
+
const asDecorator = node.getDecorator("As");
|
|
1668
|
+
if (!asDecorator) return void 0;
|
|
1669
|
+
const asName = decoratorStringArg(asDecorator.getArguments()[0]);
|
|
1670
|
+
if (!asName) {
|
|
1671
|
+
throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
|
|
1672
|
+
}
|
|
1673
|
+
return asName;
|
|
1674
|
+
}
|
|
1675
|
+
function buildRoute(args) {
|
|
1676
|
+
const {
|
|
1677
|
+
className,
|
|
1678
|
+
methodName,
|
|
1679
|
+
resolvedMethod,
|
|
1680
|
+
combinedPath,
|
|
1681
|
+
classAs,
|
|
1682
|
+
methodAs,
|
|
1683
|
+
sourceFile,
|
|
1684
|
+
seenNames,
|
|
1685
|
+
contractSource
|
|
1686
|
+
} = args;
|
|
1687
|
+
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
1688
|
+
const qualifiedRef = `${className}.${methodName}`;
|
|
1689
|
+
const existing = seenNames.get(routeName);
|
|
1690
|
+
if (existing !== void 0) {
|
|
1691
|
+
throw new Error(
|
|
1692
|
+
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
seenNames.set(routeName, qualifiedRef);
|
|
1696
|
+
return {
|
|
1697
|
+
method: resolvedMethod,
|
|
1698
|
+
path: combinedPath,
|
|
1699
|
+
name: routeName,
|
|
1700
|
+
params: extractParams(combinedPath),
|
|
1701
|
+
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
1702
|
+
contract: { contractSource }
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
function extractContractRoute(args) {
|
|
1706
|
+
const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
|
|
1707
|
+
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
1708
|
+
if (!firstDecoratorArg) return null;
|
|
1709
|
+
let contractDef = null;
|
|
1710
|
+
let bodyZodRef = null;
|
|
1711
|
+
let queryZodRef = null;
|
|
1712
|
+
if (Node7.isCallExpression(firstDecoratorArg)) {
|
|
1713
|
+
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
1714
|
+
} else if (Node7.isIdentifier(firstDecoratorArg)) {
|
|
1715
|
+
const identName = firstDecoratorArg.getText();
|
|
1716
|
+
const varDecl = sourceFile.getVariableDeclaration(identName);
|
|
1717
|
+
if (!varDecl) {
|
|
1718
|
+
console.warn(
|
|
1719
|
+
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
|
|
1720
|
+
);
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
const initializer = varDecl.getInitializer();
|
|
1724
|
+
if (!initializer) return null;
|
|
1725
|
+
contractDef = parseDefineContractCall(initializer);
|
|
1726
|
+
if (contractDef && varDecl.isExported()) {
|
|
1727
|
+
const filePath = sourceFile.getFilePath();
|
|
1728
|
+
if (contractDef.body !== null) {
|
|
1729
|
+
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
1730
|
+
}
|
|
1731
|
+
if (contractDef.query !== null) {
|
|
1732
|
+
queryZodRef = { name: `${identName}.query`, filePath };
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
} else {
|
|
1736
|
+
console.warn(
|
|
1737
|
+
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
1738
|
+
);
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
if (!contractDef) return null;
|
|
1742
|
+
if (!verb) return null;
|
|
1743
|
+
const resolvedPath = joinPaths(prefix, verb.handlerPath);
|
|
1744
|
+
const methodName = method.getName();
|
|
1745
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
1746
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
1747
|
+
return buildRoute({
|
|
1748
|
+
className,
|
|
1749
|
+
methodName,
|
|
1750
|
+
resolvedMethod: verb.httpMethod,
|
|
1751
|
+
combinedPath: resolvedPath,
|
|
1752
|
+
classAs,
|
|
1753
|
+
methodAs,
|
|
1754
|
+
sourceFile,
|
|
1755
|
+
seenNames,
|
|
1756
|
+
contractSource: {
|
|
1757
|
+
query: contractDef.query,
|
|
1758
|
+
body: contractDef.body,
|
|
1759
|
+
response: contractDef.response,
|
|
1760
|
+
// Path A: capture both the importable ref and the raw text. The emitter
|
|
1761
|
+
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
1762
|
+
// would drag server-only deps into the client bundle).
|
|
1763
|
+
bodyZodRef,
|
|
1764
|
+
bodyZodText: contractDef.bodyZodText,
|
|
1765
|
+
queryZodRef,
|
|
1766
|
+
queryZodText: contractDef.queryZodText
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
function extractDtoRoute(args) {
|
|
1771
|
+
const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
|
|
1772
|
+
if (!verb) return null;
|
|
1773
|
+
const combined = joinPaths(prefix, verb.handlerPath);
|
|
1774
|
+
const methodName = method.getName();
|
|
1775
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
1776
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
1777
|
+
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
1778
|
+
return buildRoute({
|
|
1779
|
+
className,
|
|
1780
|
+
methodName,
|
|
1781
|
+
resolvedMethod: verb.httpMethod,
|
|
1782
|
+
combinedPath: combined,
|
|
1783
|
+
classAs,
|
|
1784
|
+
methodAs,
|
|
1785
|
+
sourceFile,
|
|
1786
|
+
seenNames,
|
|
1787
|
+
contractSource: {
|
|
1788
|
+
query: dtoContract?.query ?? null,
|
|
1789
|
+
body: dtoContract?.body ?? null,
|
|
1790
|
+
response: dtoContract?.response ?? "unknown",
|
|
1791
|
+
queryRef: dtoContract?.queryRef ?? null,
|
|
1792
|
+
bodyRef: dtoContract?.bodyRef ?? null,
|
|
1793
|
+
responseRef: dtoContract?.responseRef ?? null,
|
|
1794
|
+
filterFields: dtoContract?.filterFields ?? null,
|
|
1795
|
+
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
1796
|
+
filterSource: dtoContract?.filterSource ?? null,
|
|
1797
|
+
formWarnings: dtoContract?.formWarnings ?? [],
|
|
1798
|
+
bodySchema: dtoContract?.bodySchema ?? null,
|
|
1799
|
+
querySchema: dtoContract?.querySchema ?? null
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
2085
1803
|
function extractFromSourceFile(sourceFile, project) {
|
|
2086
1804
|
const routes = [];
|
|
2087
1805
|
const seenNames = /* @__PURE__ */ new Map();
|
|
2088
|
-
const
|
|
2089
|
-
for (const cls of classes) {
|
|
1806
|
+
for (const cls of sourceFile.getClasses()) {
|
|
2090
1807
|
const controllerDecorator = cls.getDecorator("Controller");
|
|
2091
1808
|
if (!controllerDecorator) continue;
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2094
|
-
const prefix = decoratorStringArg(firstArg3) ?? "";
|
|
1809
|
+
const firstArg2 = controllerDecorator.getArguments()[0];
|
|
1810
|
+
const prefix = decoratorStringArg(firstArg2) ?? "";
|
|
2095
1811
|
const className = cls.getName() ?? "Unknown";
|
|
2096
1812
|
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
|
-
}
|
|
1813
|
+
const verb = resolveVerb(method);
|
|
2109
1814
|
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
|
-
}
|
|
1815
|
+
const route = applyContractDecorator ? extractContractRoute({
|
|
1816
|
+
cls,
|
|
1817
|
+
method,
|
|
1818
|
+
applyContractDecorator,
|
|
1819
|
+
verb,
|
|
1820
|
+
prefix,
|
|
1821
|
+
className,
|
|
1822
|
+
sourceFile,
|
|
1823
|
+
seenNames
|
|
1824
|
+
}) : extractDtoRoute({
|
|
1825
|
+
cls,
|
|
1826
|
+
method,
|
|
1827
|
+
verb,
|
|
1828
|
+
prefix,
|
|
1829
|
+
className,
|
|
1830
|
+
sourceFile,
|
|
1831
|
+
project,
|
|
1832
|
+
seenNames
|
|
1833
|
+
});
|
|
1834
|
+
if (route) routes.push(route);
|
|
2255
1835
|
}
|
|
2256
1836
|
}
|
|
2257
1837
|
return routes;
|
|
@@ -2259,8 +1839,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
2259
1839
|
|
|
2260
1840
|
// src/generate.ts
|
|
2261
1841
|
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";
|
|
1842
|
+
import { dirname as dirname3, join as join11 } from "path";
|
|
2264
1843
|
|
|
2265
1844
|
// src/discovery/pages.ts
|
|
2266
1845
|
import { readFile } from "fs/promises";
|
|
@@ -2315,7 +1894,8 @@ function extractPropsSource(source, exportName) {
|
|
|
2315
1894
|
}
|
|
2316
1895
|
|
|
2317
1896
|
// src/discovery/shared-props.ts
|
|
2318
|
-
import {
|
|
1897
|
+
import { join as join4 } from "path";
|
|
1898
|
+
import { Node as Node8, Project as Project2, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
2319
1899
|
function discoverSharedProps(project, moduleEntry) {
|
|
2320
1900
|
try {
|
|
2321
1901
|
let sourceFile = project.getSourceFile(moduleEntry);
|
|
@@ -2335,14 +1915,39 @@ function discoverSharedProps(project, moduleEntry) {
|
|
|
2335
1915
|
return null;
|
|
2336
1916
|
}
|
|
2337
1917
|
}
|
|
1918
|
+
function discoverSharedPropsFromConfig(config) {
|
|
1919
|
+
if (!config.app?.moduleEntry) return null;
|
|
1920
|
+
try {
|
|
1921
|
+
const tsconfigPath = config.app.tsconfig ?? join4(config.codegen.cwd, "tsconfig.json");
|
|
1922
|
+
let project;
|
|
1923
|
+
try {
|
|
1924
|
+
project = new Project2({
|
|
1925
|
+
tsConfigFilePath: tsconfigPath,
|
|
1926
|
+
skipAddingFilesFromTsConfig: true,
|
|
1927
|
+
skipLoadingLibFiles: true,
|
|
1928
|
+
skipFileDependencyResolution: true
|
|
1929
|
+
});
|
|
1930
|
+
} catch {
|
|
1931
|
+
project = new Project2({
|
|
1932
|
+
skipAddingFilesFromTsConfig: true,
|
|
1933
|
+
skipLoadingLibFiles: true,
|
|
1934
|
+
skipFileDependencyResolution: true,
|
|
1935
|
+
compilerOptions: { allowJs: true, strict: false }
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
return discoverSharedProps(project, config.app.moduleEntry);
|
|
1939
|
+
} catch {
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
2338
1943
|
function findForRootCall(sourceFile) {
|
|
2339
|
-
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
1944
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression);
|
|
2340
1945
|
for (const call of callExpressions) {
|
|
2341
1946
|
const expr = call.getExpression();
|
|
2342
|
-
if (!
|
|
1947
|
+
if (!Node8.isPropertyAccessExpression(expr)) continue;
|
|
2343
1948
|
const methodName = expr.getName();
|
|
2344
1949
|
const objectExpr = expr.getExpression();
|
|
2345
|
-
if (methodName === "forRoot" &&
|
|
1950
|
+
if (methodName === "forRoot" && Node8.isIdentifier(objectExpr)) {
|
|
2346
1951
|
const name = objectExpr.getText();
|
|
2347
1952
|
if (name === "InertiaModule") {
|
|
2348
1953
|
return call;
|
|
@@ -2352,19 +1957,19 @@ function findForRootCall(sourceFile) {
|
|
|
2352
1957
|
return null;
|
|
2353
1958
|
}
|
|
2354
1959
|
function findShareInitializer(forRootCall) {
|
|
2355
|
-
if (!
|
|
1960
|
+
if (!Node8.isCallExpression(forRootCall)) return null;
|
|
2356
1961
|
const args = forRootCall.getArguments();
|
|
2357
|
-
const
|
|
2358
|
-
if (!
|
|
2359
|
-
for (const prop of
|
|
2360
|
-
if (
|
|
1962
|
+
const firstArg2 = args[0];
|
|
1963
|
+
if (!firstArg2 || !Node8.isObjectLiteralExpression(firstArg2)) return null;
|
|
1964
|
+
for (const prop of firstArg2.getProperties()) {
|
|
1965
|
+
if (Node8.isPropertyAssignment(prop) && prop.getName() === "share") {
|
|
2361
1966
|
return prop.getInitializer() ?? null;
|
|
2362
1967
|
}
|
|
2363
1968
|
}
|
|
2364
1969
|
return null;
|
|
2365
1970
|
}
|
|
2366
1971
|
function extractShareType(node, sourceFile, project) {
|
|
2367
|
-
if (
|
|
1972
|
+
if (Node8.isIdentifier(node)) {
|
|
2368
1973
|
const ref = resolveIdentifierToImportRef(node, sourceFile, project);
|
|
2369
1974
|
if (ref) {
|
|
2370
1975
|
return {
|
|
@@ -2374,22 +1979,22 @@ function extractShareType(node, sourceFile, project) {
|
|
|
2374
1979
|
};
|
|
2375
1980
|
}
|
|
2376
1981
|
}
|
|
2377
|
-
if (
|
|
1982
|
+
if (Node8.isArrowFunction(node)) {
|
|
2378
1983
|
const result = extractFromFunctionLike(node, sourceFile);
|
|
2379
1984
|
return result ? { ...result, isImportRef: false } : null;
|
|
2380
1985
|
}
|
|
2381
|
-
if (
|
|
1986
|
+
if (Node8.isFunctionExpression(node)) {
|
|
2382
1987
|
const result = extractFromFunctionLike(node, sourceFile);
|
|
2383
1988
|
return result ? { ...result, isImportRef: false } : null;
|
|
2384
1989
|
}
|
|
2385
|
-
if (
|
|
1990
|
+
if (Node8.isObjectLiteralExpression(node)) {
|
|
2386
1991
|
const result = extractFromObjectLiteral(node);
|
|
2387
1992
|
return result ? { ...result, isImportRef: false } : null;
|
|
2388
1993
|
}
|
|
2389
1994
|
return null;
|
|
2390
1995
|
}
|
|
2391
1996
|
function resolveIdentifierToImportRef(id, sourceFile, project) {
|
|
2392
|
-
if (!
|
|
1997
|
+
if (!Node8.isIdentifier(id)) return null;
|
|
2393
1998
|
const name = id.getText();
|
|
2394
1999
|
const localFunc = sourceFile.getFunction(name);
|
|
2395
2000
|
if (localFunc?.isExported()) {
|
|
@@ -2423,49 +2028,49 @@ function resolveIdentifierToImportRef(id, sourceFile, project) {
|
|
|
2423
2028
|
return null;
|
|
2424
2029
|
}
|
|
2425
2030
|
function extractFromFunctionLike(node, _sourceFile) {
|
|
2426
|
-
const returnTypeNode =
|
|
2031
|
+
const returnTypeNode = Node8.isArrowFunction(node) || Node8.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
|
|
2427
2032
|
if (returnTypeNode) {
|
|
2428
2033
|
return extractFromReturnTypeAnnotation(returnTypeNode);
|
|
2429
2034
|
}
|
|
2430
|
-
if (
|
|
2035
|
+
if (Node8.isArrowFunction(node)) {
|
|
2431
2036
|
const body = node.getBody();
|
|
2432
|
-
if (
|
|
2037
|
+
if (Node8.isParenthesizedExpression(body)) {
|
|
2433
2038
|
const inner = body.getExpression();
|
|
2434
|
-
if (
|
|
2039
|
+
if (Node8.isObjectLiteralExpression(inner)) {
|
|
2435
2040
|
return extractFromObjectLiteral(inner);
|
|
2436
2041
|
}
|
|
2437
2042
|
}
|
|
2438
|
-
if (
|
|
2043
|
+
if (Node8.isObjectLiteralExpression(body)) {
|
|
2439
2044
|
return extractFromObjectLiteral(body);
|
|
2440
2045
|
}
|
|
2441
|
-
if (
|
|
2046
|
+
if (Node8.isBlock(body)) {
|
|
2442
2047
|
return extractFromBlockReturn(body);
|
|
2443
2048
|
}
|
|
2444
2049
|
}
|
|
2445
|
-
if (
|
|
2050
|
+
if (Node8.isFunctionExpression(node)) {
|
|
2446
2051
|
const body = node.getBody();
|
|
2447
|
-
if (
|
|
2052
|
+
if (Node8.isBlock(body)) {
|
|
2448
2053
|
return extractFromBlockReturn(body);
|
|
2449
2054
|
}
|
|
2450
2055
|
}
|
|
2451
2056
|
return null;
|
|
2452
2057
|
}
|
|
2453
2058
|
function extractFromReturnTypeAnnotation(typeNode) {
|
|
2454
|
-
if (
|
|
2059
|
+
if (Node8.isTypeReference(typeNode)) {
|
|
2455
2060
|
const typeName = typeNode.getTypeName();
|
|
2456
|
-
if (
|
|
2061
|
+
if (Node8.isIdentifier(typeName) && typeName.getText() === "Promise") {
|
|
2457
2062
|
const typeArgs = typeNode.getTypeArguments();
|
|
2458
|
-
const
|
|
2459
|
-
if (
|
|
2460
|
-
return extractFromReturnTypeAnnotation(
|
|
2063
|
+
const firstArg2 = typeArgs[0];
|
|
2064
|
+
if (firstArg2) {
|
|
2065
|
+
return extractFromReturnTypeAnnotation(firstArg2);
|
|
2461
2066
|
}
|
|
2462
2067
|
return null;
|
|
2463
2068
|
}
|
|
2464
2069
|
}
|
|
2465
|
-
if (
|
|
2070
|
+
if (Node8.isTypeLiteral(typeNode)) {
|
|
2466
2071
|
const properties = [];
|
|
2467
2072
|
for (const member of typeNode.getMembers()) {
|
|
2468
|
-
if (
|
|
2073
|
+
if (Node8.isPropertySignature(member)) {
|
|
2469
2074
|
const name = member.getName();
|
|
2470
2075
|
const memberTypeNode = member.getTypeNode();
|
|
2471
2076
|
const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
|
|
@@ -2479,19 +2084,19 @@ function extractFromReturnTypeAnnotation(typeNode) {
|
|
|
2479
2084
|
return null;
|
|
2480
2085
|
}
|
|
2481
2086
|
function extractFromBlockReturn(block) {
|
|
2482
|
-
if (!
|
|
2087
|
+
if (!Node8.isBlock(block)) return null;
|
|
2483
2088
|
const statements = block.getStatements();
|
|
2484
2089
|
for (let i = statements.length - 1; i >= 0; i--) {
|
|
2485
2090
|
const stmt = statements[i];
|
|
2486
|
-
if (!
|
|
2091
|
+
if (!Node8.isReturnStatement(stmt)) continue;
|
|
2487
2092
|
const expr = stmt.getExpression();
|
|
2488
2093
|
if (!expr) continue;
|
|
2489
|
-
if (
|
|
2094
|
+
if (Node8.isObjectLiteralExpression(expr)) {
|
|
2490
2095
|
return extractFromObjectLiteral(expr);
|
|
2491
2096
|
}
|
|
2492
|
-
if (
|
|
2097
|
+
if (Node8.isParenthesizedExpression(expr)) {
|
|
2493
2098
|
const inner = expr.getExpression();
|
|
2494
|
-
if (
|
|
2099
|
+
if (Node8.isObjectLiteralExpression(inner)) {
|
|
2495
2100
|
return extractFromObjectLiteral(inner);
|
|
2496
2101
|
}
|
|
2497
2102
|
}
|
|
@@ -2500,10 +2105,10 @@ function extractFromBlockReturn(block) {
|
|
|
2500
2105
|
return null;
|
|
2501
2106
|
}
|
|
2502
2107
|
function extractFromObjectLiteral(objLiteral) {
|
|
2503
|
-
if (!
|
|
2108
|
+
if (!Node8.isObjectLiteralExpression(objLiteral)) return null;
|
|
2504
2109
|
const properties = [];
|
|
2505
2110
|
for (const prop of objLiteral.getProperties()) {
|
|
2506
|
-
if (!
|
|
2111
|
+
if (!Node8.isPropertyAssignment(prop)) continue;
|
|
2507
2112
|
const name = prop.getName();
|
|
2508
2113
|
const initializer = prop.getInitializer();
|
|
2509
2114
|
if (!initializer) continue;
|
|
@@ -2515,21 +2120,21 @@ function extractFromObjectLiteral(objLiteral) {
|
|
|
2515
2120
|
return { typeString, properties, isImportRef: false };
|
|
2516
2121
|
}
|
|
2517
2122
|
function inferExpressionType(node) {
|
|
2518
|
-
if (
|
|
2519
|
-
if (
|
|
2123
|
+
if (Node8.isStringLiteral(node)) return "string";
|
|
2124
|
+
if (Node8.isTemplateExpression(node) || Node8.isNoSubstitutionTemplateLiteral(node))
|
|
2520
2125
|
return "string";
|
|
2521
|
-
if (
|
|
2522
|
-
if (node.getKind() ===
|
|
2126
|
+
if (Node8.isNumericLiteral(node)) return "number";
|
|
2127
|
+
if (node.getKind() === SyntaxKind4.TrueKeyword || node.getKind() === SyntaxKind4.FalseKeyword) {
|
|
2523
2128
|
return "boolean";
|
|
2524
2129
|
}
|
|
2525
|
-
if (node.getKind() ===
|
|
2526
|
-
if (
|
|
2527
|
-
if (
|
|
2130
|
+
if (node.getKind() === SyntaxKind4.NullKeyword) return "null";
|
|
2131
|
+
if (Node8.isIdentifier(node) && node.getText() === "undefined") return "undefined";
|
|
2132
|
+
if (Node8.isObjectLiteralExpression(node)) {
|
|
2528
2133
|
const props = node.getProperties();
|
|
2529
2134
|
if (props.length === 0) return "Record<string, unknown>";
|
|
2530
2135
|
const entries = [];
|
|
2531
2136
|
for (const prop of props) {
|
|
2532
|
-
if (!
|
|
2137
|
+
if (!Node8.isPropertyAssignment(prop)) continue;
|
|
2533
2138
|
const key = prop.getName();
|
|
2534
2139
|
const init = prop.getInitializer();
|
|
2535
2140
|
if (!init) continue;
|
|
@@ -2538,23 +2143,23 @@ function inferExpressionType(node) {
|
|
|
2538
2143
|
if (entries.length === 0) return "Record<string, unknown>";
|
|
2539
2144
|
return `{ ${entries.join("; ")} }`;
|
|
2540
2145
|
}
|
|
2541
|
-
if (
|
|
2146
|
+
if (Node8.isArrayLiteralExpression(node)) {
|
|
2542
2147
|
const elements = node.getElements();
|
|
2543
2148
|
if (elements.length === 0) return "Array<unknown>";
|
|
2544
2149
|
const first = elements[0];
|
|
2545
2150
|
if (first) return `Array<${inferExpressionType(first)}>`;
|
|
2546
2151
|
return "Array<unknown>";
|
|
2547
2152
|
}
|
|
2548
|
-
if (
|
|
2153
|
+
if (Node8.isConditionalExpression(node)) {
|
|
2549
2154
|
const whenTrue = inferExpressionType(node.getWhenTrue());
|
|
2550
2155
|
const whenFalse = inferExpressionType(node.getWhenFalse());
|
|
2551
2156
|
if (whenTrue === whenFalse) return whenTrue;
|
|
2552
2157
|
return `${whenTrue} | ${whenFalse}`;
|
|
2553
2158
|
}
|
|
2554
|
-
if (
|
|
2159
|
+
if (Node8.isParenthesizedExpression(node)) {
|
|
2555
2160
|
return inferExpressionType(node.getExpression());
|
|
2556
2161
|
}
|
|
2557
|
-
if (
|
|
2162
|
+
if (Node8.isAsExpression(node)) {
|
|
2558
2163
|
const typeNode = node.getTypeNode();
|
|
2559
2164
|
if (typeNode) return typeNode.getText();
|
|
2560
2165
|
}
|
|
@@ -2563,25 +2168,14 @@ function inferExpressionType(node) {
|
|
|
2563
2168
|
|
|
2564
2169
|
// src/emit/emit-api.ts
|
|
2565
2170
|
import { mkdir, writeFile } from "fs/promises";
|
|
2566
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2171
|
+
import { isAbsolute as isAbsolute2, join as join5, relative as relative3 } from "path";
|
|
2567
2172
|
|
|
2568
2173
|
// src/extension/registry.ts
|
|
2569
|
-
import { Project as
|
|
2174
|
+
import { Project as Project3 } from "ts-morph";
|
|
2570
2175
|
function resolveApiSlots(extensions) {
|
|
2571
|
-
let transport;
|
|
2572
|
-
let transportOwner;
|
|
2573
2176
|
let layer;
|
|
2574
2177
|
let layerOwner;
|
|
2575
2178
|
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
2179
|
if (ext.apiClientLayer) {
|
|
2586
2180
|
if (layer) {
|
|
2587
2181
|
throw new CodegenError(
|
|
@@ -2593,11 +2187,22 @@ function resolveApiSlots(extensions) {
|
|
|
2593
2187
|
}
|
|
2594
2188
|
}
|
|
2595
2189
|
return {
|
|
2596
|
-
...transport ? { transport } : {},
|
|
2597
2190
|
...layer ? { layer } : {}
|
|
2598
2191
|
};
|
|
2599
2192
|
}
|
|
2600
2193
|
var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
|
|
2194
|
+
function mergeExclusive(target, incoming, {
|
|
2195
|
+
owner,
|
|
2196
|
+
describe
|
|
2197
|
+
}) {
|
|
2198
|
+
for (const [key, value] of incoming) {
|
|
2199
|
+
const prev = target.get(key);
|
|
2200
|
+
if (prev !== void 0) {
|
|
2201
|
+
throw new CodegenError(describe(key, prev.owner, owner));
|
|
2202
|
+
}
|
|
2203
|
+
target.set(key, { value, owner });
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2601
2206
|
function createExtensionContext(config, getRoutes) {
|
|
2602
2207
|
let project;
|
|
2603
2208
|
return {
|
|
@@ -2609,7 +2214,7 @@ function createExtensionContext(config, getRoutes) {
|
|
|
2609
2214
|
},
|
|
2610
2215
|
project() {
|
|
2611
2216
|
if (!project) {
|
|
2612
|
-
project = new
|
|
2217
|
+
project = new Project3({
|
|
2613
2218
|
skipAddingFilesFromTsConfig: true,
|
|
2614
2219
|
skipLoadingLibFiles: true,
|
|
2615
2220
|
skipFileDependencyResolution: true,
|
|
@@ -2642,29 +2247,36 @@ async function collectEmittedFiles(extensions, ctx) {
|
|
|
2642
2247
|
`Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
|
|
2643
2248
|
);
|
|
2644
2249
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
);
|
|
2650
|
-
}
|
|
2651
|
-
owners.set(key, ext.name);
|
|
2250
|
+
mergeExclusive(owners, [[key, file]], {
|
|
2251
|
+
owner: ext.name,
|
|
2252
|
+
describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
|
|
2253
|
+
});
|
|
2652
2254
|
files.push(file);
|
|
2653
2255
|
}
|
|
2654
2256
|
}
|
|
2655
2257
|
return files;
|
|
2656
2258
|
}
|
|
2657
2259
|
|
|
2260
|
+
// src/extension/types.ts
|
|
2261
|
+
function requestShape(route) {
|
|
2262
|
+
const cs = route.contract?.contractSource;
|
|
2263
|
+
const isGet = route.method.toUpperCase() === "GET";
|
|
2264
|
+
const isQuery = isGet || !!cs?.filterFields?.length;
|
|
2265
|
+
const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
|
|
2266
|
+
const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
|
|
2267
|
+
return { isGet, isQuery, hasBody, hasQuery };
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2658
2270
|
// src/emit/emit-api.ts
|
|
2659
2271
|
async function emitApi(routes, outDir, opts = {}) {
|
|
2660
2272
|
await mkdir(outDir, { recursive: true });
|
|
2661
2273
|
const content = buildApiFile(routes, outDir, opts);
|
|
2662
|
-
await writeFile(
|
|
2274
|
+
await writeFile(join5(outDir, "api.ts"), content, "utf8");
|
|
2663
2275
|
}
|
|
2664
2276
|
function splitName(name) {
|
|
2665
2277
|
return name.split(".");
|
|
2666
2278
|
}
|
|
2667
|
-
function
|
|
2279
|
+
function toObjectKey(segment) {
|
|
2668
2280
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
|
|
2669
2281
|
return segment;
|
|
2670
2282
|
}
|
|
@@ -2771,7 +2383,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2771
2383
|
const pad = " ".repeat(indent);
|
|
2772
2384
|
const lines = [];
|
|
2773
2385
|
for (const [key, node] of tree) {
|
|
2774
|
-
const objKey =
|
|
2386
|
+
const objKey = toObjectKey(key);
|
|
2775
2387
|
if (node.kind === "leaf") {
|
|
2776
2388
|
const c = node;
|
|
2777
2389
|
const method = c.method.toUpperCase();
|
|
@@ -2797,15 +2409,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2797
2409
|
return lines;
|
|
2798
2410
|
}
|
|
2799
2411
|
function buildRequestModel(c) {
|
|
2800
|
-
const isGet = c.method.toUpperCase() === "GET";
|
|
2801
2412
|
const m = c.method.toLowerCase();
|
|
2802
2413
|
const flat = JSON.stringify(c.name);
|
|
2803
2414
|
const path = JSON.stringify(c.path);
|
|
2804
2415
|
const TA = buildRouterTypeAccess(c.name);
|
|
2805
2416
|
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";
|
|
2417
|
+
const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
|
|
2809
2418
|
const fields = [];
|
|
2810
2419
|
if (withParams) fields.push(`params: ${TA}['params']`);
|
|
2811
2420
|
if (hasQuery) fields.push(`query?: ${TA}['query']`);
|
|
@@ -2827,7 +2436,6 @@ function buildRequestModel(c) {
|
|
|
2827
2436
|
urlExpr,
|
|
2828
2437
|
optsExpr,
|
|
2829
2438
|
responseType: `${TA}['response']`,
|
|
2830
|
-
bodyType: `${TA}['body']`,
|
|
2831
2439
|
queryKeyExpr: `[${flat}, input] as const`
|
|
2832
2440
|
};
|
|
2833
2441
|
}
|
|
@@ -2875,7 +2483,7 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2875
2483
|
const pad = " ".repeat(indent);
|
|
2876
2484
|
const lines = [];
|
|
2877
2485
|
for (const [key, node] of tree) {
|
|
2878
|
-
const objKey =
|
|
2486
|
+
const objKey = toObjectKey(key);
|
|
2879
2487
|
if (node.kind === "branch") {
|
|
2880
2488
|
lines.push(`${pad}${objKey}: {`);
|
|
2881
2489
|
lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
|
|
@@ -2883,30 +2491,28 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2883
2491
|
continue;
|
|
2884
2492
|
}
|
|
2885
2493
|
const req = buildRequestModel(node);
|
|
2886
|
-
const
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
params: node.params,
|
|
2891
|
-
contract: { contractSource: node.contractSource },
|
|
2892
|
-
...node.controllerRef ? { controllerRef: node.controllerRef } : {}
|
|
2494
|
+
const leaf = {
|
|
2495
|
+
route: node.route,
|
|
2496
|
+
request: req,
|
|
2497
|
+
requestExpr: renderFetcherRequest(req)
|
|
2893
2498
|
};
|
|
2894
|
-
const
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2499
|
+
const owned = /* @__PURE__ */ new Map();
|
|
2500
|
+
if (p.layer) {
|
|
2501
|
+
mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
|
|
2502
|
+
owner: p.layer.name,
|
|
2503
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2898
2506
|
for (const ext of p.memberExts) {
|
|
2899
2507
|
const extra = ext.apiMembers?.(leaf, p.ctx);
|
|
2900
2508
|
if (!extra) continue;
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
);
|
|
2906
|
-
}
|
|
2907
|
-
members[name] = value;
|
|
2908
|
-
}
|
|
2509
|
+
mergeExclusive(owned, Object.entries(extra), {
|
|
2510
|
+
owner: ext.name,
|
|
2511
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
2512
|
+
});
|
|
2909
2513
|
}
|
|
2514
|
+
const members = {};
|
|
2515
|
+
for (const [name, { value }] of owned) members[name] = value;
|
|
2910
2516
|
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
|
|
2911
2517
|
}
|
|
2912
2518
|
return lines;
|
|
@@ -2915,10 +2521,82 @@ function buildRouterTypeAccess(name) {
|
|
|
2915
2521
|
const segments = splitName(name);
|
|
2916
2522
|
return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
|
|
2917
2523
|
}
|
|
2524
|
+
var RESOLVER_HELPERS = [
|
|
2525
|
+
// --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
|
|
2526
|
+
"type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
|
|
2527
|
+
" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
|
|
2528
|
+
" : P extends keyof R ? R[P] : never;",
|
|
2529
|
+
"",
|
|
2530
|
+
// --- ResolveByName: resolve a field from a dot-path name ---
|
|
2531
|
+
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
|
|
2532
|
+
"",
|
|
2533
|
+
// --- ResolveByPath: scan all leaves for matching method + url ---
|
|
2534
|
+
// Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
|
|
2535
|
+
"type _LeafValues<T> = T extends { method: string; url: string }",
|
|
2536
|
+
" ? T",
|
|
2537
|
+
" : T extends object ? _LeafValues<T[keyof T]> : never;",
|
|
2538
|
+
"",
|
|
2539
|
+
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
|
|
2540
|
+
" ? L extends { method: M; url: U }",
|
|
2541
|
+
" ? Field extends keyof L ? L[Field] : never",
|
|
2542
|
+
" : never",
|
|
2543
|
+
" : never;",
|
|
2544
|
+
""
|
|
2545
|
+
];
|
|
2546
|
+
var ROUTE_NAMESPACE = [
|
|
2547
|
+
"export namespace Route {",
|
|
2548
|
+
' export type Response<K extends string> = ResolveByName<K, "response">;',
|
|
2549
|
+
' export type Body<K extends string> = ResolveByName<K, "body">;',
|
|
2550
|
+
' export type Query<K extends string> = ResolveByName<K, "query">;',
|
|
2551
|
+
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
2552
|
+
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
2553
|
+
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
2554
|
+
" export type Request<K extends string> = {",
|
|
2555
|
+
" body: Body<K>;",
|
|
2556
|
+
" query: Query<K>;",
|
|
2557
|
+
" params: Params<K>;",
|
|
2558
|
+
" };",
|
|
2559
|
+
"}",
|
|
2560
|
+
""
|
|
2561
|
+
];
|
|
2562
|
+
var PATH_NAMESPACE = [
|
|
2563
|
+
"export namespace Path {",
|
|
2564
|
+
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
2565
|
+
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
|
|
2566
|
+
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
|
|
2567
|
+
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
2568
|
+
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
2569
|
+
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
2570
|
+
"}",
|
|
2571
|
+
""
|
|
2572
|
+
];
|
|
2573
|
+
var EMPTY_ROUTE_NAMESPACE = [
|
|
2574
|
+
"export namespace Route {",
|
|
2575
|
+
" export type Response<K extends string> = never;",
|
|
2576
|
+
" export type Body<K extends string> = never;",
|
|
2577
|
+
" export type Query<K extends string> = never;",
|
|
2578
|
+
" export type Params<K extends string> = never;",
|
|
2579
|
+
" export type Error<K extends string> = never;",
|
|
2580
|
+
" export type FilterFields<K extends string> = never;",
|
|
2581
|
+
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
2582
|
+
"}",
|
|
2583
|
+
""
|
|
2584
|
+
];
|
|
2585
|
+
var EMPTY_PATH_NAMESPACE = [
|
|
2586
|
+
"export namespace Path {",
|
|
2587
|
+
" export type Response<M extends string, U extends string> = never;",
|
|
2588
|
+
" export type Body<M extends string, U extends string> = never;",
|
|
2589
|
+
" export type Query<M extends string, U extends string> = never;",
|
|
2590
|
+
" export type Params<M extends string, U extends string> = never;",
|
|
2591
|
+
" export type Error<M extends string, U extends string> = never;",
|
|
2592
|
+
" export type FilterFields<M extends string, U extends string> = never;",
|
|
2593
|
+
"}",
|
|
2594
|
+
""
|
|
2595
|
+
];
|
|
2918
2596
|
function buildApiFile(routes, outDir, opts = {}) {
|
|
2919
2597
|
const fetcherImportPath = opts.fetcherImportPath;
|
|
2920
2598
|
const extensions = opts.extensions ?? [];
|
|
2921
|
-
const {
|
|
2599
|
+
const { layer } = resolveApiSlots(extensions);
|
|
2922
2600
|
const memberExts = extensions.filter((e) => e.apiMembers);
|
|
2923
2601
|
const headerExts = extensions.filter((e) => e.apiHeader);
|
|
2924
2602
|
const contracted = routes.filter((r) => r.contract);
|
|
@@ -2963,7 +2641,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
2963
2641
|
seenImports.add(imp);
|
|
2964
2642
|
extImports.push(imp);
|
|
2965
2643
|
};
|
|
2966
|
-
for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
|
|
2967
2644
|
for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
|
|
2968
2645
|
for (const ext of headerExts) {
|
|
2969
2646
|
for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
|
|
@@ -3008,27 +2685,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3008
2685
|
lines.push("}");
|
|
3009
2686
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
3010
2687
|
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("");
|
|
2688
|
+
lines.push(...EMPTY_ROUTE_NAMESPACE);
|
|
2689
|
+
lines.push(...EMPTY_PATH_NAMESPACE);
|
|
3032
2690
|
return lines.join("\n");
|
|
3033
2691
|
}
|
|
3034
2692
|
const tree = /* @__PURE__ */ new Map();
|
|
@@ -3046,7 +2704,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3046
2704
|
path: r.path,
|
|
3047
2705
|
params: r.params,
|
|
3048
2706
|
controllerRef: r.controllerRef,
|
|
3049
|
-
contractSource: c.contractSource
|
|
2707
|
+
contractSource: c.contractSource,
|
|
2708
|
+
route: r
|
|
3050
2709
|
};
|
|
3051
2710
|
insertIntoTree(tree, segments, leaf, name);
|
|
3052
2711
|
}
|
|
@@ -3059,7 +2718,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3059
2718
|
lines.push(" return {");
|
|
3060
2719
|
lines.push(
|
|
3061
2720
|
...emitApiObjectBlock(tree, 4, {
|
|
3062
|
-
...transport ? { transport } : {},
|
|
3063
2721
|
...layer ? { layer } : {},
|
|
3064
2722
|
memberExts,
|
|
3065
2723
|
ctx
|
|
@@ -3070,61 +2728,9 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3070
2728
|
lines.push("");
|
|
3071
2729
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
3072
2730
|
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("");
|
|
2731
|
+
lines.push(...RESOLVER_HELPERS);
|
|
2732
|
+
lines.push(...ROUTE_NAMESPACE);
|
|
2733
|
+
lines.push(...PATH_NAMESPACE);
|
|
3128
2734
|
for (const ext of headerExts) {
|
|
3129
2735
|
const statements = ext.apiHeader?.(ctx)?.statements;
|
|
3130
2736
|
if (statements?.length) {
|
|
@@ -3136,7 +2742,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3136
2742
|
|
|
3137
2743
|
// src/emit/emit-cache.ts
|
|
3138
2744
|
import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
3139
|
-
import { join as
|
|
2745
|
+
import { join as join6 } from "path";
|
|
3140
2746
|
async function emitCache(pages, outDir) {
|
|
3141
2747
|
await mkdir2(outDir, { recursive: true });
|
|
3142
2748
|
const entries = await Promise.all(
|
|
@@ -3150,95 +2756,21 @@ async function emitCache(pages, outDir) {
|
|
|
3150
2756
|
})
|
|
3151
2757
|
);
|
|
3152
2758
|
const cache = { pages: entries };
|
|
3153
|
-
await writeFile2(
|
|
2759
|
+
await writeFile2(join6(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
|
|
3154
2760
|
`, "utf8");
|
|
3155
2761
|
}
|
|
3156
2762
|
|
|
3157
2763
|
// src/emit/emit-forms.ts
|
|
3158
2764
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
3159
|
-
import { join as
|
|
2765
|
+
import { join as join7, relative as relative4 } from "path";
|
|
3160
2766
|
async function emitForms(routes, outDir, config, adapter) {
|
|
3161
2767
|
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;
|
|
2768
|
+
const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
|
|
2769
|
+
if (content === null) return false;
|
|
3171
2770
|
await mkdir3(outDir, { recursive: true });
|
|
3172
|
-
|
|
3173
|
-
await writeFile3(join6(outDir, "forms.ts"), content, "utf8");
|
|
2771
|
+
await writeFile3(join7(outDir, "forms.ts"), content, "utf8");
|
|
3174
2772
|
return true;
|
|
3175
2773
|
}
|
|
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
2774
|
function pascal(segment) {
|
|
3243
2775
|
return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
3244
2776
|
}
|
|
@@ -3248,37 +2780,6 @@ function deriveBaseName(routeName) {
|
|
|
3248
2780
|
const full = segments.map(pascal).join("");
|
|
3249
2781
|
return { method, full };
|
|
3250
2782
|
}
|
|
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
2783
|
function relImport(outDir, filePath) {
|
|
3283
2784
|
let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
|
|
3284
2785
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
@@ -3287,85 +2788,8 @@ function relImport(outDir, filePath) {
|
|
|
3287
2788
|
function refRootIdentifier(refName) {
|
|
3288
2789
|
return refName.split(".")[0] ?? refName;
|
|
3289
2790
|
}
|
|
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");
|
|
2791
|
+
function hasSource(src) {
|
|
2792
|
+
return !!(src.schema || src.zodText || src.zodRef);
|
|
3369
2793
|
}
|
|
3370
2794
|
function applyRenames(text, renames) {
|
|
3371
2795
|
if (!renames || renames.size === 0) return text;
|
|
@@ -3431,20 +2855,172 @@ function planNestedSchemas(entries) {
|
|
|
3431
2855
|
}
|
|
3432
2856
|
return { globalSchemas, renamesByEntry };
|
|
3433
2857
|
}
|
|
3434
|
-
function
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
2858
|
+
function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
|
|
2859
|
+
const acceptsRawZod = adapter.acceptsRawZodSource === true;
|
|
2860
|
+
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
2861
|
+
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
2862
|
+
const candidates = [];
|
|
2863
|
+
for (const route of sorted) {
|
|
2864
|
+
const cs = route.contract.contractSource;
|
|
2865
|
+
const body = {
|
|
2866
|
+
schema: cs.bodySchema ?? null,
|
|
2867
|
+
zodText: cs.bodyZodText ?? null,
|
|
2868
|
+
zodRef: cs.bodyZodRef ?? null
|
|
2869
|
+
};
|
|
2870
|
+
const query = {
|
|
2871
|
+
schema: cs.querySchema ?? null,
|
|
2872
|
+
zodText: cs.queryZodText ?? null,
|
|
2873
|
+
zodRef: cs.queryZodRef ?? null
|
|
2874
|
+
};
|
|
2875
|
+
if (!hasSource(body) && !hasSource(query)) continue;
|
|
2876
|
+
const { method, full } = deriveBaseName(route.name);
|
|
2877
|
+
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
2878
|
+
candidates.push({
|
|
2879
|
+
routeName: route.name,
|
|
2880
|
+
baseName: full,
|
|
2881
|
+
// resolved below
|
|
2882
|
+
body: hasSource(body) ? body : void 0,
|
|
2883
|
+
query: hasSource(query) ? query : void 0,
|
|
2884
|
+
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
2885
|
+
warnings: cs.formWarnings ?? []
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
const entries = candidates.map((c) => {
|
|
2889
|
+
const { method, full } = deriveBaseName(c.routeName);
|
|
2890
|
+
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
2891
|
+
return { ...c, baseName: collision ? full : method };
|
|
2892
|
+
});
|
|
2893
|
+
if (entries.length === 0) return null;
|
|
2894
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
2895
|
+
const refAlias = /* @__PURE__ */ new Map();
|
|
2896
|
+
for (const entry of entries) {
|
|
2897
|
+
for (const src of [entry.body, entry.query]) {
|
|
2898
|
+
if (src?.zodRef && !src.zodText && !src.schema) {
|
|
2899
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
2900
|
+
const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
|
|
2901
|
+
set.add(root);
|
|
2902
|
+
importsByFile.set(src.zodRef.filePath, set);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
const importLines = [];
|
|
2907
|
+
if (importsByFile.size > 0) {
|
|
2908
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2909
|
+
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
2910
|
+
const relPath = relImport(outDir, filePath);
|
|
2911
|
+
const specifiers = [];
|
|
2912
|
+
for (const root of [...roots].sort()) {
|
|
2913
|
+
if (emitted.has(root)) {
|
|
2914
|
+
const alias = `${root}_${emitted.size}`;
|
|
2915
|
+
specifiers.push(`${root} as ${alias}`);
|
|
2916
|
+
emitted.add(alias);
|
|
2917
|
+
refAlias.set(`${filePath}\0${root}`, alias);
|
|
2918
|
+
} else {
|
|
2919
|
+
specifiers.push(root);
|
|
2920
|
+
emitted.add(root);
|
|
2921
|
+
refAlias.set(`${filePath}\0${root}`, root);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
2928
|
+
const irNamed = /* @__PURE__ */ new Map();
|
|
2929
|
+
const decls = [];
|
|
2930
|
+
const mapEntries = [];
|
|
2931
|
+
let used = false;
|
|
2932
|
+
const renderSource = (src, rename) => {
|
|
2933
|
+
if (src.schema) {
|
|
2934
|
+
const r = adapter.renderModule(src.schema);
|
|
2935
|
+
for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
|
|
2936
|
+
return { text: r.schemaText };
|
|
2937
|
+
}
|
|
2938
|
+
if (src.zodText) {
|
|
2939
|
+
if (!acceptsRawZod) {
|
|
2940
|
+
return {
|
|
2941
|
+
text: "",
|
|
2942
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
return { text: applyRenames(src.zodText, rename) };
|
|
2946
|
+
}
|
|
2947
|
+
if (src.zodRef) {
|
|
2948
|
+
if (!acceptsRawZod) {
|
|
2949
|
+
return {
|
|
2950
|
+
text: "",
|
|
2951
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
2955
|
+
const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
|
|
2956
|
+
const member = src.zodRef.name.slice(root.length);
|
|
2957
|
+
return { text: `${alias}${member}` };
|
|
2958
|
+
}
|
|
2959
|
+
return null;
|
|
2960
|
+
};
|
|
2961
|
+
for (const entry of entries) {
|
|
2962
|
+
const block = [];
|
|
2963
|
+
const rename = renamesByEntry.get(entry) ?? null;
|
|
2964
|
+
let bodyConst;
|
|
2965
|
+
if (entry.warnings && entry.warnings.length > 0) {
|
|
2966
|
+
for (const w of entry.warnings) block.push(`// warning: ${w}`);
|
|
2967
|
+
}
|
|
2968
|
+
if (entry.body) {
|
|
2969
|
+
const rendered = renderSource(entry.body, rename);
|
|
2970
|
+
if (rendered?.warn) {
|
|
2971
|
+
block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
|
|
2972
|
+
} else if (rendered) {
|
|
2973
|
+
used = true;
|
|
2974
|
+
bodyConst = `${entry.baseName}BodySchema`;
|
|
2975
|
+
block.push(`export const ${bodyConst} = ${rendered.text};`);
|
|
2976
|
+
block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
if (entry.query) {
|
|
2980
|
+
const rendered = renderSource(entry.query, rename);
|
|
2981
|
+
if (rendered?.warn) {
|
|
2982
|
+
block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
|
|
2983
|
+
} else if (rendered) {
|
|
2984
|
+
used = true;
|
|
2985
|
+
const queryConst = `${entry.baseName}QuerySchema`;
|
|
2986
|
+
block.push(`export const ${queryConst} = ${rendered.text};`);
|
|
2987
|
+
block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
if (block.length === 0) continue;
|
|
2991
|
+
decls.push(`// ${entry.routeName}`, ...block, "");
|
|
2992
|
+
if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
|
|
2993
|
+
}
|
|
2994
|
+
if (!used) return null;
|
|
2995
|
+
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
2996
|
+
if (acceptsRawZod) {
|
|
2997
|
+
const zodImport = config?.zodImport ?? "zod";
|
|
2998
|
+
lines.push(`import { z } from '${zodImport}';`);
|
|
2999
|
+
} else {
|
|
3000
|
+
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
3001
|
+
}
|
|
3002
|
+
lines.push(...importLines);
|
|
3003
|
+
lines.push("");
|
|
3004
|
+
const allNested = /* @__PURE__ */ new Map();
|
|
3005
|
+
for (const [n, t] of globalSchemas) allNested.set(n, t);
|
|
3006
|
+
for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
|
|
3007
|
+
if (allNested.size > 0) {
|
|
3008
|
+
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
3009
|
+
for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
|
|
3010
|
+
lines.push("");
|
|
3441
3011
|
}
|
|
3442
|
-
|
|
3012
|
+
lines.push(...decls);
|
|
3013
|
+
lines.push("/** Route name \u2192 body schema map. */");
|
|
3014
|
+
lines.push("export const formSchemas = {");
|
|
3015
|
+
lines.push(...mapEntries);
|
|
3016
|
+
lines.push("} as const;");
|
|
3017
|
+
lines.push("");
|
|
3018
|
+
return lines.join("\n");
|
|
3443
3019
|
}
|
|
3444
3020
|
|
|
3445
3021
|
// src/emit/emit-index.ts
|
|
3446
3022
|
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
3447
|
-
import { join as
|
|
3023
|
+
import { join as join8 } from "path";
|
|
3448
3024
|
async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
3449
3025
|
await mkdir4(outDir, { recursive: true });
|
|
3450
3026
|
const exports = ["export * from './pages.js';", "export * from './routes.js';"];
|
|
@@ -3457,12 +3033,12 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
3457
3033
|
const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
|
|
3458
3034
|
"\n"
|
|
3459
3035
|
);
|
|
3460
|
-
await writeFile4(
|
|
3036
|
+
await writeFile4(join8(outDir, "index.d.ts"), content, "utf8");
|
|
3461
3037
|
}
|
|
3462
3038
|
|
|
3463
3039
|
// src/emit/emit-pages.ts
|
|
3464
3040
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
3465
|
-
import { join as
|
|
3041
|
+
import { join as join9, relative as relative5 } from "path";
|
|
3466
3042
|
async function emitPages(pages, outDir, _options = {}) {
|
|
3467
3043
|
await mkdir5(outDir, { recursive: true });
|
|
3468
3044
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
@@ -3483,7 +3059,7 @@ ${augBody}
|
|
|
3483
3059
|
}
|
|
3484
3060
|
${sharedPropsBlock}}
|
|
3485
3061
|
`;
|
|
3486
|
-
await writeFile5(
|
|
3062
|
+
await writeFile5(join9(outDir, "pages.d.ts"), content, "utf8");
|
|
3487
3063
|
}
|
|
3488
3064
|
function buildSharedPropsBlock(sharedProps) {
|
|
3489
3065
|
if (!sharedProps) return "";
|
|
@@ -3514,11 +3090,11 @@ function needsQuotes(name) {
|
|
|
3514
3090
|
|
|
3515
3091
|
// src/emit/emit-routes.ts
|
|
3516
3092
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
3517
|
-
import { join as
|
|
3093
|
+
import { join as join10 } from "path";
|
|
3518
3094
|
async function emitRoutes(routes, outDir) {
|
|
3519
3095
|
await mkdir6(outDir, { recursive: true });
|
|
3520
3096
|
const content = buildRoutesFile(routes);
|
|
3521
|
-
await writeFile6(
|
|
3097
|
+
await writeFile6(join10(outDir, "routes.ts"), content, "utf8");
|
|
3522
3098
|
}
|
|
3523
3099
|
function buildRoutesFile(routes) {
|
|
3524
3100
|
if (routes.length === 0) {
|
|
@@ -3646,30 +3222,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
3646
3222
|
propsExport: pagesConfig.propsExport,
|
|
3647
3223
|
componentNameStrategy: pagesConfig.componentNameStrategy
|
|
3648
3224
|
});
|
|
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
|
-
}
|
|
3225
|
+
const sharedProps = discoverSharedPropsFromConfig(config);
|
|
3673
3226
|
await emitPages(pages, config.codegen.outDir, {
|
|
3674
3227
|
propsExport: pagesConfig.propsExport,
|
|
3675
3228
|
sharedProps
|
|
@@ -3693,7 +3246,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
3693
3246
|
if (extensions.length > 0) {
|
|
3694
3247
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
3695
3248
|
for (const file of extraFiles) {
|
|
3696
|
-
const dest =
|
|
3249
|
+
const dest = join11(config.codegen.outDir, file.path);
|
|
3697
3250
|
await mkdir7(dirname3(dest), { recursive: true });
|
|
3698
3251
|
await writeFile7(dest, file.contents, "utf8");
|
|
3699
3252
|
}
|
|
@@ -3703,7 +3256,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
3703
3256
|
// src/watch/lock-file.ts
|
|
3704
3257
|
import { open } from "fs/promises";
|
|
3705
3258
|
import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
|
|
3706
|
-
import { join as
|
|
3259
|
+
import { join as join12 } from "path";
|
|
3707
3260
|
var LOCK_FILE = ".watcher.lock";
|
|
3708
3261
|
function isProcessAlive(pid) {
|
|
3709
3262
|
try {
|
|
@@ -3715,7 +3268,7 @@ function isProcessAlive(pid) {
|
|
|
3715
3268
|
}
|
|
3716
3269
|
async function acquireLock(outDir) {
|
|
3717
3270
|
await mkdir8(outDir, { recursive: true });
|
|
3718
|
-
const lockPath =
|
|
3271
|
+
const lockPath = join12(outDir, LOCK_FILE);
|
|
3719
3272
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3720
3273
|
try {
|
|
3721
3274
|
const fd = await open(lockPath, "wx");
|
|
@@ -3755,7 +3308,7 @@ async function watch(config, onChange) {
|
|
|
3755
3308
|
if (lock === null) {
|
|
3756
3309
|
let holderPid = "unknown";
|
|
3757
3310
|
try {
|
|
3758
|
-
const raw = await readFile3(
|
|
3311
|
+
const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3759
3312
|
const data = JSON.parse(raw);
|
|
3760
3313
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3761
3314
|
} catch {
|
|
@@ -3783,7 +3336,7 @@ async function watch(config, onChange) {
|
|
|
3783
3336
|
}
|
|
3784
3337
|
let pagesDebounceTimer;
|
|
3785
3338
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3786
|
-
const pagesWatcher = chokidar.watch(
|
|
3339
|
+
const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
|
|
3787
3340
|
ignoreInitial: true,
|
|
3788
3341
|
persistent: true,
|
|
3789
3342
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3809,7 +3362,7 @@ async function watch(config, onChange) {
|
|
|
3809
3362
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3810
3363
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3811
3364
|
let contractsDebounceTimer;
|
|
3812
|
-
const contractsWatcher = chokidar.watch(
|
|
3365
|
+
const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
|
|
3813
3366
|
ignoreInitial: true,
|
|
3814
3367
|
persistent: true,
|
|
3815
3368
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3839,7 +3392,7 @@ async function watch(config, onChange) {
|
|
|
3839
3392
|
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3840
3393
|
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3841
3394
|
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3842
|
-
const formsWatcher = chokidar.watch(
|
|
3395
|
+
const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
|
|
3843
3396
|
ignoreInitial: true,
|
|
3844
3397
|
persistent: true,
|
|
3845
3398
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|