@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.cjs
CHANGED
|
@@ -68,112 +68,9 @@ var CodegenError = class extends Error {
|
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
// src/adapters/zod.ts
|
|
72
|
-
function toObjectKey(name) {
|
|
73
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
74
|
-
}
|
|
75
|
-
function messageSuffix(messageRaw2) {
|
|
76
|
-
return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
|
|
77
|
-
}
|
|
78
|
-
function renderStringChecks(checks) {
|
|
79
|
-
let out = "";
|
|
80
|
-
for (const c of checks) {
|
|
81
|
-
switch (c.check) {
|
|
82
|
-
case "email":
|
|
83
|
-
out += `.email(${messageSuffix(c.messageRaw)})`;
|
|
84
|
-
break;
|
|
85
|
-
case "url":
|
|
86
|
-
out += `.url(${messageSuffix(c.messageRaw)})`;
|
|
87
|
-
break;
|
|
88
|
-
case "uuid":
|
|
89
|
-
out += `.uuid(${messageSuffix(c.messageRaw)})`;
|
|
90
|
-
break;
|
|
91
|
-
case "regex":
|
|
92
|
-
out += `.regex(${c.pattern})`;
|
|
93
|
-
break;
|
|
94
|
-
case "min":
|
|
95
|
-
out += `.min(${c.value})`;
|
|
96
|
-
break;
|
|
97
|
-
case "max":
|
|
98
|
-
out += `.max(${c.value})`;
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
function render(node, ctx) {
|
|
105
|
-
switch (node.kind) {
|
|
106
|
-
case "string":
|
|
107
|
-
return `z.string()${renderStringChecks(node.checks)}`;
|
|
108
|
-
case "number": {
|
|
109
|
-
let out = "z.number()";
|
|
110
|
-
for (const c of node.checks) {
|
|
111
|
-
if (c.check === "int") out += ".int()";
|
|
112
|
-
else if (c.check === "positive") out += ".positive()";
|
|
113
|
-
else if (c.check === "negative") out += ".negative()";
|
|
114
|
-
else if (c.check === "min") out += `.min(${c.value})`;
|
|
115
|
-
else if (c.check === "max") out += `.max(${c.value})`;
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
case "boolean":
|
|
120
|
-
return "z.boolean()";
|
|
121
|
-
case "date":
|
|
122
|
-
return "z.coerce.date()";
|
|
123
|
-
case "unknown":
|
|
124
|
-
return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
|
|
125
|
-
case "instanceof":
|
|
126
|
-
return `z.instanceof(${node.ctor})`;
|
|
127
|
-
case "enum":
|
|
128
|
-
return `z.enum([${node.literals.join(", ")}])`;
|
|
129
|
-
case "literal":
|
|
130
|
-
return `z.literal(${node.raw})`;
|
|
131
|
-
case "union":
|
|
132
|
-
return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
|
|
133
|
-
case "object": {
|
|
134
|
-
if (node.fields.length === 0) {
|
|
135
|
-
return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
|
|
136
|
-
}
|
|
137
|
-
const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
|
|
138
|
-
return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
|
|
139
|
-
}
|
|
140
|
-
case "array":
|
|
141
|
-
return `z.array(${render(node.element, ctx)})`;
|
|
142
|
-
case "optional":
|
|
143
|
-
return `${render(node.inner, ctx)}.optional()`;
|
|
144
|
-
case "ref":
|
|
145
|
-
return node.name;
|
|
146
|
-
case "lazyRef":
|
|
147
|
-
return `z.lazy(() => ${node.name})`;
|
|
148
|
-
case "annotated": {
|
|
149
|
-
const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
|
|
150
|
-
return `${render(node.inner, ctx)} ${comments}`;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
var zodAdapter = {
|
|
155
|
-
name: "zod",
|
|
156
|
-
importStatements(usage) {
|
|
157
|
-
return usage.used ? ["import { z } from 'zod';"] : [];
|
|
158
|
-
},
|
|
159
|
-
render,
|
|
160
|
-
inferType(schemaConst) {
|
|
161
|
-
return `z.infer<typeof ${schemaConst}>`;
|
|
162
|
-
},
|
|
163
|
-
renderModule(mod) {
|
|
164
|
-
const ctx = { named: mod.named };
|
|
165
|
-
const namedNestedSchemas = /* @__PURE__ */ new Map();
|
|
166
|
-
for (const [name, node] of mod.named) {
|
|
167
|
-
namedNestedSchemas.set(name, render(node, ctx));
|
|
168
|
-
}
|
|
169
|
-
return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
71
|
// src/adapters/registry.ts
|
|
174
72
|
function resolveAdapter(option) {
|
|
175
73
|
if (typeof option !== "string") return option;
|
|
176
|
-
if (option === "zod") return zodAdapter;
|
|
177
74
|
const pkg = `@dudousxd/nestjs-codegen-${option}`;
|
|
178
75
|
const named = `${option}Adapter`;
|
|
179
76
|
throw new ConfigError(
|
|
@@ -203,8 +100,21 @@ If this is intentional, move the file inside your project directory.`
|
|
|
203
100
|
function resolveConfig(userConfig, cwd) {
|
|
204
101
|
return applyDefaults(userConfig, cwd ?? process.cwd());
|
|
205
102
|
}
|
|
103
|
+
function validateUserConfig(userConfig) {
|
|
104
|
+
if (userConfig.validation == null) {
|
|
105
|
+
throw new ConfigError(
|
|
106
|
+
"validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
|
|
110
|
+
throw new ConfigError(
|
|
111
|
+
"Config validation failed: `pages.glob` must be a string when `pages` is set"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
206
115
|
function applyDefaults(userConfig, cwd) {
|
|
207
|
-
|
|
116
|
+
validateUserConfig(userConfig);
|
|
117
|
+
const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-codegen");
|
|
208
118
|
const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
|
|
209
119
|
let app = null;
|
|
210
120
|
if (userConfig.app) {
|
|
@@ -222,7 +132,8 @@ function applyDefaults(userConfig, cwd) {
|
|
|
222
132
|
}
|
|
223
133
|
return {
|
|
224
134
|
extensions: userConfig.extensions ?? [],
|
|
225
|
-
|
|
135
|
+
// Non-null: validateUserConfig() above throws when `validation` is absent.
|
|
136
|
+
validation: resolveAdapter(userConfig.validation),
|
|
226
137
|
pages: userConfig.pages ? {
|
|
227
138
|
glob: userConfig.pages.glob,
|
|
228
139
|
propsExport: userConfig.pages.propsExport ?? "ComponentProps",
|
|
@@ -249,13 +160,16 @@ function applyDefaults(userConfig, cwd) {
|
|
|
249
160
|
|
|
250
161
|
// src/watch/watcher.ts
|
|
251
162
|
var import_promises12 = require("fs/promises");
|
|
252
|
-
var
|
|
163
|
+
var import_node_path14 = require("path");
|
|
253
164
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
254
165
|
|
|
255
166
|
// src/discovery/contracts-fast.ts
|
|
256
167
|
var import_node_path3 = require("path");
|
|
257
168
|
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
258
|
-
var
|
|
169
|
+
var import_ts_morph7 = require("ts-morph");
|
|
170
|
+
|
|
171
|
+
// src/discovery/dto-type-resolver.ts
|
|
172
|
+
var import_ts_morph5 = require("ts-morph");
|
|
259
173
|
|
|
260
174
|
// src/discovery/dto-to-ir.ts
|
|
261
175
|
var import_ts_morph2 = require("ts-morph");
|
|
@@ -264,20 +178,13 @@ var import_ts_morph2 = require("ts-morph");
|
|
|
264
178
|
var import_node_fs = require("fs");
|
|
265
179
|
var import_node_path2 = require("path");
|
|
266
180
|
var import_ts_morph = require("ts-morph");
|
|
267
|
-
var
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return prev;
|
|
181
|
+
var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
|
|
182
|
+
var _ctxByProject = /* @__PURE__ */ new WeakMap();
|
|
183
|
+
function setDiscoveryContext(project, ctx) {
|
|
184
|
+
_ctxByProject.set(project, ctx);
|
|
272
185
|
}
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
function _projectRoot() {
|
|
277
|
-
return _ctx.projectRoot;
|
|
278
|
-
}
|
|
279
|
-
function _tsconfigPaths() {
|
|
280
|
-
return _ctx.tsconfigPaths;
|
|
186
|
+
function _ctxFor(project) {
|
|
187
|
+
return _ctxByProject.get(project) ?? _EMPTY_CTX;
|
|
281
188
|
}
|
|
282
189
|
var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
|
|
283
190
|
function dbg(...args) {
|
|
@@ -319,7 +226,7 @@ function findTypeInFile(name, file) {
|
|
|
319
226
|
}
|
|
320
227
|
return null;
|
|
321
228
|
}
|
|
322
|
-
function resolveModuleSpecifier(moduleSpecifier, sourceFile,
|
|
229
|
+
function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
323
230
|
if (moduleSpecifier.startsWith(".")) {
|
|
324
231
|
const dir = (0, import_node_path2.dirname)(sourceFile.getFilePath());
|
|
325
232
|
const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
|
|
@@ -329,8 +236,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
|
|
|
329
236
|
(0, import_node_path2.resolve)(dir, moduleSpecifier, "index.ts")
|
|
330
237
|
];
|
|
331
238
|
}
|
|
332
|
-
const
|
|
333
|
-
const
|
|
239
|
+
const ctx = _ctxFor(project);
|
|
240
|
+
const baseUrl = ctx.projectRoot;
|
|
241
|
+
const tsconfigPaths = ctx.tsconfigPaths;
|
|
334
242
|
dbg(
|
|
335
243
|
"resolveModuleSpecifier",
|
|
336
244
|
moduleSpecifier,
|
|
@@ -476,7 +384,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
476
384
|
if (!namedImport) continue;
|
|
477
385
|
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
478
386
|
if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
479
|
-
const tsconfigPaths =
|
|
387
|
+
const tsconfigPaths = _ctxFor(project).tsconfigPaths;
|
|
480
388
|
const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
|
|
481
389
|
const prefix = p.replace("*", "");
|
|
482
390
|
return moduleSpecifier.startsWith(prefix);
|
|
@@ -829,339 +737,11 @@ function inSchemaFromDecorator(decorator) {
|
|
|
829
737
|
return null;
|
|
830
738
|
}
|
|
831
739
|
|
|
832
|
-
// src/discovery/dto-to-zod.ts
|
|
833
|
-
var import_ts_morph3 = require("ts-morph");
|
|
834
|
-
var KNOWN_DECORATORS2 = /* @__PURE__ */ new Set([
|
|
835
|
-
"IsString",
|
|
836
|
-
"IsNumber",
|
|
837
|
-
"IsInt",
|
|
838
|
-
"IsBoolean",
|
|
839
|
-
"IsDate",
|
|
840
|
-
"IsEmail",
|
|
841
|
-
"IsUrl",
|
|
842
|
-
"IsUUID",
|
|
843
|
-
"MinLength",
|
|
844
|
-
"MaxLength",
|
|
845
|
-
"Length",
|
|
846
|
-
"Min",
|
|
847
|
-
"Max",
|
|
848
|
-
"IsPositive",
|
|
849
|
-
"IsNegative",
|
|
850
|
-
"Matches",
|
|
851
|
-
"IsEnum",
|
|
852
|
-
"IsIn",
|
|
853
|
-
"IsOptional",
|
|
854
|
-
"IsNotEmpty",
|
|
855
|
-
"IsArray",
|
|
856
|
-
"ValidateNested",
|
|
857
|
-
"Type",
|
|
858
|
-
"IsObject",
|
|
859
|
-
"Allow",
|
|
860
|
-
"IsDefined"
|
|
861
|
-
]);
|
|
862
|
-
function extractZodFromDto(classDecl, sourceFile, project) {
|
|
863
|
-
const ctx = {
|
|
864
|
-
sourceFile,
|
|
865
|
-
project,
|
|
866
|
-
namedNestedSchemas: /* @__PURE__ */ new Map(),
|
|
867
|
-
warnings: [],
|
|
868
|
-
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
869
|
-
emittedClasses: /* @__PURE__ */ new Map(),
|
|
870
|
-
visiting: /* @__PURE__ */ new Set(),
|
|
871
|
-
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
872
|
-
depth: 0
|
|
873
|
-
};
|
|
874
|
-
const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
|
|
875
|
-
for (const schemaName of ctx.recursiveSchemas) {
|
|
876
|
-
ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
|
|
877
|
-
}
|
|
878
|
-
return {
|
|
879
|
-
schemaText,
|
|
880
|
-
namedNestedSchemas: ctx.namedNestedSchemas,
|
|
881
|
-
warnings: ctx.warnings
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
function buildObjectSchema(classDecl, classFile, ctx) {
|
|
885
|
-
const props = classDecl.getProperties();
|
|
886
|
-
if (props.length === 0) {
|
|
887
|
-
return "z.object({}).passthrough()";
|
|
888
|
-
}
|
|
889
|
-
const fields = [];
|
|
890
|
-
for (const prop of props) {
|
|
891
|
-
const name = prop.getName();
|
|
892
|
-
const expr = buildPropertySchema(prop, classFile, ctx);
|
|
893
|
-
fields.push(`${toObjectKey2(name)}: ${expr}`);
|
|
894
|
-
}
|
|
895
|
-
return `z.object({ ${fields.join(", ")} })`;
|
|
896
|
-
}
|
|
897
|
-
function toObjectKey2(name) {
|
|
898
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
899
|
-
}
|
|
900
|
-
function buildPropertySchema(prop, classFile, ctx) {
|
|
901
|
-
const decorators = /* @__PURE__ */ new Map();
|
|
902
|
-
for (const d of prop.getDecorators()) decorators.set(d.getName(), d);
|
|
903
|
-
const has = (n) => decorators.has(n);
|
|
904
|
-
const dec = (n) => decorators.get(n);
|
|
905
|
-
const typeNode = prop.getTypeNode();
|
|
906
|
-
const typeText = typeNode?.getText() ?? "unknown";
|
|
907
|
-
const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
|
|
908
|
-
const comments = [];
|
|
909
|
-
const typeRefName = resolveTypeFactoryName2(dec("Type"));
|
|
910
|
-
if (has("ValidateNested") || typeRefName) {
|
|
911
|
-
const childName = typeRefName ?? singularClassName2(typeText);
|
|
912
|
-
if (childName) {
|
|
913
|
-
const childExpr = buildNestedReference2(childName, classFile, ctx);
|
|
914
|
-
const wrapArray = has("IsArray") || isArrayType;
|
|
915
|
-
let expr2 = wrapArray ? `z.array(${childExpr})` : childExpr;
|
|
916
|
-
expr2 = applyPresence2(expr2, decorators);
|
|
917
|
-
return expr2;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
let base = baseFromType2(typeText, isArrayType, ctx, classFile);
|
|
921
|
-
const refinements = [];
|
|
922
|
-
if (has("IsString")) base = "z.string()";
|
|
923
|
-
if (has("IsBoolean")) base = "z.boolean()";
|
|
924
|
-
if (has("IsDate")) base = "z.coerce.date()";
|
|
925
|
-
if (has("IsNumber")) base = "z.number()";
|
|
926
|
-
if (has("IsInt")) base = "z.number().int()";
|
|
927
|
-
if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
|
|
928
|
-
if (has("Allow")) base = "z.unknown()";
|
|
929
|
-
if (has("IsEmail")) {
|
|
930
|
-
base = ensureStringBase(base);
|
|
931
|
-
refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
|
|
932
|
-
}
|
|
933
|
-
if (has("IsUrl")) {
|
|
934
|
-
base = ensureStringBase(base);
|
|
935
|
-
refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
|
|
936
|
-
}
|
|
937
|
-
if (has("IsUUID")) {
|
|
938
|
-
base = ensureStringBase(base);
|
|
939
|
-
refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
|
|
940
|
-
}
|
|
941
|
-
if (has("Matches")) {
|
|
942
|
-
const re = firstArgText2(dec("Matches"));
|
|
943
|
-
if (re) {
|
|
944
|
-
base = ensureStringBase(base);
|
|
945
|
-
refinements.push(`.regex(${re})`);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
if (has("MinLength")) {
|
|
949
|
-
const n = numericArg2(dec("MinLength"));
|
|
950
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
951
|
-
}
|
|
952
|
-
if (has("MaxLength")) {
|
|
953
|
-
const n = numericArg2(dec("MaxLength"));
|
|
954
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
955
|
-
}
|
|
956
|
-
if (has("Length")) {
|
|
957
|
-
const [min, max] = numericArgs2(dec("Length"));
|
|
958
|
-
if (min !== null) refinements.push(`.min(${min})`);
|
|
959
|
-
if (max !== null) refinements.push(`.max(${max})`);
|
|
960
|
-
}
|
|
961
|
-
if (has("Min")) {
|
|
962
|
-
const n = numericArg2(dec("Min"));
|
|
963
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
964
|
-
}
|
|
965
|
-
if (has("Max")) {
|
|
966
|
-
const n = numericArg2(dec("Max"));
|
|
967
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
968
|
-
}
|
|
969
|
-
if (has("IsPositive")) refinements.push(".positive()");
|
|
970
|
-
if (has("IsNegative")) refinements.push(".negative()");
|
|
971
|
-
if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
|
|
972
|
-
if (has("IsEnum")) {
|
|
973
|
-
const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
|
|
974
|
-
if (enumExpr) base = enumExpr;
|
|
975
|
-
}
|
|
976
|
-
if (has("IsIn")) {
|
|
977
|
-
const inExpr = inSchemaFromDecorator2(dec("IsIn"));
|
|
978
|
-
if (inExpr) base = inExpr;
|
|
979
|
-
}
|
|
980
|
-
for (const name of decorators.keys()) {
|
|
981
|
-
if (!KNOWN_DECORATORS2.has(name)) {
|
|
982
|
-
comments.push(`/* @${name}: not translatable to zod (server-only) */`);
|
|
983
|
-
if (!ctx.warnedDecorators.has(name)) {
|
|
984
|
-
ctx.warnedDecorators.add(name);
|
|
985
|
-
const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
|
|
986
|
-
ctx.warnings.push(msg);
|
|
987
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
let expr = base + refinements.join("");
|
|
992
|
-
if (isArrayType && !expr.startsWith("z.array(")) {
|
|
993
|
-
expr = `z.array(${expr})`;
|
|
994
|
-
}
|
|
995
|
-
expr = applyPresence2(expr, decorators);
|
|
996
|
-
if (comments.length > 0) {
|
|
997
|
-
expr = `${expr} ${comments.join(" ")}`;
|
|
998
|
-
}
|
|
999
|
-
return expr;
|
|
1000
|
-
}
|
|
1001
|
-
function applyPresence2(expr, decorators) {
|
|
1002
|
-
if (decorators.has("IsDefined")) return expr;
|
|
1003
|
-
if (decorators.has("IsOptional")) return `${expr}.optional()`;
|
|
1004
|
-
return expr;
|
|
1005
|
-
}
|
|
1006
|
-
function baseFromType2(typeText, isArrayType, ctx, classFile) {
|
|
1007
|
-
const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
|
|
1008
|
-
switch (inner) {
|
|
1009
|
-
case "string":
|
|
1010
|
-
return "z.string()";
|
|
1011
|
-
case "number":
|
|
1012
|
-
return "z.number()";
|
|
1013
|
-
case "boolean":
|
|
1014
|
-
return "z.boolean()";
|
|
1015
|
-
case "Date":
|
|
1016
|
-
return "z.coerce.date()";
|
|
1017
|
-
case "File":
|
|
1018
|
-
case "Express.Multer.File":
|
|
1019
|
-
return "z.instanceof(File)";
|
|
1020
|
-
default:
|
|
1021
|
-
return "z.unknown()";
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
function ensureStringBase(base) {
|
|
1025
|
-
return isStringBase(base) ? base : "z.string()";
|
|
1026
|
-
}
|
|
1027
|
-
function isStringBase(base) {
|
|
1028
|
-
return base.startsWith("z.string(");
|
|
1029
|
-
}
|
|
1030
|
-
function buildNestedReference2(className, fromFile, ctx) {
|
|
1031
|
-
if (ctx.visiting.has(className) || ctx.depth >= 8) {
|
|
1032
|
-
const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
|
|
1033
|
-
ctx.emittedClasses.set(className, reserved);
|
|
1034
|
-
ctx.recursiveSchemas.add(reserved);
|
|
1035
|
-
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
1036
|
-
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
1037
|
-
const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
|
|
1038
|
-
ctx.warnings.push(msg);
|
|
1039
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
1040
|
-
}
|
|
1041
|
-
return `z.lazy(() => ${reserved})`;
|
|
1042
|
-
}
|
|
1043
|
-
const existing = ctx.emittedClasses.get(className);
|
|
1044
|
-
if (existing) return existing;
|
|
1045
|
-
const schemaName = aliasFor2(className, ctx);
|
|
1046
|
-
const resolved = findType(className, fromFile, ctx.project);
|
|
1047
|
-
if (!resolved || resolved.kind !== "class") {
|
|
1048
|
-
return "z.object({}).passthrough()";
|
|
1049
|
-
}
|
|
1050
|
-
ctx.emittedClasses.set(className, schemaName);
|
|
1051
|
-
ctx.visiting.add(className);
|
|
1052
|
-
ctx.depth += 1;
|
|
1053
|
-
const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
|
|
1054
|
-
ctx.depth -= 1;
|
|
1055
|
-
ctx.visiting.delete(className);
|
|
1056
|
-
ctx.namedNestedSchemas.set(schemaName, childText);
|
|
1057
|
-
return schemaName;
|
|
1058
|
-
}
|
|
1059
|
-
function aliasFor2(className, ctx) {
|
|
1060
|
-
const baseName = `${className}Schema`;
|
|
1061
|
-
let candidate = baseName;
|
|
1062
|
-
let i = 1;
|
|
1063
|
-
const used = new Set(ctx.namedNestedSchemas.keys());
|
|
1064
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
1065
|
-
while (used.has(candidate)) {
|
|
1066
|
-
candidate = `${baseName}_${i}`;
|
|
1067
|
-
i += 1;
|
|
1068
|
-
}
|
|
1069
|
-
return candidate;
|
|
1070
|
-
}
|
|
1071
|
-
function firstArg2(decorator) {
|
|
1072
|
-
return decorator?.getArguments()[0];
|
|
1073
|
-
}
|
|
1074
|
-
function firstArgText2(decorator) {
|
|
1075
|
-
const arg = firstArg2(decorator);
|
|
1076
|
-
return arg ? arg.getText() : null;
|
|
1077
|
-
}
|
|
1078
|
-
function numericArg2(decorator) {
|
|
1079
|
-
const arg = firstArg2(decorator);
|
|
1080
|
-
if (arg && import_ts_morph3.Node.isNumericLiteral(arg)) return arg.getText();
|
|
1081
|
-
return null;
|
|
1082
|
-
}
|
|
1083
|
-
function numericArgs2(decorator) {
|
|
1084
|
-
const args = decorator?.getArguments() ?? [];
|
|
1085
|
-
const num = (n) => n && import_ts_morph3.Node.isNumericLiteral(n) ? n.getText() : null;
|
|
1086
|
-
return [num(args[0]), num(args[1])];
|
|
1087
|
-
}
|
|
1088
|
-
function messageArg(decorator) {
|
|
1089
|
-
const args = decorator?.getArguments() ?? [];
|
|
1090
|
-
for (const arg of args) {
|
|
1091
|
-
if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
|
|
1092
|
-
for (const prop of arg.getProperties()) {
|
|
1093
|
-
if (import_ts_morph3.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
|
|
1094
|
-
const init = prop.getInitializer();
|
|
1095
|
-
if (init && import_ts_morph3.Node.isStringLiteral(init)) {
|
|
1096
|
-
return `{ message: ${init.getText()} }`;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
return "";
|
|
1103
|
-
}
|
|
1104
|
-
function resolveTypeFactoryName2(decorator) {
|
|
1105
|
-
const arg = firstArg2(decorator);
|
|
1106
|
-
if (!arg) return null;
|
|
1107
|
-
if (import_ts_morph3.Node.isArrowFunction(arg)) {
|
|
1108
|
-
const body = arg.getBody();
|
|
1109
|
-
if (import_ts_morph3.Node.isIdentifier(body)) return body.getText();
|
|
1110
|
-
}
|
|
1111
|
-
return null;
|
|
1112
|
-
}
|
|
1113
|
-
function singularClassName2(typeText) {
|
|
1114
|
-
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
1115
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
1116
|
-
}
|
|
1117
|
-
function enumSchemaFromDecorator2(decorator, classFile, ctx) {
|
|
1118
|
-
const arg = firstArg2(decorator);
|
|
1119
|
-
if (!arg) return null;
|
|
1120
|
-
if (import_ts_morph3.Node.isIdentifier(arg)) {
|
|
1121
|
-
const name = arg.getText();
|
|
1122
|
-
const resolved = findType(name, classFile, ctx.project);
|
|
1123
|
-
if (resolved && resolved.kind === "enum") {
|
|
1124
|
-
return `z.enum([${resolved.members.join(", ")}])`;
|
|
1125
|
-
}
|
|
1126
|
-
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().`;
|
|
1127
|
-
if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
|
|
1128
|
-
ctx.warnedDecorators.add(`IsEnum:${name}`);
|
|
1129
|
-
ctx.warnings.push(msg);
|
|
1130
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
1131
|
-
}
|
|
1132
|
-
return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
|
|
1133
|
-
}
|
|
1134
|
-
if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
|
|
1135
|
-
const values = [];
|
|
1136
|
-
for (const p of arg.getProperties()) {
|
|
1137
|
-
if (!import_ts_morph3.Node.isPropertyAssignment(p)) continue;
|
|
1138
|
-
const init = p.getInitializer();
|
|
1139
|
-
if (init && import_ts_morph3.Node.isStringLiteral(init)) values.push(init.getText());
|
|
1140
|
-
}
|
|
1141
|
-
if (values.length > 0) return `z.enum([${values.join(", ")}])`;
|
|
1142
|
-
}
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
function inSchemaFromDecorator2(decorator) {
|
|
1146
|
-
const arg = firstArg2(decorator);
|
|
1147
|
-
if (arg && import_ts_morph3.Node.isArrayLiteralExpression(arg)) {
|
|
1148
|
-
const elements = arg.getElements();
|
|
1149
|
-
const allStrings = elements.every((e) => import_ts_morph3.Node.isStringLiteral(e));
|
|
1150
|
-
if (allStrings && elements.length > 0) {
|
|
1151
|
-
return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
|
|
1152
|
-
}
|
|
1153
|
-
if (elements.length > 0) {
|
|
1154
|
-
return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
return null;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
740
|
// src/discovery/filter-for.ts
|
|
1161
|
-
var
|
|
741
|
+
var import_ts_morph4 = require("ts-morph");
|
|
1162
742
|
|
|
1163
743
|
// src/discovery/filter-field-types.ts
|
|
1164
|
-
var
|
|
744
|
+
var import_ts_morph3 = require("ts-morph");
|
|
1165
745
|
|
|
1166
746
|
// src/discovery/enum-resolution.ts
|
|
1167
747
|
function resolveEnumValues(name, sourceFile, project) {
|
|
@@ -1196,28 +776,28 @@ function markNullable(r, nullable) {
|
|
|
1196
776
|
return nullable ? { ...r, nullable: true } : r;
|
|
1197
777
|
}
|
|
1198
778
|
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
1199
|
-
if (
|
|
779
|
+
if (import_ts_morph3.Node.isUnionTypeNode(typeNode)) {
|
|
1200
780
|
let nullable = false;
|
|
1201
781
|
const stringLits = [];
|
|
1202
782
|
const numberLits = [];
|
|
1203
783
|
const others = [];
|
|
1204
784
|
for (const member of typeNode.getTypeNodes()) {
|
|
1205
785
|
const kind = member.getKind();
|
|
1206
|
-
if (kind ===
|
|
786
|
+
if (kind === import_ts_morph3.SyntaxKind.NullKeyword || kind === import_ts_morph3.SyntaxKind.UndefinedKeyword) {
|
|
1207
787
|
nullable = true;
|
|
1208
788
|
continue;
|
|
1209
789
|
}
|
|
1210
|
-
if (
|
|
790
|
+
if (import_ts_morph3.Node.isLiteralTypeNode(member)) {
|
|
1211
791
|
const lit = member.getLiteral();
|
|
1212
|
-
if (
|
|
792
|
+
if (import_ts_morph3.Node.isStringLiteral(lit)) {
|
|
1213
793
|
stringLits.push(lit.getLiteralValue());
|
|
1214
794
|
continue;
|
|
1215
795
|
}
|
|
1216
|
-
if (
|
|
796
|
+
if (import_ts_morph3.Node.isNumericLiteral(lit)) {
|
|
1217
797
|
numberLits.push(lit.getText());
|
|
1218
798
|
continue;
|
|
1219
799
|
}
|
|
1220
|
-
if (lit.getKind() ===
|
|
800
|
+
if (lit.getKind() === import_ts_morph3.SyntaxKind.NullKeyword) {
|
|
1221
801
|
nullable = true;
|
|
1222
802
|
continue;
|
|
1223
803
|
}
|
|
@@ -1237,19 +817,19 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
1237
817
|
return markNullable({ kind: "unknown" }, nullable);
|
|
1238
818
|
}
|
|
1239
819
|
switch (typeNode.getKind()) {
|
|
1240
|
-
case
|
|
820
|
+
case import_ts_morph3.SyntaxKind.StringKeyword:
|
|
1241
821
|
return { kind: "string" };
|
|
1242
|
-
case
|
|
822
|
+
case import_ts_morph3.SyntaxKind.NumberKeyword:
|
|
1243
823
|
return { kind: "number" };
|
|
1244
|
-
case
|
|
824
|
+
case import_ts_morph3.SyntaxKind.BooleanKeyword:
|
|
1245
825
|
return { kind: "boolean" };
|
|
1246
|
-
case
|
|
1247
|
-
case
|
|
826
|
+
case import_ts_morph3.SyntaxKind.AnyKeyword:
|
|
827
|
+
case import_ts_morph3.SyntaxKind.UnknownKeyword:
|
|
1248
828
|
return { kind: "unknown" };
|
|
1249
829
|
default:
|
|
1250
830
|
break;
|
|
1251
831
|
}
|
|
1252
|
-
if (
|
|
832
|
+
if (import_ts_morph3.Node.isTypeReference(typeNode)) {
|
|
1253
833
|
const refName = typeNode.getTypeName().getText();
|
|
1254
834
|
if (refName === "Date") return { kind: "date" };
|
|
1255
835
|
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
@@ -1262,25 +842,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
1262
842
|
if (typeRef) return { kind: "unknown", typeRef };
|
|
1263
843
|
return { kind: "unknown" };
|
|
1264
844
|
}
|
|
1265
|
-
if (
|
|
845
|
+
if (import_ts_morph3.Node.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
1266
846
|
return { kind: "unknown" };
|
|
1267
847
|
}
|
|
1268
848
|
function enumFromDecoratorArgs(args, sourceFile, project) {
|
|
1269
849
|
for (const arg of args) {
|
|
1270
|
-
if (
|
|
850
|
+
if (import_ts_morph3.Node.isArrowFunction(arg)) {
|
|
1271
851
|
const body = arg.getBody();
|
|
1272
|
-
if (
|
|
852
|
+
if (import_ts_morph3.Node.isIdentifier(body)) {
|
|
1273
853
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
1274
854
|
if (en) return en;
|
|
1275
855
|
}
|
|
1276
856
|
}
|
|
1277
|
-
if (
|
|
857
|
+
if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
|
|
1278
858
|
const itemsProp = arg.getProperty("items");
|
|
1279
|
-
if (itemsProp &&
|
|
859
|
+
if (itemsProp && import_ts_morph3.Node.isPropertyAssignment(itemsProp)) {
|
|
1280
860
|
const init = itemsProp.getInitializer();
|
|
1281
|
-
if (init &&
|
|
861
|
+
if (init && import_ts_morph3.Node.isArrowFunction(init)) {
|
|
1282
862
|
const body = init.getBody();
|
|
1283
|
-
if (
|
|
863
|
+
if (import_ts_morph3.Node.isIdentifier(body)) {
|
|
1284
864
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
1285
865
|
if (en) return en;
|
|
1286
866
|
}
|
|
@@ -1303,7 +883,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1303
883
|
return { kind: "string" };
|
|
1304
884
|
}
|
|
1305
885
|
for (const arg of args) {
|
|
1306
|
-
if (
|
|
886
|
+
if (import_ts_morph3.Node.isStringLiteral(arg)) {
|
|
1307
887
|
const raw = arg.getLiteralValue();
|
|
1308
888
|
const kind = classifyTypeKeyword(raw);
|
|
1309
889
|
if (kind) {
|
|
@@ -1311,11 +891,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1311
891
|
return { kind };
|
|
1312
892
|
}
|
|
1313
893
|
}
|
|
1314
|
-
if (
|
|
894
|
+
if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
|
|
1315
895
|
const enumProp = arg.getProperty("enum");
|
|
1316
|
-
if (enumProp &&
|
|
896
|
+
if (enumProp && import_ts_morph3.Node.isPropertyAssignment(enumProp)) {
|
|
1317
897
|
const init = enumProp.getInitializer();
|
|
1318
|
-
if (init &&
|
|
898
|
+
if (init && import_ts_morph3.Node.isIdentifier(init)) {
|
|
1319
899
|
const en = resolveEnumValues(init.getText(), sourceFile, project);
|
|
1320
900
|
if (en) {
|
|
1321
901
|
return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
@@ -1324,9 +904,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
1324
904
|
}
|
|
1325
905
|
}
|
|
1326
906
|
const typeProp = arg.getProperty("type");
|
|
1327
|
-
if (typeProp &&
|
|
907
|
+
if (typeProp && import_ts_morph3.Node.isPropertyAssignment(typeProp)) {
|
|
1328
908
|
const init = typeProp.getInitializer();
|
|
1329
|
-
if (init &&
|
|
909
|
+
if (init && import_ts_morph3.Node.isStringLiteral(init)) {
|
|
1330
910
|
const kind = classifyTypeKeyword(init.getLiteralValue());
|
|
1331
911
|
if (kind) return { kind };
|
|
1332
912
|
}
|
|
@@ -1362,7 +942,7 @@ function toFilterFieldType(name, r) {
|
|
|
1362
942
|
|
|
1363
943
|
// src/discovery/filter-for.ts
|
|
1364
944
|
function classifyFilterForHint(typeInit) {
|
|
1365
|
-
if (
|
|
945
|
+
if (import_ts_morph4.Node.isStringLiteral(typeInit)) {
|
|
1366
946
|
switch (typeInit.getLiteralValue()) {
|
|
1367
947
|
case "string":
|
|
1368
948
|
return { kind: "string" };
|
|
@@ -1376,10 +956,10 @@ function classifyFilterForHint(typeInit) {
|
|
|
1376
956
|
return null;
|
|
1377
957
|
}
|
|
1378
958
|
}
|
|
1379
|
-
if (
|
|
959
|
+
if (import_ts_morph4.Node.isArrayLiteralExpression(typeInit)) {
|
|
1380
960
|
const values = [];
|
|
1381
961
|
for (const el of typeInit.getElements()) {
|
|
1382
|
-
if (!
|
|
962
|
+
if (!import_ts_morph4.Node.isStringLiteral(el)) return null;
|
|
1383
963
|
values.push(el.getLiteralValue());
|
|
1384
964
|
}
|
|
1385
965
|
if (values.length === 0) return null;
|
|
@@ -1414,11 +994,11 @@ function extractFilterForHints(classDecl, project) {
|
|
|
1414
994
|
if (!filterForDec) continue;
|
|
1415
995
|
const args = filterForDec.getArguments();
|
|
1416
996
|
const keyArg = args[0];
|
|
1417
|
-
const inputKey = keyArg &&
|
|
997
|
+
const inputKey = keyArg && import_ts_morph4.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
|
|
1418
998
|
const optsArg = args[1];
|
|
1419
|
-
if (optsArg &&
|
|
999
|
+
if (optsArg && import_ts_morph4.Node.isObjectLiteralExpression(optsArg)) {
|
|
1420
1000
|
const typeProp = optsArg.getProperty("type");
|
|
1421
|
-
if (typeProp &&
|
|
1001
|
+
if (typeProp && import_ts_morph4.Node.isPropertyAssignment(typeProp)) {
|
|
1422
1002
|
const typeInit = typeProp.getInitializer();
|
|
1423
1003
|
if (typeInit) {
|
|
1424
1004
|
const classified = classifyFilterForHint(typeInit);
|
|
@@ -1441,14 +1021,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
|
|
|
1441
1021
|
const args = filterDecorator.getArguments();
|
|
1442
1022
|
if (args.length === 0) continue;
|
|
1443
1023
|
const filterClassArg = args[0];
|
|
1444
|
-
if (!filterClassArg || !
|
|
1024
|
+
if (!filterClassArg || !import_ts_morph4.Node.isIdentifier(filterClassArg)) continue;
|
|
1445
1025
|
let source = "query";
|
|
1446
1026
|
const optionsArg = args[1];
|
|
1447
|
-
if (optionsArg &&
|
|
1027
|
+
if (optionsArg && import_ts_morph4.Node.isObjectLiteralExpression(optionsArg)) {
|
|
1448
1028
|
const sourceProp = optionsArg.getProperty("source");
|
|
1449
|
-
if (sourceProp &&
|
|
1029
|
+
if (sourceProp && import_ts_morph4.Node.isPropertyAssignment(sourceProp)) {
|
|
1450
1030
|
const init = sourceProp.getInitializer();
|
|
1451
|
-
if (init &&
|
|
1031
|
+
if (init && import_ts_morph4.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
|
|
1452
1032
|
source = "body";
|
|
1453
1033
|
}
|
|
1454
1034
|
}
|
|
@@ -1511,22 +1091,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
|
|
|
1511
1091
|
const args = dec.getArguments();
|
|
1512
1092
|
if (args.length === 0) continue;
|
|
1513
1093
|
const arg = args[0];
|
|
1514
|
-
if (
|
|
1094
|
+
if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
|
|
1515
1095
|
const entityProp = arg.getProperty("entity");
|
|
1516
|
-
if (entityProp &&
|
|
1096
|
+
if (entityProp && import_ts_morph4.Node.isPropertyAssignment(entityProp)) {
|
|
1517
1097
|
const init = entityProp.getInitializer();
|
|
1518
|
-
if (init &&
|
|
1098
|
+
if (init && import_ts_morph4.Node.isArrowFunction(init)) {
|
|
1519
1099
|
const body = init.getBody();
|
|
1520
|
-
if (
|
|
1100
|
+
if (import_ts_morph4.Node.isIdentifier(body)) {
|
|
1521
1101
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
1522
1102
|
if (resolved?.kind === "class") return resolved.decl;
|
|
1523
1103
|
}
|
|
1524
1104
|
}
|
|
1525
1105
|
}
|
|
1526
1106
|
}
|
|
1527
|
-
if (
|
|
1107
|
+
if (import_ts_morph4.Node.isArrowFunction(arg)) {
|
|
1528
1108
|
const body = arg.getBody();
|
|
1529
|
-
if (
|
|
1109
|
+
if (import_ts_morph4.Node.isIdentifier(body)) {
|
|
1530
1110
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
1531
1111
|
if (resolved?.kind === "class") return resolved.decl;
|
|
1532
1112
|
}
|
|
@@ -1550,11 +1130,11 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1550
1130
|
const args = filterableDecorator.getArguments();
|
|
1551
1131
|
if (args.length === 0) return [];
|
|
1552
1132
|
const optionsArg = args[0];
|
|
1553
|
-
if (!
|
|
1133
|
+
if (!import_ts_morph4.Node.isObjectLiteralExpression(optionsArg)) return [];
|
|
1554
1134
|
const entityProp = optionsArg.getProperty("entity");
|
|
1555
|
-
if (!entityProp || !
|
|
1135
|
+
if (!entityProp || !import_ts_morph4.Node.isPropertyAssignment(entityProp)) return [];
|
|
1556
1136
|
const entityInit = entityProp.getInitializer();
|
|
1557
|
-
if (!entityInit || !
|
|
1137
|
+
if (!entityInit || !import_ts_morph4.Node.isIdentifier(entityInit)) return [];
|
|
1558
1138
|
const entityName = entityInit.getText();
|
|
1559
1139
|
const filterSourceFile = filterClass.getSourceFile();
|
|
1560
1140
|
const resolvedEntity = findType(entityName, filterSourceFile, project);
|
|
@@ -1570,17 +1150,17 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1570
1150
|
const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
|
|
1571
1151
|
if (relationsDecorator) {
|
|
1572
1152
|
const relArgs = relationsDecorator.getArguments();
|
|
1573
|
-
if (relArgs.length > 0 &&
|
|
1153
|
+
if (relArgs.length > 0 && import_ts_morph4.Node.isObjectLiteralExpression(relArgs[0])) {
|
|
1574
1154
|
for (const relProp of relArgs[0].getProperties()) {
|
|
1575
|
-
if (!
|
|
1155
|
+
if (!import_ts_morph4.Node.isPropertyAssignment(relProp)) continue;
|
|
1576
1156
|
const relInit = relProp.getInitializer();
|
|
1577
|
-
if (!relInit || !
|
|
1157
|
+
if (!relInit || !import_ts_morph4.Node.isObjectLiteralExpression(relInit)) continue;
|
|
1578
1158
|
const keysProp = relInit.getProperty("keys");
|
|
1579
|
-
if (!keysProp || !
|
|
1159
|
+
if (!keysProp || !import_ts_morph4.Node.isPropertyAssignment(keysProp)) continue;
|
|
1580
1160
|
const keysInit = keysProp.getInitializer();
|
|
1581
|
-
if (!keysInit || !
|
|
1161
|
+
if (!keysInit || !import_ts_morph4.Node.isArrayLiteralExpression(keysInit)) continue;
|
|
1582
1162
|
for (const el of keysInit.getElements()) {
|
|
1583
|
-
if (
|
|
1163
|
+
if (import_ts_morph4.Node.isStringLiteral(el)) {
|
|
1584
1164
|
fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
|
|
1585
1165
|
}
|
|
1586
1166
|
}
|
|
@@ -1590,267 +1170,65 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
1590
1170
|
return fields;
|
|
1591
1171
|
}
|
|
1592
1172
|
|
|
1593
|
-
// src/discovery/
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
const prevCtx = setDiscoveryContext({
|
|
1623
|
-
projectRoot: cwd,
|
|
1624
|
-
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
1625
|
-
});
|
|
1626
|
-
try {
|
|
1627
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
1628
|
-
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
1629
|
-
}
|
|
1630
|
-
} finally {
|
|
1631
|
-
restoreDiscoveryContext(prevCtx);
|
|
1632
|
-
}
|
|
1633
|
-
return routes;
|
|
1634
|
-
}
|
|
1635
|
-
function zodAstToTs(node) {
|
|
1636
|
-
if (!import_ts_morph6.Node.isCallExpression(node)) return "unknown";
|
|
1637
|
-
const expr = node.getExpression();
|
|
1638
|
-
if (import_ts_morph6.Node.isPropertyAccessExpression(expr)) {
|
|
1639
|
-
const methodName = expr.getName();
|
|
1640
|
-
const receiver = expr.getExpression();
|
|
1641
|
-
if (methodName === "optional") {
|
|
1642
|
-
return `${zodAstToTs(receiver)} | undefined`;
|
|
1643
|
-
}
|
|
1644
|
-
if (methodName === "nullable") {
|
|
1645
|
-
return `${zodAstToTs(receiver)} | null`;
|
|
1646
|
-
}
|
|
1647
|
-
const args = node.getArguments();
|
|
1648
|
-
switch (methodName) {
|
|
1649
|
-
case "string":
|
|
1650
|
-
return "string";
|
|
1651
|
-
case "number":
|
|
1652
|
-
return "number";
|
|
1653
|
-
case "boolean":
|
|
1654
|
-
return "boolean";
|
|
1655
|
-
case "unknown":
|
|
1656
|
-
return "unknown";
|
|
1657
|
-
case "any":
|
|
1658
|
-
return "unknown";
|
|
1659
|
-
case "literal": {
|
|
1660
|
-
const lit = args[0];
|
|
1661
|
-
if (!lit) return "unknown";
|
|
1662
|
-
if (import_ts_morph6.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
1663
|
-
if (import_ts_morph6.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
1664
|
-
if (lit.getKind() === import_ts_morph6.SyntaxKind.TrueKeyword) return "true";
|
|
1665
|
-
if (lit.getKind() === import_ts_morph6.SyntaxKind.FalseKeyword) return "false";
|
|
1666
|
-
return "unknown";
|
|
1667
|
-
}
|
|
1668
|
-
case "enum": {
|
|
1669
|
-
const arrArg = args[0];
|
|
1670
|
-
if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1671
|
-
const members = arrArg.getElements().map(
|
|
1672
|
-
(el) => import_ts_morph6.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
1673
|
-
);
|
|
1674
|
-
return members.join(" | ");
|
|
1675
|
-
}
|
|
1676
|
-
case "array": {
|
|
1677
|
-
const inner = args[0];
|
|
1678
|
-
if (!inner) return "unknown";
|
|
1679
|
-
return `Array<${zodAstToTs(inner)}>`;
|
|
1680
|
-
}
|
|
1681
|
-
case "object": {
|
|
1682
|
-
const objArg = args[0];
|
|
1683
|
-
if (!objArg || !import_ts_morph6.Node.isObjectLiteralExpression(objArg)) return "unknown";
|
|
1684
|
-
const lines = [];
|
|
1685
|
-
for (const prop of objArg.getProperties()) {
|
|
1686
|
-
if (!import_ts_morph6.Node.isPropertyAssignment(prop)) continue;
|
|
1687
|
-
const key = prop.getName();
|
|
1688
|
-
const valNode = prop.getInitializer();
|
|
1689
|
-
if (!valNode) continue;
|
|
1690
|
-
const tsType = zodAstToTs(valNode);
|
|
1691
|
-
const isOpt = isOptionalChain(valNode);
|
|
1692
|
-
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
1693
|
-
}
|
|
1694
|
-
return `{ ${lines.join("; ")} }`;
|
|
1695
|
-
}
|
|
1696
|
-
case "union": {
|
|
1697
|
-
const arrArg = args[0];
|
|
1698
|
-
if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1699
|
-
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
1700
|
-
}
|
|
1701
|
-
case "record": {
|
|
1702
|
-
const valArg = args.length === 1 ? args[0] : args[1];
|
|
1703
|
-
if (!valArg) return "unknown";
|
|
1704
|
-
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
1705
|
-
}
|
|
1706
|
-
case "tuple": {
|
|
1707
|
-
const arrArg = args[0];
|
|
1708
|
-
if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1709
|
-
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
1710
|
-
}
|
|
1711
|
-
default:
|
|
1712
|
-
return "unknown";
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
return "unknown";
|
|
1716
|
-
}
|
|
1717
|
-
function isOptionalChain(node) {
|
|
1718
|
-
if (!import_ts_morph6.Node.isCallExpression(node)) return false;
|
|
1719
|
-
const expr = node.getExpression();
|
|
1720
|
-
return import_ts_morph6.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
1721
|
-
}
|
|
1722
|
-
function decoratorStringArg(decoratorExpr) {
|
|
1723
|
-
if (!decoratorExpr) return void 0;
|
|
1724
|
-
if (import_ts_morph6.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
1725
|
-
if (import_ts_morph6.Node.isArrayLiteralExpression(decoratorExpr)) {
|
|
1726
|
-
const first = decoratorExpr.getElements()[0];
|
|
1727
|
-
if (first && import_ts_morph6.Node.isStringLiteral(first)) return first.getLiteralValue();
|
|
1728
|
-
}
|
|
1729
|
-
return void 0;
|
|
1730
|
-
}
|
|
1731
|
-
function parseDefineContractCall(callExpr) {
|
|
1732
|
-
if (!import_ts_morph6.Node.isCallExpression(callExpr)) return null;
|
|
1733
|
-
const callee = callExpr.getExpression();
|
|
1734
|
-
const calleeName = import_ts_morph6.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph6.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
1735
|
-
if (calleeName !== "defineContract") return null;
|
|
1736
|
-
const args = callExpr.getArguments();
|
|
1737
|
-
const optsArg = args[0];
|
|
1738
|
-
if (!optsArg || !import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
1739
|
-
let query = null;
|
|
1740
|
-
let body = null;
|
|
1741
|
-
let response = "unknown";
|
|
1742
|
-
let bodyZodText = null;
|
|
1743
|
-
let queryZodText = null;
|
|
1744
|
-
for (const prop of optsArg.getProperties()) {
|
|
1745
|
-
if (!import_ts_morph6.Node.isPropertyAssignment(prop)) continue;
|
|
1746
|
-
const propName = prop.getName();
|
|
1747
|
-
const val = prop.getInitializer();
|
|
1748
|
-
if (!val) continue;
|
|
1749
|
-
if (propName === "query") {
|
|
1750
|
-
query = zodAstToTs(val);
|
|
1751
|
-
queryZodText = val.getText();
|
|
1752
|
-
} else if (propName === "body") {
|
|
1753
|
-
body = zodAstToTs(val);
|
|
1754
|
-
bodyZodText = val.getText();
|
|
1755
|
-
} else if (propName === "response") {
|
|
1756
|
-
response = zodAstToTs(val);
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
1760
|
-
}
|
|
1761
|
-
function deriveClassSegment(className) {
|
|
1762
|
-
const noSuffix = className.replace(/Controller$/, "");
|
|
1763
|
-
if (!noSuffix) {
|
|
1764
|
-
throw new Error(
|
|
1765
|
-
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
1766
|
-
);
|
|
1767
|
-
}
|
|
1768
|
-
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
1769
|
-
}
|
|
1770
|
-
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
1771
|
-
const classPortion = classAs ?? deriveClassSegment(className);
|
|
1772
|
-
const methodPortion = methodAs ?? methodName;
|
|
1773
|
-
return `${classPortion}.${methodPortion}`;
|
|
1774
|
-
}
|
|
1775
|
-
function joinPaths(prefix, suffix) {
|
|
1776
|
-
if (!prefix && !suffix) return "/";
|
|
1777
|
-
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1778
|
-
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1779
|
-
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1780
|
-
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1781
|
-
const combined = p + s;
|
|
1782
|
-
return combined === "" ? "/" : combined;
|
|
1783
|
-
}
|
|
1784
|
-
function extractParams(path) {
|
|
1785
|
-
const matches = path.matchAll(/:(\w+)/g);
|
|
1786
|
-
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
1787
|
-
}
|
|
1173
|
+
// src/discovery/dto-type-resolver.ts
|
|
1174
|
+
var WRAPPER_TYPES = {
|
|
1175
|
+
// MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
|
|
1176
|
+
// wrappers around related entities; the wire shape is just the referenced
|
|
1177
|
+
// entity. Unwrap to the type argument.
|
|
1178
|
+
Ref: "unwrap",
|
|
1179
|
+
Reference: "unwrap",
|
|
1180
|
+
LoadedReference: "unwrap",
|
|
1181
|
+
IdentifiedReference: "unwrap",
|
|
1182
|
+
// MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
|
|
1183
|
+
Opt: "unwrap",
|
|
1184
|
+
Loaded: "unwrap",
|
|
1185
|
+
// Promise<T> — unwrap
|
|
1186
|
+
Promise: "unwrap",
|
|
1187
|
+
// MikroORM Collection<T> serializes as an array of T on the wire.
|
|
1188
|
+
Collection: "arrayOf",
|
|
1189
|
+
// Array<T> generic form
|
|
1190
|
+
Array: "arrayOf"
|
|
1191
|
+
};
|
|
1192
|
+
var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
1193
|
+
"Record",
|
|
1194
|
+
"Omit",
|
|
1195
|
+
"Pick",
|
|
1196
|
+
"Partial",
|
|
1197
|
+
"Required",
|
|
1198
|
+
"Readonly",
|
|
1199
|
+
"Map",
|
|
1200
|
+
"Set"
|
|
1201
|
+
]);
|
|
1788
1202
|
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
1789
1203
|
if (depth <= 0) return "unknown";
|
|
1790
|
-
if (
|
|
1204
|
+
if (import_ts_morph5.Node.isArrayTypeNode(typeNode)) {
|
|
1791
1205
|
const elementType = typeNode.getElementTypeNode();
|
|
1792
1206
|
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
1793
1207
|
}
|
|
1794
|
-
if (
|
|
1208
|
+
if (import_ts_morph5.Node.isUnionTypeNode(typeNode)) {
|
|
1795
1209
|
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
1796
1210
|
}
|
|
1797
|
-
if (
|
|
1211
|
+
if (import_ts_morph5.Node.isIntersectionTypeNode(typeNode)) {
|
|
1798
1212
|
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
1799
1213
|
}
|
|
1800
|
-
if (
|
|
1214
|
+
if (import_ts_morph5.Node.isParenthesizedTypeNode(typeNode)) {
|
|
1801
1215
|
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
1802
1216
|
}
|
|
1803
|
-
if (
|
|
1217
|
+
if (import_ts_morph5.Node.isTypeReference(typeNode)) {
|
|
1804
1218
|
const typeName = typeNode.getTypeName();
|
|
1805
|
-
const name =
|
|
1219
|
+
const name = import_ts_morph5.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
1806
1220
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
1807
1221
|
if (name === "Date") return "string";
|
|
1808
1222
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
1809
1223
|
if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
|
|
1810
1224
|
return "unknown";
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1815
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1816
|
-
}
|
|
1817
|
-
return "unknown";
|
|
1225
|
+
const wrapperMode = WRAPPER_TYPES[name];
|
|
1226
|
+
if (wrapperMode) {
|
|
1227
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
1818
1228
|
}
|
|
1819
|
-
if (name
|
|
1820
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
1821
|
-
const firstTypeArg = typeArgs[0];
|
|
1822
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1823
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
1824
|
-
}
|
|
1825
|
-
return "Array<unknown>";
|
|
1826
|
-
}
|
|
1827
|
-
if (name === "Opt" || name === "Loaded") {
|
|
1828
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
1829
|
-
const firstTypeArg = typeArgs[0];
|
|
1830
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1831
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1832
|
-
}
|
|
1833
|
-
return "unknown";
|
|
1834
|
-
}
|
|
1835
|
-
if (name === "Array") {
|
|
1836
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
1837
|
-
const firstTypeArg = typeArgs[0];
|
|
1838
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1839
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
1840
|
-
}
|
|
1841
|
-
return "Array<unknown>";
|
|
1842
|
-
}
|
|
1843
|
-
if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
|
|
1229
|
+
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
1844
1230
|
return typeNode.getText();
|
|
1845
1231
|
}
|
|
1846
|
-
if (name === "Promise") {
|
|
1847
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
1848
|
-
const firstTypeArg = typeArgs[0];
|
|
1849
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1850
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1851
|
-
}
|
|
1852
|
-
return "unknown";
|
|
1853
|
-
}
|
|
1854
1232
|
const resolved = findType(name, sourceFile, project);
|
|
1855
1233
|
if (resolved) {
|
|
1856
1234
|
return expandTypeDecl(resolved, project, depth - 1);
|
|
@@ -1859,13 +1237,22 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
1859
1237
|
return "unknown";
|
|
1860
1238
|
}
|
|
1861
1239
|
const kind = typeNode.getKind();
|
|
1862
|
-
if (kind ===
|
|
1863
|
-
if (kind ===
|
|
1864
|
-
if (kind ===
|
|
1865
|
-
if (kind ===
|
|
1866
|
-
if (kind ===
|
|
1240
|
+
if (kind === import_ts_morph5.SyntaxKind.StringKeyword) return "string";
|
|
1241
|
+
if (kind === import_ts_morph5.SyntaxKind.NumberKeyword) return "number";
|
|
1242
|
+
if (kind === import_ts_morph5.SyntaxKind.BooleanKeyword) return "boolean";
|
|
1243
|
+
if (kind === import_ts_morph5.SyntaxKind.UnknownKeyword) return "unknown";
|
|
1244
|
+
if (kind === import_ts_morph5.SyntaxKind.AnyKeyword) return "unknown";
|
|
1867
1245
|
return typeNode.getText();
|
|
1868
1246
|
}
|
|
1247
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
1248
|
+
const typeArgs = typeNode.getTypeArguments();
|
|
1249
|
+
const firstTypeArg = typeArgs[0];
|
|
1250
|
+
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
1251
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
1252
|
+
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
1253
|
+
}
|
|
1254
|
+
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
1255
|
+
}
|
|
1869
1256
|
function expandTypeDecl(result, project, depth) {
|
|
1870
1257
|
if (depth < 0) return "unknown";
|
|
1871
1258
|
switch (result.kind) {
|
|
@@ -1931,7 +1318,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
1931
1318
|
const paramArgs = paramDecorator.getArguments();
|
|
1932
1319
|
if (paramArgs.length === 0) continue;
|
|
1933
1320
|
const nameArg = paramArgs[0];
|
|
1934
|
-
if (!
|
|
1321
|
+
if (!import_ts_morph5.Node.isStringLiteral(nameArg)) continue;
|
|
1935
1322
|
const paramName = nameArg.getLiteralValue();
|
|
1936
1323
|
const typeNode = param.getTypeNode();
|
|
1937
1324
|
const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
|
|
@@ -1944,13 +1331,13 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1944
1331
|
if (apiResponseDecorator) {
|
|
1945
1332
|
const args = apiResponseDecorator.getArguments();
|
|
1946
1333
|
const optsArg = args[0];
|
|
1947
|
-
if (optsArg &&
|
|
1334
|
+
if (optsArg && import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) {
|
|
1948
1335
|
for (const prop of optsArg.getProperties()) {
|
|
1949
|
-
if (!
|
|
1336
|
+
if (!import_ts_morph5.Node.isPropertyAssignment(prop)) continue;
|
|
1950
1337
|
if (prop.getName() !== "type") continue;
|
|
1951
1338
|
const val = prop.getInitializer();
|
|
1952
1339
|
if (!val) continue;
|
|
1953
|
-
if (
|
|
1340
|
+
if (import_ts_morph5.Node.isArrayLiteralExpression(val)) {
|
|
1954
1341
|
const elements = val.getElements();
|
|
1955
1342
|
const firstEl = elements[0];
|
|
1956
1343
|
if (elements.length > 0 && firstEl !== void 0) {
|
|
@@ -1970,7 +1357,7 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
1970
1357
|
return "unknown";
|
|
1971
1358
|
}
|
|
1972
1359
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
1973
|
-
if (!
|
|
1360
|
+
if (!import_ts_morph5.Node.isIdentifier(node)) return "unknown";
|
|
1974
1361
|
const name = node.getText();
|
|
1975
1362
|
const resolved = findType(name, sourceFile, project);
|
|
1976
1363
|
if (resolved) {
|
|
@@ -2017,11 +1404,11 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2017
1404
|
if (apiResp) {
|
|
2018
1405
|
const args = apiResp.getArguments();
|
|
2019
1406
|
const optsArg = args[0];
|
|
2020
|
-
if (optsArg &&
|
|
1407
|
+
if (optsArg && import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) {
|
|
2021
1408
|
for (const prop of optsArg.getProperties()) {
|
|
2022
|
-
if (
|
|
1409
|
+
if (import_ts_morph5.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
|
|
2023
1410
|
const val = prop.getInitializer();
|
|
2024
|
-
if (val &&
|
|
1411
|
+
if (val && import_ts_morph5.Node.isIdentifier(val)) {
|
|
2025
1412
|
const name = val.getText();
|
|
2026
1413
|
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
2027
1414
|
if (localDecl?.isExported()) {
|
|
@@ -2038,27 +1425,18 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2038
1425
|
}
|
|
2039
1426
|
}
|
|
2040
1427
|
}
|
|
2041
|
-
let bodyZodText = null;
|
|
2042
|
-
let queryZodText = null;
|
|
2043
1428
|
let bodySchema = null;
|
|
2044
1429
|
let querySchema = null;
|
|
2045
|
-
const formNested = {};
|
|
2046
1430
|
const formWarnings = [];
|
|
2047
1431
|
const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
|
|
2048
1432
|
if (bodyClass) {
|
|
2049
|
-
const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
|
|
2050
|
-
bodyZodText = result.schemaText;
|
|
2051
|
-
for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
|
|
2052
|
-
formWarnings.push(...result.warnings);
|
|
2053
1433
|
bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
|
|
1434
|
+
formWarnings.push(...bodySchema.warnings);
|
|
2054
1435
|
}
|
|
2055
1436
|
const queryClass = resolveParamClass(method, "Query", sourceFile, project);
|
|
2056
1437
|
if (queryClass) {
|
|
2057
|
-
const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
|
|
2058
|
-
queryZodText = result.schemaText;
|
|
2059
|
-
for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
|
|
2060
|
-
formWarnings.push(...result.warnings);
|
|
2061
1438
|
querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
|
|
1439
|
+
formWarnings.push(...querySchema.warnings);
|
|
2062
1440
|
}
|
|
2063
1441
|
return {
|
|
2064
1442
|
query,
|
|
@@ -2071,9 +1449,6 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
2071
1449
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
2072
1450
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
2073
1451
|
filterSource: filterInfo?.source ?? null,
|
|
2074
|
-
bodyZodText,
|
|
2075
|
-
queryZodText,
|
|
2076
|
-
formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
|
|
2077
1452
|
formWarnings,
|
|
2078
1453
|
bodySchema,
|
|
2079
1454
|
querySchema
|
|
@@ -2093,6 +1468,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
|
2093
1468
|
}
|
|
2094
1469
|
return null;
|
|
2095
1470
|
}
|
|
1471
|
+
|
|
1472
|
+
// src/discovery/zod-ast-to-ts.ts
|
|
1473
|
+
var import_ts_morph6 = require("ts-morph");
|
|
1474
|
+
function zodAstToTs(node) {
|
|
1475
|
+
if (!import_ts_morph6.Node.isCallExpression(node)) return "unknown";
|
|
1476
|
+
const expr = node.getExpression();
|
|
1477
|
+
if (import_ts_morph6.Node.isPropertyAccessExpression(expr)) {
|
|
1478
|
+
const methodName = expr.getName();
|
|
1479
|
+
const receiver = expr.getExpression();
|
|
1480
|
+
if (methodName === "optional") {
|
|
1481
|
+
return `${zodAstToTs(receiver)} | undefined`;
|
|
1482
|
+
}
|
|
1483
|
+
if (methodName === "nullable") {
|
|
1484
|
+
return `${zodAstToTs(receiver)} | null`;
|
|
1485
|
+
}
|
|
1486
|
+
const args = node.getArguments();
|
|
1487
|
+
switch (methodName) {
|
|
1488
|
+
case "string":
|
|
1489
|
+
return "string";
|
|
1490
|
+
case "number":
|
|
1491
|
+
return "number";
|
|
1492
|
+
case "boolean":
|
|
1493
|
+
return "boolean";
|
|
1494
|
+
case "unknown":
|
|
1495
|
+
return "unknown";
|
|
1496
|
+
case "any":
|
|
1497
|
+
return "unknown";
|
|
1498
|
+
case "literal": {
|
|
1499
|
+
const lit = args[0];
|
|
1500
|
+
if (!lit) return "unknown";
|
|
1501
|
+
if (import_ts_morph6.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
1502
|
+
if (import_ts_morph6.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
1503
|
+
if (lit.getKind() === import_ts_morph6.SyntaxKind.TrueKeyword) return "true";
|
|
1504
|
+
if (lit.getKind() === import_ts_morph6.SyntaxKind.FalseKeyword) return "false";
|
|
1505
|
+
return "unknown";
|
|
1506
|
+
}
|
|
1507
|
+
case "enum": {
|
|
1508
|
+
const arrArg = args[0];
|
|
1509
|
+
if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1510
|
+
const members = arrArg.getElements().map(
|
|
1511
|
+
(el) => import_ts_morph6.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
1512
|
+
);
|
|
1513
|
+
return members.join(" | ");
|
|
1514
|
+
}
|
|
1515
|
+
case "array": {
|
|
1516
|
+
const inner = args[0];
|
|
1517
|
+
if (!inner) return "unknown";
|
|
1518
|
+
return `Array<${zodAstToTs(inner)}>`;
|
|
1519
|
+
}
|
|
1520
|
+
case "object": {
|
|
1521
|
+
const objArg = args[0];
|
|
1522
|
+
if (!objArg || !import_ts_morph6.Node.isObjectLiteralExpression(objArg)) return "unknown";
|
|
1523
|
+
const lines = [];
|
|
1524
|
+
for (const prop of objArg.getProperties()) {
|
|
1525
|
+
if (!import_ts_morph6.Node.isPropertyAssignment(prop)) continue;
|
|
1526
|
+
const key = prop.getName();
|
|
1527
|
+
const valNode = prop.getInitializer();
|
|
1528
|
+
if (!valNode) continue;
|
|
1529
|
+
const tsType = zodAstToTs(valNode);
|
|
1530
|
+
const isOpt = isOptionalChain(valNode);
|
|
1531
|
+
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
1532
|
+
}
|
|
1533
|
+
return `{ ${lines.join("; ")} }`;
|
|
1534
|
+
}
|
|
1535
|
+
case "union": {
|
|
1536
|
+
const arrArg = args[0];
|
|
1537
|
+
if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1538
|
+
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
1539
|
+
}
|
|
1540
|
+
case "record": {
|
|
1541
|
+
const valArg = args.length === 1 ? args[0] : args[1];
|
|
1542
|
+
if (!valArg) return "unknown";
|
|
1543
|
+
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
1544
|
+
}
|
|
1545
|
+
case "tuple": {
|
|
1546
|
+
const arrArg = args[0];
|
|
1547
|
+
if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
1548
|
+
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
1549
|
+
}
|
|
1550
|
+
default:
|
|
1551
|
+
return "unknown";
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return "unknown";
|
|
1555
|
+
}
|
|
1556
|
+
function isOptionalChain(node) {
|
|
1557
|
+
if (!import_ts_morph6.Node.isCallExpression(node)) return false;
|
|
1558
|
+
const expr = node.getExpression();
|
|
1559
|
+
return import_ts_morph6.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
1560
|
+
}
|
|
1561
|
+
function parseDefineContractCall(callExpr) {
|
|
1562
|
+
if (!import_ts_morph6.Node.isCallExpression(callExpr)) return null;
|
|
1563
|
+
const callee = callExpr.getExpression();
|
|
1564
|
+
const calleeName = import_ts_morph6.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph6.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
1565
|
+
if (calleeName !== "defineContract") return null;
|
|
1566
|
+
const args = callExpr.getArguments();
|
|
1567
|
+
const optsArg = args[0];
|
|
1568
|
+
if (!optsArg || !import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
1569
|
+
let query = null;
|
|
1570
|
+
let body = null;
|
|
1571
|
+
let response = "unknown";
|
|
1572
|
+
let bodyZodText = null;
|
|
1573
|
+
let queryZodText = null;
|
|
1574
|
+
for (const prop of optsArg.getProperties()) {
|
|
1575
|
+
if (!import_ts_morph6.Node.isPropertyAssignment(prop)) continue;
|
|
1576
|
+
const propName = prop.getName();
|
|
1577
|
+
const val = prop.getInitializer();
|
|
1578
|
+
if (!val) continue;
|
|
1579
|
+
if (propName === "query") {
|
|
1580
|
+
query = zodAstToTs(val);
|
|
1581
|
+
queryZodText = val.getText();
|
|
1582
|
+
} else if (propName === "body") {
|
|
1583
|
+
body = zodAstToTs(val);
|
|
1584
|
+
bodyZodText = val.getText();
|
|
1585
|
+
} else if (propName === "response") {
|
|
1586
|
+
response = zodAstToTs(val);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return { query, body, response, bodyZodText, queryZodText };
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/discovery/contracts-fast.ts
|
|
1593
|
+
async function discoverContractsFast(opts) {
|
|
1594
|
+
const { cwd, glob, tsconfig } = opts;
|
|
1595
|
+
const tsconfigPath = tsconfig ? (0, import_node_path3.resolve)(tsconfig) : (0, import_node_path3.join)(cwd, "tsconfig.json");
|
|
1596
|
+
let project;
|
|
1597
|
+
try {
|
|
1598
|
+
project = new import_ts_morph7.Project({
|
|
1599
|
+
tsConfigFilePath: tsconfigPath,
|
|
1600
|
+
skipAddingFilesFromTsConfig: true,
|
|
1601
|
+
skipLoadingLibFiles: true,
|
|
1602
|
+
skipFileDependencyResolution: true
|
|
1603
|
+
});
|
|
1604
|
+
} catch {
|
|
1605
|
+
project = new import_ts_morph7.Project({
|
|
1606
|
+
skipAddingFilesFromTsConfig: true,
|
|
1607
|
+
skipLoadingLibFiles: true,
|
|
1608
|
+
skipFileDependencyResolution: true,
|
|
1609
|
+
compilerOptions: {
|
|
1610
|
+
allowJs: true,
|
|
1611
|
+
resolveJsonModule: false,
|
|
1612
|
+
strict: false
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
const files = await (0, import_fast_glob.default)(glob, { cwd, absolute: true, onlyFiles: true });
|
|
1617
|
+
for (const f of files) {
|
|
1618
|
+
project.addSourceFileAtPath(f);
|
|
1619
|
+
}
|
|
1620
|
+
const routes = [];
|
|
1621
|
+
setDiscoveryContext(project, {
|
|
1622
|
+
projectRoot: cwd,
|
|
1623
|
+
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
1624
|
+
});
|
|
1625
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
1626
|
+
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
1627
|
+
}
|
|
1628
|
+
return routes;
|
|
1629
|
+
}
|
|
1630
|
+
function decoratorStringArg(decoratorExpr) {
|
|
1631
|
+
if (!decoratorExpr) return void 0;
|
|
1632
|
+
if (import_ts_morph7.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
1633
|
+
if (import_ts_morph7.Node.isArrayLiteralExpression(decoratorExpr)) {
|
|
1634
|
+
const first = decoratorExpr.getElements()[0];
|
|
1635
|
+
if (first && import_ts_morph7.Node.isStringLiteral(first)) return first.getLiteralValue();
|
|
1636
|
+
}
|
|
1637
|
+
return void 0;
|
|
1638
|
+
}
|
|
1639
|
+
function deriveClassSegment(className) {
|
|
1640
|
+
const noSuffix = className.replace(/Controller$/, "");
|
|
1641
|
+
if (!noSuffix) {
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
1647
|
+
}
|
|
1648
|
+
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
1649
|
+
const classPortion = classAs ?? deriveClassSegment(className);
|
|
1650
|
+
const methodPortion = methodAs ?? methodName;
|
|
1651
|
+
return `${classPortion}.${methodPortion}`;
|
|
1652
|
+
}
|
|
1653
|
+
function joinPaths(prefix, suffix) {
|
|
1654
|
+
if (!prefix && !suffix) return "/";
|
|
1655
|
+
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1656
|
+
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1657
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1658
|
+
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
1659
|
+
const combined = p + s;
|
|
1660
|
+
return combined === "" ? "/" : combined;
|
|
1661
|
+
}
|
|
1662
|
+
function extractParams(path) {
|
|
1663
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
1664
|
+
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
1665
|
+
}
|
|
2096
1666
|
var HTTP_METHOD_DECORATORS = {
|
|
2097
1667
|
Get: "GET",
|
|
2098
1668
|
Post: "POST",
|
|
@@ -2103,176 +1673,186 @@ var HTTP_METHOD_DECORATORS = {
|
|
|
2103
1673
|
Head: "HEAD",
|
|
2104
1674
|
All: "ALL"
|
|
2105
1675
|
};
|
|
1676
|
+
function resolveVerb(method) {
|
|
1677
|
+
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
1678
|
+
const httpDecorator = method.getDecorator(decoratorName);
|
|
1679
|
+
if (httpDecorator) {
|
|
1680
|
+
const httpArgs = httpDecorator.getArguments();
|
|
1681
|
+
const pathArg = httpArgs[0];
|
|
1682
|
+
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return null;
|
|
1686
|
+
}
|
|
1687
|
+
function readAsDecorator(node, label) {
|
|
1688
|
+
const asDecorator = node.getDecorator("As");
|
|
1689
|
+
if (!asDecorator) return void 0;
|
|
1690
|
+
const asName = decoratorStringArg(asDecorator.getArguments()[0]);
|
|
1691
|
+
if (!asName) {
|
|
1692
|
+
throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
|
|
1693
|
+
}
|
|
1694
|
+
return asName;
|
|
1695
|
+
}
|
|
1696
|
+
function buildRoute(args) {
|
|
1697
|
+
const {
|
|
1698
|
+
className,
|
|
1699
|
+
methodName,
|
|
1700
|
+
resolvedMethod,
|
|
1701
|
+
combinedPath,
|
|
1702
|
+
classAs,
|
|
1703
|
+
methodAs,
|
|
1704
|
+
sourceFile,
|
|
1705
|
+
seenNames,
|
|
1706
|
+
contractSource
|
|
1707
|
+
} = args;
|
|
1708
|
+
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
1709
|
+
const qualifiedRef = `${className}.${methodName}`;
|
|
1710
|
+
const existing = seenNames.get(routeName);
|
|
1711
|
+
if (existing !== void 0) {
|
|
1712
|
+
throw new Error(
|
|
1713
|
+
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
seenNames.set(routeName, qualifiedRef);
|
|
1717
|
+
return {
|
|
1718
|
+
method: resolvedMethod,
|
|
1719
|
+
path: combinedPath,
|
|
1720
|
+
name: routeName,
|
|
1721
|
+
params: extractParams(combinedPath),
|
|
1722
|
+
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
1723
|
+
contract: { contractSource }
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function extractContractRoute(args) {
|
|
1727
|
+
const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
|
|
1728
|
+
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
1729
|
+
if (!firstDecoratorArg) return null;
|
|
1730
|
+
let contractDef = null;
|
|
1731
|
+
let bodyZodRef = null;
|
|
1732
|
+
let queryZodRef = null;
|
|
1733
|
+
if (import_ts_morph7.Node.isCallExpression(firstDecoratorArg)) {
|
|
1734
|
+
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
1735
|
+
} else if (import_ts_morph7.Node.isIdentifier(firstDecoratorArg)) {
|
|
1736
|
+
const identName = firstDecoratorArg.getText();
|
|
1737
|
+
const varDecl = sourceFile.getVariableDeclaration(identName);
|
|
1738
|
+
if (!varDecl) {
|
|
1739
|
+
console.warn(
|
|
1740
|
+
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
|
|
1741
|
+
);
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
const initializer = varDecl.getInitializer();
|
|
1745
|
+
if (!initializer) return null;
|
|
1746
|
+
contractDef = parseDefineContractCall(initializer);
|
|
1747
|
+
if (contractDef && varDecl.isExported()) {
|
|
1748
|
+
const filePath = sourceFile.getFilePath();
|
|
1749
|
+
if (contractDef.body !== null) {
|
|
1750
|
+
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
1751
|
+
}
|
|
1752
|
+
if (contractDef.query !== null) {
|
|
1753
|
+
queryZodRef = { name: `${identName}.query`, filePath };
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
} else {
|
|
1757
|
+
console.warn(
|
|
1758
|
+
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
1759
|
+
);
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
if (!contractDef) return null;
|
|
1763
|
+
if (!verb) return null;
|
|
1764
|
+
const resolvedPath = joinPaths(prefix, verb.handlerPath);
|
|
1765
|
+
const methodName = method.getName();
|
|
1766
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
1767
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
1768
|
+
return buildRoute({
|
|
1769
|
+
className,
|
|
1770
|
+
methodName,
|
|
1771
|
+
resolvedMethod: verb.httpMethod,
|
|
1772
|
+
combinedPath: resolvedPath,
|
|
1773
|
+
classAs,
|
|
1774
|
+
methodAs,
|
|
1775
|
+
sourceFile,
|
|
1776
|
+
seenNames,
|
|
1777
|
+
contractSource: {
|
|
1778
|
+
query: contractDef.query,
|
|
1779
|
+
body: contractDef.body,
|
|
1780
|
+
response: contractDef.response,
|
|
1781
|
+
// Path A: capture both the importable ref and the raw text. The emitter
|
|
1782
|
+
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
1783
|
+
// would drag server-only deps into the client bundle).
|
|
1784
|
+
bodyZodRef,
|
|
1785
|
+
bodyZodText: contractDef.bodyZodText,
|
|
1786
|
+
queryZodRef,
|
|
1787
|
+
queryZodText: contractDef.queryZodText
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
function extractDtoRoute(args) {
|
|
1792
|
+
const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
|
|
1793
|
+
if (!verb) return null;
|
|
1794
|
+
const combined = joinPaths(prefix, verb.handlerPath);
|
|
1795
|
+
const methodName = method.getName();
|
|
1796
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
1797
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
1798
|
+
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
1799
|
+
return buildRoute({
|
|
1800
|
+
className,
|
|
1801
|
+
methodName,
|
|
1802
|
+
resolvedMethod: verb.httpMethod,
|
|
1803
|
+
combinedPath: combined,
|
|
1804
|
+
classAs,
|
|
1805
|
+
methodAs,
|
|
1806
|
+
sourceFile,
|
|
1807
|
+
seenNames,
|
|
1808
|
+
contractSource: {
|
|
1809
|
+
query: dtoContract?.query ?? null,
|
|
1810
|
+
body: dtoContract?.body ?? null,
|
|
1811
|
+
response: dtoContract?.response ?? "unknown",
|
|
1812
|
+
queryRef: dtoContract?.queryRef ?? null,
|
|
1813
|
+
bodyRef: dtoContract?.bodyRef ?? null,
|
|
1814
|
+
responseRef: dtoContract?.responseRef ?? null,
|
|
1815
|
+
filterFields: dtoContract?.filterFields ?? null,
|
|
1816
|
+
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
1817
|
+
filterSource: dtoContract?.filterSource ?? null,
|
|
1818
|
+
formWarnings: dtoContract?.formWarnings ?? [],
|
|
1819
|
+
bodySchema: dtoContract?.bodySchema ?? null,
|
|
1820
|
+
querySchema: dtoContract?.querySchema ?? null
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
2106
1824
|
function extractFromSourceFile(sourceFile, project) {
|
|
2107
1825
|
const routes = [];
|
|
2108
1826
|
const seenNames = /* @__PURE__ */ new Map();
|
|
2109
|
-
const
|
|
2110
|
-
for (const cls of classes) {
|
|
1827
|
+
for (const cls of sourceFile.getClasses()) {
|
|
2111
1828
|
const controllerDecorator = cls.getDecorator("Controller");
|
|
2112
1829
|
if (!controllerDecorator) continue;
|
|
2113
|
-
const
|
|
2114
|
-
const
|
|
2115
|
-
const prefix = decoratorStringArg(firstArg3) ?? "";
|
|
1830
|
+
const firstArg2 = controllerDecorator.getArguments()[0];
|
|
1831
|
+
const prefix = decoratorStringArg(firstArg2) ?? "";
|
|
2116
1832
|
const className = cls.getName() ?? "Unknown";
|
|
2117
1833
|
for (const method of cls.getMethods()) {
|
|
2118
|
-
|
|
2119
|
-
let handlerPath = "";
|
|
2120
|
-
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
2121
|
-
const httpDecorator = method.getDecorator(decoratorName);
|
|
2122
|
-
if (httpDecorator) {
|
|
2123
|
-
httpMethod = verb;
|
|
2124
|
-
const httpArgs = httpDecorator.getArguments();
|
|
2125
|
-
const pathArg = httpArgs[0];
|
|
2126
|
-
handlerPath = decoratorStringArg(pathArg) ?? "";
|
|
2127
|
-
break;
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
1834
|
+
const verb = resolveVerb(method);
|
|
2130
1835
|
const applyContractDecorator = method.getDecorator("ApplyContract");
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
contractDef = parseDefineContractCall(initializer);
|
|
2152
|
-
if (contractDef && varDecl.isExported()) {
|
|
2153
|
-
const filePath = sourceFile.getFilePath();
|
|
2154
|
-
if (contractDef.body !== null) {
|
|
2155
|
-
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
2156
|
-
}
|
|
2157
|
-
if (contractDef.query !== null) {
|
|
2158
|
-
queryZodRef = { name: `${identName}.query`, filePath };
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
} else {
|
|
2162
|
-
console.warn(
|
|
2163
|
-
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
2164
|
-
);
|
|
2165
|
-
continue;
|
|
2166
|
-
}
|
|
2167
|
-
if (!contractDef) continue;
|
|
2168
|
-
if (!httpMethod) continue;
|
|
2169
|
-
const resolvedMethod = httpMethod;
|
|
2170
|
-
const resolvedPath = joinPaths(prefix, handlerPath);
|
|
2171
|
-
const combined = resolvedPath;
|
|
2172
|
-
const params = extractParams(combined);
|
|
2173
|
-
const methodName = method.getName();
|
|
2174
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
2175
|
-
let classAs;
|
|
2176
|
-
if (classAsDecorator) {
|
|
2177
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
2178
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
2179
|
-
if (!classAsName) {
|
|
2180
|
-
throw new Error(
|
|
2181
|
-
`@As decorator on class ${className} must have a non-empty string argument.`
|
|
2182
|
-
);
|
|
2183
|
-
}
|
|
2184
|
-
classAs = classAsName;
|
|
2185
|
-
}
|
|
2186
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
2187
|
-
let methodAs;
|
|
2188
|
-
if (methodAsDecorator) {
|
|
2189
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
2190
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
2191
|
-
if (!methodAsName) {
|
|
2192
|
-
throw new Error(
|
|
2193
|
-
`@As decorator on ${className}.${methodName} must have a non-empty string argument.`
|
|
2194
|
-
);
|
|
2195
|
-
}
|
|
2196
|
-
methodAs = methodAsName;
|
|
2197
|
-
}
|
|
2198
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
2199
|
-
const qualifiedRef = `${className}.${methodName}`;
|
|
2200
|
-
const existing = seenNames.get(routeName);
|
|
2201
|
-
if (existing !== void 0) {
|
|
2202
|
-
throw new Error(
|
|
2203
|
-
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
2204
|
-
);
|
|
2205
|
-
}
|
|
2206
|
-
seenNames.set(routeName, qualifiedRef);
|
|
2207
|
-
routes.push({
|
|
2208
|
-
method: resolvedMethod,
|
|
2209
|
-
path: combined,
|
|
2210
|
-
name: routeName,
|
|
2211
|
-
params,
|
|
2212
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
2213
|
-
contract: {
|
|
2214
|
-
contractSource: {
|
|
2215
|
-
query: contractDef.query,
|
|
2216
|
-
body: contractDef.body,
|
|
2217
|
-
response: contractDef.response,
|
|
2218
|
-
// Path A: capture both the importable ref and the raw text. The
|
|
2219
|
-
// emitter prefers inlining the text (client-safe — re-exporting from
|
|
2220
|
-
// a controller would drag server-only deps into the client bundle).
|
|
2221
|
-
bodyZodRef,
|
|
2222
|
-
bodyZodText: contractDef.bodyZodText,
|
|
2223
|
-
queryZodRef,
|
|
2224
|
-
queryZodText: contractDef.queryZodText
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
});
|
|
2228
|
-
} else {
|
|
2229
|
-
if (!httpMethod) continue;
|
|
2230
|
-
const combined = joinPaths(prefix, handlerPath);
|
|
2231
|
-
const params = extractParams(combined);
|
|
2232
|
-
const methodName = method.getName();
|
|
2233
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
2234
|
-
let classAs;
|
|
2235
|
-
if (classAsDecorator) {
|
|
2236
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
2237
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
2238
|
-
if (classAsName) classAs = classAsName;
|
|
2239
|
-
}
|
|
2240
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
2241
|
-
let methodAs;
|
|
2242
|
-
if (methodAsDecorator) {
|
|
2243
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
2244
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
2245
|
-
if (methodAsName) methodAs = methodAsName;
|
|
2246
|
-
}
|
|
2247
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
2248
|
-
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
2249
|
-
routes.push({
|
|
2250
|
-
method: httpMethod,
|
|
2251
|
-
path: combined,
|
|
2252
|
-
name: routeName,
|
|
2253
|
-
params,
|
|
2254
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
2255
|
-
contract: {
|
|
2256
|
-
contractSource: {
|
|
2257
|
-
query: dtoContract?.query ?? null,
|
|
2258
|
-
body: dtoContract?.body ?? null,
|
|
2259
|
-
response: dtoContract?.response ?? "unknown",
|
|
2260
|
-
queryRef: dtoContract?.queryRef ?? null,
|
|
2261
|
-
bodyRef: dtoContract?.bodyRef ?? null,
|
|
2262
|
-
responseRef: dtoContract?.responseRef ?? null,
|
|
2263
|
-
filterFields: dtoContract?.filterFields ?? null,
|
|
2264
|
-
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
2265
|
-
filterSource: dtoContract?.filterSource ?? null,
|
|
2266
|
-
bodyZodText: dtoContract?.bodyZodText ?? null,
|
|
2267
|
-
queryZodText: dtoContract?.queryZodText ?? null,
|
|
2268
|
-
formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
|
|
2269
|
-
formWarnings: dtoContract?.formWarnings ?? [],
|
|
2270
|
-
bodySchema: dtoContract?.bodySchema ?? null,
|
|
2271
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
});
|
|
2275
|
-
}
|
|
1836
|
+
const route = applyContractDecorator ? extractContractRoute({
|
|
1837
|
+
cls,
|
|
1838
|
+
method,
|
|
1839
|
+
applyContractDecorator,
|
|
1840
|
+
verb,
|
|
1841
|
+
prefix,
|
|
1842
|
+
className,
|
|
1843
|
+
sourceFile,
|
|
1844
|
+
seenNames
|
|
1845
|
+
}) : extractDtoRoute({
|
|
1846
|
+
cls,
|
|
1847
|
+
method,
|
|
1848
|
+
verb,
|
|
1849
|
+
prefix,
|
|
1850
|
+
className,
|
|
1851
|
+
sourceFile,
|
|
1852
|
+
project,
|
|
1853
|
+
seenNames
|
|
1854
|
+
});
|
|
1855
|
+
if (route) routes.push(route);
|
|
2276
1856
|
}
|
|
2277
1857
|
}
|
|
2278
1858
|
return routes;
|
|
@@ -2280,8 +1860,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
2280
1860
|
|
|
2281
1861
|
// src/generate.ts
|
|
2282
1862
|
var import_promises9 = require("fs/promises");
|
|
2283
|
-
var
|
|
2284
|
-
var import_ts_morph9 = require("ts-morph");
|
|
1863
|
+
var import_node_path12 = require("path");
|
|
2285
1864
|
|
|
2286
1865
|
// src/discovery/pages.ts
|
|
2287
1866
|
var import_promises2 = require("fs/promises");
|
|
@@ -2336,7 +1915,8 @@ function extractPropsSource(source, exportName) {
|
|
|
2336
1915
|
}
|
|
2337
1916
|
|
|
2338
1917
|
// src/discovery/shared-props.ts
|
|
2339
|
-
var
|
|
1918
|
+
var import_node_path5 = require("path");
|
|
1919
|
+
var import_ts_morph8 = require("ts-morph");
|
|
2340
1920
|
function discoverSharedProps(project, moduleEntry) {
|
|
2341
1921
|
try {
|
|
2342
1922
|
let sourceFile = project.getSourceFile(moduleEntry);
|
|
@@ -2356,14 +1936,39 @@ function discoverSharedProps(project, moduleEntry) {
|
|
|
2356
1936
|
return null;
|
|
2357
1937
|
}
|
|
2358
1938
|
}
|
|
1939
|
+
function discoverSharedPropsFromConfig(config) {
|
|
1940
|
+
if (!config.app?.moduleEntry) return null;
|
|
1941
|
+
try {
|
|
1942
|
+
const tsconfigPath = config.app.tsconfig ?? (0, import_node_path5.join)(config.codegen.cwd, "tsconfig.json");
|
|
1943
|
+
let project;
|
|
1944
|
+
try {
|
|
1945
|
+
project = new import_ts_morph8.Project({
|
|
1946
|
+
tsConfigFilePath: tsconfigPath,
|
|
1947
|
+
skipAddingFilesFromTsConfig: true,
|
|
1948
|
+
skipLoadingLibFiles: true,
|
|
1949
|
+
skipFileDependencyResolution: true
|
|
1950
|
+
});
|
|
1951
|
+
} catch {
|
|
1952
|
+
project = new import_ts_morph8.Project({
|
|
1953
|
+
skipAddingFilesFromTsConfig: true,
|
|
1954
|
+
skipLoadingLibFiles: true,
|
|
1955
|
+
skipFileDependencyResolution: true,
|
|
1956
|
+
compilerOptions: { allowJs: true, strict: false }
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
return discoverSharedProps(project, config.app.moduleEntry);
|
|
1960
|
+
} catch {
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
2359
1964
|
function findForRootCall(sourceFile) {
|
|
2360
|
-
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
1965
|
+
const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph8.SyntaxKind.CallExpression);
|
|
2361
1966
|
for (const call of callExpressions) {
|
|
2362
1967
|
const expr = call.getExpression();
|
|
2363
|
-
if (!
|
|
1968
|
+
if (!import_ts_morph8.Node.isPropertyAccessExpression(expr)) continue;
|
|
2364
1969
|
const methodName = expr.getName();
|
|
2365
1970
|
const objectExpr = expr.getExpression();
|
|
2366
|
-
if (methodName === "forRoot" &&
|
|
1971
|
+
if (methodName === "forRoot" && import_ts_morph8.Node.isIdentifier(objectExpr)) {
|
|
2367
1972
|
const name = objectExpr.getText();
|
|
2368
1973
|
if (name === "InertiaModule") {
|
|
2369
1974
|
return call;
|
|
@@ -2373,19 +1978,19 @@ function findForRootCall(sourceFile) {
|
|
|
2373
1978
|
return null;
|
|
2374
1979
|
}
|
|
2375
1980
|
function findShareInitializer(forRootCall) {
|
|
2376
|
-
if (!
|
|
1981
|
+
if (!import_ts_morph8.Node.isCallExpression(forRootCall)) return null;
|
|
2377
1982
|
const args = forRootCall.getArguments();
|
|
2378
|
-
const
|
|
2379
|
-
if (!
|
|
2380
|
-
for (const prop of
|
|
2381
|
-
if (
|
|
1983
|
+
const firstArg2 = args[0];
|
|
1984
|
+
if (!firstArg2 || !import_ts_morph8.Node.isObjectLiteralExpression(firstArg2)) return null;
|
|
1985
|
+
for (const prop of firstArg2.getProperties()) {
|
|
1986
|
+
if (import_ts_morph8.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
|
|
2382
1987
|
return prop.getInitializer() ?? null;
|
|
2383
1988
|
}
|
|
2384
1989
|
}
|
|
2385
1990
|
return null;
|
|
2386
1991
|
}
|
|
2387
1992
|
function extractShareType(node, sourceFile, project) {
|
|
2388
|
-
if (
|
|
1993
|
+
if (import_ts_morph8.Node.isIdentifier(node)) {
|
|
2389
1994
|
const ref = resolveIdentifierToImportRef(node, sourceFile, project);
|
|
2390
1995
|
if (ref) {
|
|
2391
1996
|
return {
|
|
@@ -2395,22 +2000,22 @@ function extractShareType(node, sourceFile, project) {
|
|
|
2395
2000
|
};
|
|
2396
2001
|
}
|
|
2397
2002
|
}
|
|
2398
|
-
if (
|
|
2003
|
+
if (import_ts_morph8.Node.isArrowFunction(node)) {
|
|
2399
2004
|
const result = extractFromFunctionLike(node, sourceFile);
|
|
2400
2005
|
return result ? { ...result, isImportRef: false } : null;
|
|
2401
2006
|
}
|
|
2402
|
-
if (
|
|
2007
|
+
if (import_ts_morph8.Node.isFunctionExpression(node)) {
|
|
2403
2008
|
const result = extractFromFunctionLike(node, sourceFile);
|
|
2404
2009
|
return result ? { ...result, isImportRef: false } : null;
|
|
2405
2010
|
}
|
|
2406
|
-
if (
|
|
2011
|
+
if (import_ts_morph8.Node.isObjectLiteralExpression(node)) {
|
|
2407
2012
|
const result = extractFromObjectLiteral(node);
|
|
2408
2013
|
return result ? { ...result, isImportRef: false } : null;
|
|
2409
2014
|
}
|
|
2410
2015
|
return null;
|
|
2411
2016
|
}
|
|
2412
2017
|
function resolveIdentifierToImportRef(id, sourceFile, project) {
|
|
2413
|
-
if (!
|
|
2018
|
+
if (!import_ts_morph8.Node.isIdentifier(id)) return null;
|
|
2414
2019
|
const name = id.getText();
|
|
2415
2020
|
const localFunc = sourceFile.getFunction(name);
|
|
2416
2021
|
if (localFunc?.isExported()) {
|
|
@@ -2444,49 +2049,49 @@ function resolveIdentifierToImportRef(id, sourceFile, project) {
|
|
|
2444
2049
|
return null;
|
|
2445
2050
|
}
|
|
2446
2051
|
function extractFromFunctionLike(node, _sourceFile) {
|
|
2447
|
-
const returnTypeNode =
|
|
2052
|
+
const returnTypeNode = import_ts_morph8.Node.isArrowFunction(node) || import_ts_morph8.Node.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
|
|
2448
2053
|
if (returnTypeNode) {
|
|
2449
2054
|
return extractFromReturnTypeAnnotation(returnTypeNode);
|
|
2450
2055
|
}
|
|
2451
|
-
if (
|
|
2056
|
+
if (import_ts_morph8.Node.isArrowFunction(node)) {
|
|
2452
2057
|
const body = node.getBody();
|
|
2453
|
-
if (
|
|
2058
|
+
if (import_ts_morph8.Node.isParenthesizedExpression(body)) {
|
|
2454
2059
|
const inner = body.getExpression();
|
|
2455
|
-
if (
|
|
2060
|
+
if (import_ts_morph8.Node.isObjectLiteralExpression(inner)) {
|
|
2456
2061
|
return extractFromObjectLiteral(inner);
|
|
2457
2062
|
}
|
|
2458
2063
|
}
|
|
2459
|
-
if (
|
|
2064
|
+
if (import_ts_morph8.Node.isObjectLiteralExpression(body)) {
|
|
2460
2065
|
return extractFromObjectLiteral(body);
|
|
2461
2066
|
}
|
|
2462
|
-
if (
|
|
2067
|
+
if (import_ts_morph8.Node.isBlock(body)) {
|
|
2463
2068
|
return extractFromBlockReturn(body);
|
|
2464
2069
|
}
|
|
2465
2070
|
}
|
|
2466
|
-
if (
|
|
2071
|
+
if (import_ts_morph8.Node.isFunctionExpression(node)) {
|
|
2467
2072
|
const body = node.getBody();
|
|
2468
|
-
if (
|
|
2073
|
+
if (import_ts_morph8.Node.isBlock(body)) {
|
|
2469
2074
|
return extractFromBlockReturn(body);
|
|
2470
2075
|
}
|
|
2471
2076
|
}
|
|
2472
2077
|
return null;
|
|
2473
2078
|
}
|
|
2474
2079
|
function extractFromReturnTypeAnnotation(typeNode) {
|
|
2475
|
-
if (
|
|
2080
|
+
if (import_ts_morph8.Node.isTypeReference(typeNode)) {
|
|
2476
2081
|
const typeName = typeNode.getTypeName();
|
|
2477
|
-
if (
|
|
2082
|
+
if (import_ts_morph8.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
|
|
2478
2083
|
const typeArgs = typeNode.getTypeArguments();
|
|
2479
|
-
const
|
|
2480
|
-
if (
|
|
2481
|
-
return extractFromReturnTypeAnnotation(
|
|
2084
|
+
const firstArg2 = typeArgs[0];
|
|
2085
|
+
if (firstArg2) {
|
|
2086
|
+
return extractFromReturnTypeAnnotation(firstArg2);
|
|
2482
2087
|
}
|
|
2483
2088
|
return null;
|
|
2484
2089
|
}
|
|
2485
2090
|
}
|
|
2486
|
-
if (
|
|
2091
|
+
if (import_ts_morph8.Node.isTypeLiteral(typeNode)) {
|
|
2487
2092
|
const properties = [];
|
|
2488
2093
|
for (const member of typeNode.getMembers()) {
|
|
2489
|
-
if (
|
|
2094
|
+
if (import_ts_morph8.Node.isPropertySignature(member)) {
|
|
2490
2095
|
const name = member.getName();
|
|
2491
2096
|
const memberTypeNode = member.getTypeNode();
|
|
2492
2097
|
const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
|
|
@@ -2500,19 +2105,19 @@ function extractFromReturnTypeAnnotation(typeNode) {
|
|
|
2500
2105
|
return null;
|
|
2501
2106
|
}
|
|
2502
2107
|
function extractFromBlockReturn(block) {
|
|
2503
|
-
if (!
|
|
2108
|
+
if (!import_ts_morph8.Node.isBlock(block)) return null;
|
|
2504
2109
|
const statements = block.getStatements();
|
|
2505
2110
|
for (let i = statements.length - 1; i >= 0; i--) {
|
|
2506
2111
|
const stmt = statements[i];
|
|
2507
|
-
if (!
|
|
2112
|
+
if (!import_ts_morph8.Node.isReturnStatement(stmt)) continue;
|
|
2508
2113
|
const expr = stmt.getExpression();
|
|
2509
2114
|
if (!expr) continue;
|
|
2510
|
-
if (
|
|
2115
|
+
if (import_ts_morph8.Node.isObjectLiteralExpression(expr)) {
|
|
2511
2116
|
return extractFromObjectLiteral(expr);
|
|
2512
2117
|
}
|
|
2513
|
-
if (
|
|
2118
|
+
if (import_ts_morph8.Node.isParenthesizedExpression(expr)) {
|
|
2514
2119
|
const inner = expr.getExpression();
|
|
2515
|
-
if (
|
|
2120
|
+
if (import_ts_morph8.Node.isObjectLiteralExpression(inner)) {
|
|
2516
2121
|
return extractFromObjectLiteral(inner);
|
|
2517
2122
|
}
|
|
2518
2123
|
}
|
|
@@ -2521,10 +2126,10 @@ function extractFromBlockReturn(block) {
|
|
|
2521
2126
|
return null;
|
|
2522
2127
|
}
|
|
2523
2128
|
function extractFromObjectLiteral(objLiteral) {
|
|
2524
|
-
if (!
|
|
2129
|
+
if (!import_ts_morph8.Node.isObjectLiteralExpression(objLiteral)) return null;
|
|
2525
2130
|
const properties = [];
|
|
2526
2131
|
for (const prop of objLiteral.getProperties()) {
|
|
2527
|
-
if (!
|
|
2132
|
+
if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
|
|
2528
2133
|
const name = prop.getName();
|
|
2529
2134
|
const initializer = prop.getInitializer();
|
|
2530
2135
|
if (!initializer) continue;
|
|
@@ -2536,21 +2141,21 @@ function extractFromObjectLiteral(objLiteral) {
|
|
|
2536
2141
|
return { typeString, properties, isImportRef: false };
|
|
2537
2142
|
}
|
|
2538
2143
|
function inferExpressionType(node) {
|
|
2539
|
-
if (
|
|
2540
|
-
if (
|
|
2144
|
+
if (import_ts_morph8.Node.isStringLiteral(node)) return "string";
|
|
2145
|
+
if (import_ts_morph8.Node.isTemplateExpression(node) || import_ts_morph8.Node.isNoSubstitutionTemplateLiteral(node))
|
|
2541
2146
|
return "string";
|
|
2542
|
-
if (
|
|
2543
|
-
if (node.getKind() ===
|
|
2147
|
+
if (import_ts_morph8.Node.isNumericLiteral(node)) return "number";
|
|
2148
|
+
if (node.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword || node.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) {
|
|
2544
2149
|
return "boolean";
|
|
2545
2150
|
}
|
|
2546
|
-
if (node.getKind() ===
|
|
2547
|
-
if (
|
|
2548
|
-
if (
|
|
2151
|
+
if (node.getKind() === import_ts_morph8.SyntaxKind.NullKeyword) return "null";
|
|
2152
|
+
if (import_ts_morph8.Node.isIdentifier(node) && node.getText() === "undefined") return "undefined";
|
|
2153
|
+
if (import_ts_morph8.Node.isObjectLiteralExpression(node)) {
|
|
2549
2154
|
const props = node.getProperties();
|
|
2550
2155
|
if (props.length === 0) return "Record<string, unknown>";
|
|
2551
2156
|
const entries = [];
|
|
2552
2157
|
for (const prop of props) {
|
|
2553
|
-
if (!
|
|
2158
|
+
if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
|
|
2554
2159
|
const key = prop.getName();
|
|
2555
2160
|
const init = prop.getInitializer();
|
|
2556
2161
|
if (!init) continue;
|
|
@@ -2559,23 +2164,23 @@ function inferExpressionType(node) {
|
|
|
2559
2164
|
if (entries.length === 0) return "Record<string, unknown>";
|
|
2560
2165
|
return `{ ${entries.join("; ")} }`;
|
|
2561
2166
|
}
|
|
2562
|
-
if (
|
|
2167
|
+
if (import_ts_morph8.Node.isArrayLiteralExpression(node)) {
|
|
2563
2168
|
const elements = node.getElements();
|
|
2564
2169
|
if (elements.length === 0) return "Array<unknown>";
|
|
2565
2170
|
const first = elements[0];
|
|
2566
2171
|
if (first) return `Array<${inferExpressionType(first)}>`;
|
|
2567
2172
|
return "Array<unknown>";
|
|
2568
2173
|
}
|
|
2569
|
-
if (
|
|
2174
|
+
if (import_ts_morph8.Node.isConditionalExpression(node)) {
|
|
2570
2175
|
const whenTrue = inferExpressionType(node.getWhenTrue());
|
|
2571
2176
|
const whenFalse = inferExpressionType(node.getWhenFalse());
|
|
2572
2177
|
if (whenTrue === whenFalse) return whenTrue;
|
|
2573
2178
|
return `${whenTrue} | ${whenFalse}`;
|
|
2574
2179
|
}
|
|
2575
|
-
if (
|
|
2180
|
+
if (import_ts_morph8.Node.isParenthesizedExpression(node)) {
|
|
2576
2181
|
return inferExpressionType(node.getExpression());
|
|
2577
2182
|
}
|
|
2578
|
-
if (
|
|
2183
|
+
if (import_ts_morph8.Node.isAsExpression(node)) {
|
|
2579
2184
|
const typeNode = node.getTypeNode();
|
|
2580
2185
|
if (typeNode) return typeNode.getText();
|
|
2581
2186
|
}
|
|
@@ -2584,25 +2189,14 @@ function inferExpressionType(node) {
|
|
|
2584
2189
|
|
|
2585
2190
|
// src/emit/emit-api.ts
|
|
2586
2191
|
var import_promises3 = require("fs/promises");
|
|
2587
|
-
var
|
|
2192
|
+
var import_node_path6 = require("path");
|
|
2588
2193
|
|
|
2589
2194
|
// src/extension/registry.ts
|
|
2590
|
-
var
|
|
2195
|
+
var import_ts_morph9 = require("ts-morph");
|
|
2591
2196
|
function resolveApiSlots(extensions) {
|
|
2592
|
-
let transport;
|
|
2593
|
-
let transportOwner;
|
|
2594
2197
|
let layer;
|
|
2595
2198
|
let layerOwner;
|
|
2596
2199
|
for (const ext of extensions) {
|
|
2597
|
-
if (ext.apiTransport) {
|
|
2598
|
-
if (transport) {
|
|
2599
|
-
throw new CodegenError(
|
|
2600
|
-
`api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
|
|
2601
|
-
);
|
|
2602
|
-
}
|
|
2603
|
-
transport = ext.apiTransport;
|
|
2604
|
-
transportOwner = ext.name;
|
|
2605
|
-
}
|
|
2606
2200
|
if (ext.apiClientLayer) {
|
|
2607
2201
|
if (layer) {
|
|
2608
2202
|
throw new CodegenError(
|
|
@@ -2614,11 +2208,22 @@ function resolveApiSlots(extensions) {
|
|
|
2614
2208
|
}
|
|
2615
2209
|
}
|
|
2616
2210
|
return {
|
|
2617
|
-
...transport ? { transport } : {},
|
|
2618
2211
|
...layer ? { layer } : {}
|
|
2619
2212
|
};
|
|
2620
2213
|
}
|
|
2621
2214
|
var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
|
|
2215
|
+
function mergeExclusive(target, incoming, {
|
|
2216
|
+
owner,
|
|
2217
|
+
describe
|
|
2218
|
+
}) {
|
|
2219
|
+
for (const [key, value] of incoming) {
|
|
2220
|
+
const prev = target.get(key);
|
|
2221
|
+
if (prev !== void 0) {
|
|
2222
|
+
throw new CodegenError(describe(key, prev.owner, owner));
|
|
2223
|
+
}
|
|
2224
|
+
target.set(key, { value, owner });
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2622
2227
|
function createExtensionContext(config, getRoutes) {
|
|
2623
2228
|
let project;
|
|
2624
2229
|
return {
|
|
@@ -2630,7 +2235,7 @@ function createExtensionContext(config, getRoutes) {
|
|
|
2630
2235
|
},
|
|
2631
2236
|
project() {
|
|
2632
2237
|
if (!project) {
|
|
2633
|
-
project = new
|
|
2238
|
+
project = new import_ts_morph9.Project({
|
|
2634
2239
|
skipAddingFilesFromTsConfig: true,
|
|
2635
2240
|
skipLoadingLibFiles: true,
|
|
2636
2241
|
skipFileDependencyResolution: true,
|
|
@@ -2663,29 +2268,36 @@ async function collectEmittedFiles(extensions, ctx) {
|
|
|
2663
2268
|
`Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
|
|
2664
2269
|
);
|
|
2665
2270
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
);
|
|
2671
|
-
}
|
|
2672
|
-
owners.set(key, ext.name);
|
|
2271
|
+
mergeExclusive(owners, [[key, file]], {
|
|
2272
|
+
owner: ext.name,
|
|
2273
|
+
describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
|
|
2274
|
+
});
|
|
2673
2275
|
files.push(file);
|
|
2674
2276
|
}
|
|
2675
2277
|
}
|
|
2676
2278
|
return files;
|
|
2677
2279
|
}
|
|
2678
2280
|
|
|
2281
|
+
// src/extension/types.ts
|
|
2282
|
+
function requestShape(route) {
|
|
2283
|
+
const cs = route.contract?.contractSource;
|
|
2284
|
+
const isGet = route.method.toUpperCase() === "GET";
|
|
2285
|
+
const isQuery = isGet || !!cs?.filterFields?.length;
|
|
2286
|
+
const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
|
|
2287
|
+
const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
|
|
2288
|
+
return { isGet, isQuery, hasBody, hasQuery };
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2679
2291
|
// src/emit/emit-api.ts
|
|
2680
2292
|
async function emitApi(routes, outDir, opts = {}) {
|
|
2681
2293
|
await (0, import_promises3.mkdir)(outDir, { recursive: true });
|
|
2682
2294
|
const content = buildApiFile(routes, outDir, opts);
|
|
2683
|
-
await (0, import_promises3.writeFile)((0,
|
|
2295
|
+
await (0, import_promises3.writeFile)((0, import_node_path6.join)(outDir, "api.ts"), content, "utf8");
|
|
2684
2296
|
}
|
|
2685
2297
|
function splitName(name) {
|
|
2686
2298
|
return name.split(".");
|
|
2687
2299
|
}
|
|
2688
|
-
function
|
|
2300
|
+
function toObjectKey(segment) {
|
|
2689
2301
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
|
|
2690
2302
|
return segment;
|
|
2691
2303
|
}
|
|
@@ -2778,7 +2390,7 @@ function emitFilterQueryType(c) {
|
|
|
2778
2390
|
}
|
|
2779
2391
|
function buildResponseType(c, outDir) {
|
|
2780
2392
|
if (c.controllerRef) {
|
|
2781
|
-
let relPath = (0,
|
|
2393
|
+
let relPath = (0, import_node_path6.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
|
|
2782
2394
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
2783
2395
|
return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
|
|
2784
2396
|
}
|
|
@@ -2792,7 +2404,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2792
2404
|
const pad = " ".repeat(indent);
|
|
2793
2405
|
const lines = [];
|
|
2794
2406
|
for (const [key, node] of tree) {
|
|
2795
|
-
const objKey =
|
|
2407
|
+
const objKey = toObjectKey(key);
|
|
2796
2408
|
if (node.kind === "leaf") {
|
|
2797
2409
|
const c = node;
|
|
2798
2410
|
const method = c.method.toUpperCase();
|
|
@@ -2818,15 +2430,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
2818
2430
|
return lines;
|
|
2819
2431
|
}
|
|
2820
2432
|
function buildRequestModel(c) {
|
|
2821
|
-
const isGet = c.method.toUpperCase() === "GET";
|
|
2822
2433
|
const m = c.method.toLowerCase();
|
|
2823
2434
|
const flat = JSON.stringify(c.name);
|
|
2824
2435
|
const path = JSON.stringify(c.path);
|
|
2825
2436
|
const TA = buildRouterTypeAccess(c.name);
|
|
2826
2437
|
const withParams = hasPathParams(c.params);
|
|
2827
|
-
const
|
|
2828
|
-
const isQuery = isGet || !!c.contractSource.filterFields?.length;
|
|
2829
|
-
const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
|
|
2438
|
+
const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
|
|
2830
2439
|
const fields = [];
|
|
2831
2440
|
if (withParams) fields.push(`params: ${TA}['params']`);
|
|
2832
2441
|
if (hasQuery) fields.push(`query?: ${TA}['query']`);
|
|
@@ -2848,7 +2457,6 @@ function buildRequestModel(c) {
|
|
|
2848
2457
|
urlExpr,
|
|
2849
2458
|
optsExpr,
|
|
2850
2459
|
responseType: `${TA}['response']`,
|
|
2851
|
-
bodyType: `${TA}['body']`,
|
|
2852
2460
|
queryKeyExpr: `[${flat}, input] as const`
|
|
2853
2461
|
};
|
|
2854
2462
|
}
|
|
@@ -2896,7 +2504,7 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2896
2504
|
const pad = " ".repeat(indent);
|
|
2897
2505
|
const lines = [];
|
|
2898
2506
|
for (const [key, node] of tree) {
|
|
2899
|
-
const objKey =
|
|
2507
|
+
const objKey = toObjectKey(key);
|
|
2900
2508
|
if (node.kind === "branch") {
|
|
2901
2509
|
lines.push(`${pad}${objKey}: {`);
|
|
2902
2510
|
lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
|
|
@@ -2904,30 +2512,28 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
2904
2512
|
continue;
|
|
2905
2513
|
}
|
|
2906
2514
|
const req = buildRequestModel(node);
|
|
2907
|
-
const
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
params: node.params,
|
|
2912
|
-
contract: { contractSource: node.contractSource },
|
|
2913
|
-
...node.controllerRef ? { controllerRef: node.controllerRef } : {}
|
|
2515
|
+
const leaf = {
|
|
2516
|
+
route: node.route,
|
|
2517
|
+
request: req,
|
|
2518
|
+
requestExpr: renderFetcherRequest(req)
|
|
2914
2519
|
};
|
|
2915
|
-
const
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2520
|
+
const owned = /* @__PURE__ */ new Map();
|
|
2521
|
+
if (p.layer) {
|
|
2522
|
+
mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
|
|
2523
|
+
owner: p.layer.name,
|
|
2524
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2919
2527
|
for (const ext of p.memberExts) {
|
|
2920
2528
|
const extra = ext.apiMembers?.(leaf, p.ctx);
|
|
2921
2529
|
if (!extra) continue;
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
);
|
|
2927
|
-
}
|
|
2928
|
-
members[name] = value;
|
|
2929
|
-
}
|
|
2530
|
+
mergeExclusive(owned, Object.entries(extra), {
|
|
2531
|
+
owner: ext.name,
|
|
2532
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
2533
|
+
});
|
|
2930
2534
|
}
|
|
2535
|
+
const members = {};
|
|
2536
|
+
for (const [name, { value }] of owned) members[name] = value;
|
|
2931
2537
|
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
|
|
2932
2538
|
}
|
|
2933
2539
|
return lines;
|
|
@@ -2936,10 +2542,82 @@ function buildRouterTypeAccess(name) {
|
|
|
2936
2542
|
const segments = splitName(name);
|
|
2937
2543
|
return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
|
|
2938
2544
|
}
|
|
2545
|
+
var RESOLVER_HELPERS = [
|
|
2546
|
+
// --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
|
|
2547
|
+
"type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
|
|
2548
|
+
" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
|
|
2549
|
+
" : P extends keyof R ? R[P] : never;",
|
|
2550
|
+
"",
|
|
2551
|
+
// --- ResolveByName: resolve a field from a dot-path name ---
|
|
2552
|
+
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
|
|
2553
|
+
"",
|
|
2554
|
+
// --- ResolveByPath: scan all leaves for matching method + url ---
|
|
2555
|
+
// Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
|
|
2556
|
+
"type _LeafValues<T> = T extends { method: string; url: string }",
|
|
2557
|
+
" ? T",
|
|
2558
|
+
" : T extends object ? _LeafValues<T[keyof T]> : never;",
|
|
2559
|
+
"",
|
|
2560
|
+
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
|
|
2561
|
+
" ? L extends { method: M; url: U }",
|
|
2562
|
+
" ? Field extends keyof L ? L[Field] : never",
|
|
2563
|
+
" : never",
|
|
2564
|
+
" : never;",
|
|
2565
|
+
""
|
|
2566
|
+
];
|
|
2567
|
+
var ROUTE_NAMESPACE = [
|
|
2568
|
+
"export namespace Route {",
|
|
2569
|
+
' export type Response<K extends string> = ResolveByName<K, "response">;',
|
|
2570
|
+
' export type Body<K extends string> = ResolveByName<K, "body">;',
|
|
2571
|
+
' export type Query<K extends string> = ResolveByName<K, "query">;',
|
|
2572
|
+
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
2573
|
+
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
2574
|
+
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
2575
|
+
" export type Request<K extends string> = {",
|
|
2576
|
+
" body: Body<K>;",
|
|
2577
|
+
" query: Query<K>;",
|
|
2578
|
+
" params: Params<K>;",
|
|
2579
|
+
" };",
|
|
2580
|
+
"}",
|
|
2581
|
+
""
|
|
2582
|
+
];
|
|
2583
|
+
var PATH_NAMESPACE = [
|
|
2584
|
+
"export namespace Path {",
|
|
2585
|
+
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
2586
|
+
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
|
|
2587
|
+
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
|
|
2588
|
+
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
2589
|
+
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
2590
|
+
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
2591
|
+
"}",
|
|
2592
|
+
""
|
|
2593
|
+
];
|
|
2594
|
+
var EMPTY_ROUTE_NAMESPACE = [
|
|
2595
|
+
"export namespace Route {",
|
|
2596
|
+
" export type Response<K extends string> = never;",
|
|
2597
|
+
" export type Body<K extends string> = never;",
|
|
2598
|
+
" export type Query<K extends string> = never;",
|
|
2599
|
+
" export type Params<K extends string> = never;",
|
|
2600
|
+
" export type Error<K extends string> = never;",
|
|
2601
|
+
" export type FilterFields<K extends string> = never;",
|
|
2602
|
+
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
2603
|
+
"}",
|
|
2604
|
+
""
|
|
2605
|
+
];
|
|
2606
|
+
var EMPTY_PATH_NAMESPACE = [
|
|
2607
|
+
"export namespace Path {",
|
|
2608
|
+
" export type Response<M extends string, U extends string> = never;",
|
|
2609
|
+
" export type Body<M extends string, U extends string> = never;",
|
|
2610
|
+
" export type Query<M extends string, U extends string> = never;",
|
|
2611
|
+
" export type Params<M extends string, U extends string> = never;",
|
|
2612
|
+
" export type Error<M extends string, U extends string> = never;",
|
|
2613
|
+
" export type FilterFields<M extends string, U extends string> = never;",
|
|
2614
|
+
"}",
|
|
2615
|
+
""
|
|
2616
|
+
];
|
|
2939
2617
|
function buildApiFile(routes, outDir, opts = {}) {
|
|
2940
2618
|
const fetcherImportPath = opts.fetcherImportPath;
|
|
2941
2619
|
const extensions = opts.extensions ?? [];
|
|
2942
|
-
const {
|
|
2620
|
+
const { layer } = resolveApiSlots(extensions);
|
|
2943
2621
|
const memberExts = extensions.filter((e) => e.apiMembers);
|
|
2944
2622
|
const headerExts = extensions.filter((e) => e.apiHeader);
|
|
2945
2623
|
const contracted = routes.filter((r) => r.contract);
|
|
@@ -2984,7 +2662,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
2984
2662
|
seenImports.add(imp);
|
|
2985
2663
|
extImports.push(imp);
|
|
2986
2664
|
};
|
|
2987
|
-
for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
|
|
2988
2665
|
for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
|
|
2989
2666
|
for (const ext of headerExts) {
|
|
2990
2667
|
for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
|
|
@@ -3000,8 +2677,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3000
2677
|
const emittedNames = /* @__PURE__ */ new Set();
|
|
3001
2678
|
for (const [filePath, names] of importsByFile) {
|
|
3002
2679
|
let relPath;
|
|
3003
|
-
if ((0,
|
|
3004
|
-
relPath = (0,
|
|
2680
|
+
if ((0, import_node_path6.isAbsolute)(filePath)) {
|
|
2681
|
+
relPath = (0, import_node_path6.relative)(outDir, filePath).replace(/\.ts$/, "");
|
|
3005
2682
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
3006
2683
|
} else {
|
|
3007
2684
|
relPath = filePath;
|
|
@@ -3029,27 +2706,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3029
2706
|
lines.push("}");
|
|
3030
2707
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
3031
2708
|
lines.push("");
|
|
3032
|
-
lines.push(
|
|
3033
|
-
lines.push(
|
|
3034
|
-
lines.push(" export type Body<K extends string> = never;");
|
|
3035
|
-
lines.push(" export type Query<K extends string> = never;");
|
|
3036
|
-
lines.push(" export type Params<K extends string> = never;");
|
|
3037
|
-
lines.push(" export type Error<K extends string> = never;");
|
|
3038
|
-
lines.push(" export type FilterFields<K extends string> = never;");
|
|
3039
|
-
lines.push(
|
|
3040
|
-
" export type Request<K extends string> = { body: never; query: never; params: never };"
|
|
3041
|
-
);
|
|
3042
|
-
lines.push("}");
|
|
3043
|
-
lines.push("");
|
|
3044
|
-
lines.push("export namespace Path {");
|
|
3045
|
-
lines.push(" export type Response<M extends string, U extends string> = never;");
|
|
3046
|
-
lines.push(" export type Body<M extends string, U extends string> = never;");
|
|
3047
|
-
lines.push(" export type Query<M extends string, U extends string> = never;");
|
|
3048
|
-
lines.push(" export type Params<M extends string, U extends string> = never;");
|
|
3049
|
-
lines.push(" export type Error<M extends string, U extends string> = never;");
|
|
3050
|
-
lines.push(" export type FilterFields<M extends string, U extends string> = never;");
|
|
3051
|
-
lines.push("}");
|
|
3052
|
-
lines.push("");
|
|
2709
|
+
lines.push(...EMPTY_ROUTE_NAMESPACE);
|
|
2710
|
+
lines.push(...EMPTY_PATH_NAMESPACE);
|
|
3053
2711
|
return lines.join("\n");
|
|
3054
2712
|
}
|
|
3055
2713
|
const tree = /* @__PURE__ */ new Map();
|
|
@@ -3067,7 +2725,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3067
2725
|
path: r.path,
|
|
3068
2726
|
params: r.params,
|
|
3069
2727
|
controllerRef: r.controllerRef,
|
|
3070
|
-
contractSource: c.contractSource
|
|
2728
|
+
contractSource: c.contractSource,
|
|
2729
|
+
route: r
|
|
3071
2730
|
};
|
|
3072
2731
|
insertIntoTree(tree, segments, leaf, name);
|
|
3073
2732
|
}
|
|
@@ -3080,7 +2739,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3080
2739
|
lines.push(" return {");
|
|
3081
2740
|
lines.push(
|
|
3082
2741
|
...emitApiObjectBlock(tree, 4, {
|
|
3083
|
-
...transport ? { transport } : {},
|
|
3084
2742
|
...layer ? { layer } : {},
|
|
3085
2743
|
memberExts,
|
|
3086
2744
|
ctx
|
|
@@ -3091,61 +2749,9 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3091
2749
|
lines.push("");
|
|
3092
2750
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
3093
2751
|
lines.push("");
|
|
3094
|
-
lines.push(
|
|
3095
|
-
lines.push(
|
|
3096
|
-
lines.push(
|
|
3097
|
-
lines.push("");
|
|
3098
|
-
lines.push(
|
|
3099
|
-
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
|
|
3100
|
-
);
|
|
3101
|
-
lines.push("");
|
|
3102
|
-
lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
|
|
3103
|
-
lines.push(" ? T");
|
|
3104
|
-
lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
|
|
3105
|
-
lines.push("");
|
|
3106
|
-
lines.push(
|
|
3107
|
-
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
|
|
3108
|
-
);
|
|
3109
|
-
lines.push(" ? L extends { method: M; url: U }");
|
|
3110
|
-
lines.push(" ? Field extends keyof L ? L[Field] : never");
|
|
3111
|
-
lines.push(" : never");
|
|
3112
|
-
lines.push(" : never;");
|
|
3113
|
-
lines.push("");
|
|
3114
|
-
lines.push("export namespace Route {");
|
|
3115
|
-
lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
|
|
3116
|
-
lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
|
|
3117
|
-
lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
|
|
3118
|
-
lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
|
|
3119
|
-
lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
|
|
3120
|
-
lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
|
|
3121
|
-
lines.push(" export type Request<K extends string> = {");
|
|
3122
|
-
lines.push(" body: Body<K>;");
|
|
3123
|
-
lines.push(" query: Query<K>;");
|
|
3124
|
-
lines.push(" params: Params<K>;");
|
|
3125
|
-
lines.push(" };");
|
|
3126
|
-
lines.push("}");
|
|
3127
|
-
lines.push("");
|
|
3128
|
-
lines.push("export namespace Path {");
|
|
3129
|
-
lines.push(
|
|
3130
|
-
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
|
|
3131
|
-
);
|
|
3132
|
-
lines.push(
|
|
3133
|
-
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
|
|
3134
|
-
);
|
|
3135
|
-
lines.push(
|
|
3136
|
-
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
|
|
3137
|
-
);
|
|
3138
|
-
lines.push(
|
|
3139
|
-
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
|
|
3140
|
-
);
|
|
3141
|
-
lines.push(
|
|
3142
|
-
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
|
|
3143
|
-
);
|
|
3144
|
-
lines.push(
|
|
3145
|
-
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
|
|
3146
|
-
);
|
|
3147
|
-
lines.push("}");
|
|
3148
|
-
lines.push("");
|
|
2752
|
+
lines.push(...RESOLVER_HELPERS);
|
|
2753
|
+
lines.push(...ROUTE_NAMESPACE);
|
|
2754
|
+
lines.push(...PATH_NAMESPACE);
|
|
3149
2755
|
for (const ext of headerExts) {
|
|
3150
2756
|
const statements = ext.apiHeader?.(ctx)?.statements;
|
|
3151
2757
|
if (statements?.length) {
|
|
@@ -3157,7 +2763,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
3157
2763
|
|
|
3158
2764
|
// src/emit/emit-cache.ts
|
|
3159
2765
|
var import_promises4 = require("fs/promises");
|
|
3160
|
-
var
|
|
2766
|
+
var import_node_path7 = require("path");
|
|
3161
2767
|
async function emitCache(pages, outDir) {
|
|
3162
2768
|
await (0, import_promises4.mkdir)(outDir, { recursive: true });
|
|
3163
2769
|
const entries = await Promise.all(
|
|
@@ -3171,95 +2777,21 @@ async function emitCache(pages, outDir) {
|
|
|
3171
2777
|
})
|
|
3172
2778
|
);
|
|
3173
2779
|
const cache = { pages: entries };
|
|
3174
|
-
await (0, import_promises4.writeFile)((0,
|
|
2780
|
+
await (0, import_promises4.writeFile)((0, import_node_path7.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
|
|
3175
2781
|
`, "utf8");
|
|
3176
2782
|
}
|
|
3177
2783
|
|
|
3178
2784
|
// src/emit/emit-forms.ts
|
|
3179
2785
|
var import_promises5 = require("fs/promises");
|
|
3180
|
-
var
|
|
2786
|
+
var import_node_path8 = require("path");
|
|
3181
2787
|
async function emitForms(routes, outDir, config, adapter) {
|
|
3182
2788
|
if (config && config.enabled === false) return false;
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
if (content2 === null) return false;
|
|
3186
|
-
await (0, import_promises5.mkdir)(outDir, { recursive: true });
|
|
3187
|
-
await (0, import_promises5.writeFile)((0, import_node_path7.join)(outDir, "forms.ts"), content2, "utf8");
|
|
3188
|
-
return true;
|
|
3189
|
-
}
|
|
3190
|
-
const entries = collectFormEntries(routes);
|
|
3191
|
-
if (entries.length === 0) return false;
|
|
2789
|
+
const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
|
|
2790
|
+
if (content === null) return false;
|
|
3192
2791
|
await (0, import_promises5.mkdir)(outDir, { recursive: true });
|
|
3193
|
-
|
|
3194
|
-
await (0, import_promises5.writeFile)((0, import_node_path7.join)(outDir, "forms.ts"), content, "utf8");
|
|
2792
|
+
await (0, import_promises5.writeFile)((0, import_node_path8.join)(outDir, "forms.ts"), content, "utf8");
|
|
3195
2793
|
return true;
|
|
3196
2794
|
}
|
|
3197
|
-
function buildFormsFileWithAdapter(routes, adapter) {
|
|
3198
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
3199
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
3200
|
-
for (const route of sorted) {
|
|
3201
|
-
const cs = route.contract.contractSource;
|
|
3202
|
-
if (!cs.bodySchema && !cs.querySchema) continue;
|
|
3203
|
-
methodNameCounts.set(
|
|
3204
|
-
deriveBaseName(route.name).method,
|
|
3205
|
-
(methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
|
|
3206
|
-
);
|
|
3207
|
-
}
|
|
3208
|
-
const named = /* @__PURE__ */ new Map();
|
|
3209
|
-
const decls = [];
|
|
3210
|
-
const mapEntries = [];
|
|
3211
|
-
let used = false;
|
|
3212
|
-
for (const route of sorted) {
|
|
3213
|
-
const cs = route.contract.contractSource;
|
|
3214
|
-
const { method, full } = deriveBaseName(route.name);
|
|
3215
|
-
const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
|
|
3216
|
-
const block = [];
|
|
3217
|
-
if (cs.bodyZodText && !cs.bodySchema) {
|
|
3218
|
-
block.push(
|
|
3219
|
-
`// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
3220
|
-
);
|
|
3221
|
-
}
|
|
3222
|
-
let bodyConst;
|
|
3223
|
-
if (cs.bodySchema) {
|
|
3224
|
-
used = true;
|
|
3225
|
-
const r = adapter.renderModule(cs.bodySchema);
|
|
3226
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
3227
|
-
bodyConst = `${base}BodySchema`;
|
|
3228
|
-
block.push(`export const ${bodyConst} = ${r.schemaText};`);
|
|
3229
|
-
block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
|
|
3230
|
-
}
|
|
3231
|
-
if (cs.querySchema) {
|
|
3232
|
-
used = true;
|
|
3233
|
-
const r = adapter.renderModule(cs.querySchema);
|
|
3234
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
3235
|
-
const queryConst = `${base}QuerySchema`;
|
|
3236
|
-
block.push(`export const ${queryConst} = ${r.schemaText};`);
|
|
3237
|
-
block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
|
|
3238
|
-
}
|
|
3239
|
-
if (block.length === 0) continue;
|
|
3240
|
-
decls.push(`// ${route.name}`, ...block, "");
|
|
3241
|
-
if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
|
|
3242
|
-
}
|
|
3243
|
-
if (!used) return null;
|
|
3244
|
-
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
3245
|
-
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
3246
|
-
lines.push("");
|
|
3247
|
-
if (named.size > 0) {
|
|
3248
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
3249
|
-
for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
|
|
3250
|
-
lines.push("");
|
|
3251
|
-
}
|
|
3252
|
-
lines.push(...decls);
|
|
3253
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
3254
|
-
lines.push("export const formSchemas = {");
|
|
3255
|
-
lines.push(...mapEntries);
|
|
3256
|
-
lines.push("} as const;");
|
|
3257
|
-
lines.push("");
|
|
3258
|
-
return lines.join("\n");
|
|
3259
|
-
}
|
|
3260
|
-
function hasSchema(src) {
|
|
3261
|
-
return !!src && (src.ref !== null || src.text !== null);
|
|
3262
|
-
}
|
|
3263
2795
|
function pascal(segment) {
|
|
3264
2796
|
return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
3265
2797
|
}
|
|
@@ -3269,124 +2801,16 @@ function deriveBaseName(routeName) {
|
|
|
3269
2801
|
const full = segments.map(pascal).join("");
|
|
3270
2802
|
return { method, full };
|
|
3271
2803
|
}
|
|
3272
|
-
function collectFormEntries(routes) {
|
|
3273
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
3274
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
3275
|
-
const candidates = [];
|
|
3276
|
-
for (const route of sorted) {
|
|
3277
|
-
const cs = route.contract.contractSource;
|
|
3278
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
3279
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
3280
|
-
if (!hasSchema(body) && !hasSchema(query)) continue;
|
|
3281
|
-
const { method, full } = deriveBaseName(route.name);
|
|
3282
|
-
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
3283
|
-
candidates.push({ route, method, full });
|
|
3284
|
-
}
|
|
3285
|
-
const entries = [];
|
|
3286
|
-
for (const { route, method, full } of candidates) {
|
|
3287
|
-
const cs = route.contract.contractSource;
|
|
3288
|
-
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
3289
|
-
const baseName = collision ? full : method;
|
|
3290
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
3291
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
3292
|
-
entries.push({
|
|
3293
|
-
routeName: route.name,
|
|
3294
|
-
baseName,
|
|
3295
|
-
body: hasSchema(body) ? body : void 0,
|
|
3296
|
-
query: hasSchema(query) ? query : void 0,
|
|
3297
|
-
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
3298
|
-
warnings: cs.formWarnings ?? []
|
|
3299
|
-
});
|
|
3300
|
-
}
|
|
3301
|
-
return entries;
|
|
3302
|
-
}
|
|
3303
2804
|
function relImport(outDir, filePath) {
|
|
3304
|
-
let relPath = (0,
|
|
2805
|
+
let relPath = (0, import_node_path8.relative)(outDir, filePath).replace(/\.ts$/, "");
|
|
3305
2806
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
3306
2807
|
return relPath;
|
|
3307
2808
|
}
|
|
3308
2809
|
function refRootIdentifier(refName) {
|
|
3309
2810
|
return refName.split(".")[0] ?? refName;
|
|
3310
2811
|
}
|
|
3311
|
-
function
|
|
3312
|
-
|
|
3313
|
-
const lines = [
|
|
3314
|
-
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
3315
|
-
`import { z } from '${zodImport}';`
|
|
3316
|
-
];
|
|
3317
|
-
const importsByFile = /* @__PURE__ */ new Map();
|
|
3318
|
-
const refAlias = /* @__PURE__ */ new Map();
|
|
3319
|
-
for (const entry of entries) {
|
|
3320
|
-
for (const src of [entry.body, entry.query]) {
|
|
3321
|
-
if (src?.ref && !src.text) {
|
|
3322
|
-
const root = refRootIdentifier(src.ref.name);
|
|
3323
|
-
const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
|
|
3324
|
-
set.add(root);
|
|
3325
|
-
importsByFile.set(src.ref.filePath, set);
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
}
|
|
3329
|
-
if (importsByFile.size > 0) {
|
|
3330
|
-
const emitted = /* @__PURE__ */ new Set();
|
|
3331
|
-
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
3332
|
-
const relPath = relImport(outDir, filePath);
|
|
3333
|
-
const specifiers = [];
|
|
3334
|
-
for (const root of [...roots].sort()) {
|
|
3335
|
-
if (emitted.has(root)) {
|
|
3336
|
-
const alias = `${root}_${emitted.size}`;
|
|
3337
|
-
specifiers.push(`${root} as ${alias}`);
|
|
3338
|
-
emitted.add(alias);
|
|
3339
|
-
refAlias.set(`${filePath}\0${root}`, alias);
|
|
3340
|
-
} else {
|
|
3341
|
-
specifiers.push(root);
|
|
3342
|
-
emitted.add(root);
|
|
3343
|
-
refAlias.set(`${filePath}\0${root}`, root);
|
|
3344
|
-
}
|
|
3345
|
-
}
|
|
3346
|
-
lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
3347
|
-
}
|
|
3348
|
-
}
|
|
3349
|
-
lines.push("");
|
|
3350
|
-
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
3351
|
-
if (globalSchemas.size > 0) {
|
|
3352
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
3353
|
-
for (const [name, text] of globalSchemas) {
|
|
3354
|
-
lines.push(`const ${name} = ${text};`);
|
|
3355
|
-
}
|
|
3356
|
-
lines.push("");
|
|
3357
|
-
}
|
|
3358
|
-
const mapEntries = [];
|
|
3359
|
-
for (const entry of entries) {
|
|
3360
|
-
lines.push(`// ${entry.routeName}`);
|
|
3361
|
-
if (entry.warnings && entry.warnings.length > 0) {
|
|
3362
|
-
for (const w of entry.warnings) {
|
|
3363
|
-
lines.push(`// warning: ${w}`);
|
|
3364
|
-
}
|
|
3365
|
-
}
|
|
3366
|
-
const rename = renamesByEntry.get(entry) ?? null;
|
|
3367
|
-
if (entry.body) {
|
|
3368
|
-
const schemaName = `${entry.baseName}BodySchema`;
|
|
3369
|
-
const typeName = `${entry.baseName}Body`;
|
|
3370
|
-
const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
|
|
3371
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
3372
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
3373
|
-
mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
|
|
3374
|
-
}
|
|
3375
|
-
if (entry.query) {
|
|
3376
|
-
const schemaName = `${entry.baseName}QuerySchema`;
|
|
3377
|
-
const typeName = `${entry.baseName}Query`;
|
|
3378
|
-
const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
|
|
3379
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
3380
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
3381
|
-
}
|
|
3382
|
-
lines.push("");
|
|
3383
|
-
}
|
|
3384
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
3385
|
-
lines.push("export const formSchemas = {");
|
|
3386
|
-
lines.push(...mapEntries);
|
|
3387
|
-
lines.push("} as const;");
|
|
3388
|
-
lines.push("");
|
|
3389
|
-
return lines.join("\n");
|
|
2812
|
+
function hasSource(src) {
|
|
2813
|
+
return !!(src.schema || src.zodText || src.zodRef);
|
|
3390
2814
|
}
|
|
3391
2815
|
function applyRenames(text, renames) {
|
|
3392
2816
|
if (!renames || renames.size === 0) return text;
|
|
@@ -3452,20 +2876,172 @@ function planNestedSchemas(entries) {
|
|
|
3452
2876
|
}
|
|
3453
2877
|
return { globalSchemas, renamesByEntry };
|
|
3454
2878
|
}
|
|
3455
|
-
function
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
2879
|
+
function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
|
|
2880
|
+
const acceptsRawZod = adapter.acceptsRawZodSource === true;
|
|
2881
|
+
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
2882
|
+
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
2883
|
+
const candidates = [];
|
|
2884
|
+
for (const route of sorted) {
|
|
2885
|
+
const cs = route.contract.contractSource;
|
|
2886
|
+
const body = {
|
|
2887
|
+
schema: cs.bodySchema ?? null,
|
|
2888
|
+
zodText: cs.bodyZodText ?? null,
|
|
2889
|
+
zodRef: cs.bodyZodRef ?? null
|
|
2890
|
+
};
|
|
2891
|
+
const query = {
|
|
2892
|
+
schema: cs.querySchema ?? null,
|
|
2893
|
+
zodText: cs.queryZodText ?? null,
|
|
2894
|
+
zodRef: cs.queryZodRef ?? null
|
|
2895
|
+
};
|
|
2896
|
+
if (!hasSource(body) && !hasSource(query)) continue;
|
|
2897
|
+
const { method, full } = deriveBaseName(route.name);
|
|
2898
|
+
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
2899
|
+
candidates.push({
|
|
2900
|
+
routeName: route.name,
|
|
2901
|
+
baseName: full,
|
|
2902
|
+
// resolved below
|
|
2903
|
+
body: hasSource(body) ? body : void 0,
|
|
2904
|
+
query: hasSource(query) ? query : void 0,
|
|
2905
|
+
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
2906
|
+
warnings: cs.formWarnings ?? []
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
const entries = candidates.map((c) => {
|
|
2910
|
+
const { method, full } = deriveBaseName(c.routeName);
|
|
2911
|
+
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
2912
|
+
return { ...c, baseName: collision ? full : method };
|
|
2913
|
+
});
|
|
2914
|
+
if (entries.length === 0) return null;
|
|
2915
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
2916
|
+
const refAlias = /* @__PURE__ */ new Map();
|
|
2917
|
+
for (const entry of entries) {
|
|
2918
|
+
for (const src of [entry.body, entry.query]) {
|
|
2919
|
+
if (src?.zodRef && !src.zodText && !src.schema) {
|
|
2920
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
2921
|
+
const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
|
|
2922
|
+
set.add(root);
|
|
2923
|
+
importsByFile.set(src.zodRef.filePath, set);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
const importLines = [];
|
|
2928
|
+
if (importsByFile.size > 0) {
|
|
2929
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2930
|
+
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
2931
|
+
const relPath = relImport(outDir, filePath);
|
|
2932
|
+
const specifiers = [];
|
|
2933
|
+
for (const root of [...roots].sort()) {
|
|
2934
|
+
if (emitted.has(root)) {
|
|
2935
|
+
const alias = `${root}_${emitted.size}`;
|
|
2936
|
+
specifiers.push(`${root} as ${alias}`);
|
|
2937
|
+
emitted.add(alias);
|
|
2938
|
+
refAlias.set(`${filePath}\0${root}`, alias);
|
|
2939
|
+
} else {
|
|
2940
|
+
specifiers.push(root);
|
|
2941
|
+
emitted.add(root);
|
|
2942
|
+
refAlias.set(`${filePath}\0${root}`, root);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
2949
|
+
const irNamed = /* @__PURE__ */ new Map();
|
|
2950
|
+
const decls = [];
|
|
2951
|
+
const mapEntries = [];
|
|
2952
|
+
let used = false;
|
|
2953
|
+
const renderSource = (src, rename) => {
|
|
2954
|
+
if (src.schema) {
|
|
2955
|
+
const r = adapter.renderModule(src.schema);
|
|
2956
|
+
for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
|
|
2957
|
+
return { text: r.schemaText };
|
|
2958
|
+
}
|
|
2959
|
+
if (src.zodText) {
|
|
2960
|
+
if (!acceptsRawZod) {
|
|
2961
|
+
return {
|
|
2962
|
+
text: "",
|
|
2963
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
return { text: applyRenames(src.zodText, rename) };
|
|
2967
|
+
}
|
|
2968
|
+
if (src.zodRef) {
|
|
2969
|
+
if (!acceptsRawZod) {
|
|
2970
|
+
return {
|
|
2971
|
+
text: "",
|
|
2972
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
2976
|
+
const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
|
|
2977
|
+
const member = src.zodRef.name.slice(root.length);
|
|
2978
|
+
return { text: `${alias}${member}` };
|
|
2979
|
+
}
|
|
2980
|
+
return null;
|
|
2981
|
+
};
|
|
2982
|
+
for (const entry of entries) {
|
|
2983
|
+
const block = [];
|
|
2984
|
+
const rename = renamesByEntry.get(entry) ?? null;
|
|
2985
|
+
let bodyConst;
|
|
2986
|
+
if (entry.warnings && entry.warnings.length > 0) {
|
|
2987
|
+
for (const w of entry.warnings) block.push(`// warning: ${w}`);
|
|
2988
|
+
}
|
|
2989
|
+
if (entry.body) {
|
|
2990
|
+
const rendered = renderSource(entry.body, rename);
|
|
2991
|
+
if (rendered?.warn) {
|
|
2992
|
+
block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
|
|
2993
|
+
} else if (rendered) {
|
|
2994
|
+
used = true;
|
|
2995
|
+
bodyConst = `${entry.baseName}BodySchema`;
|
|
2996
|
+
block.push(`export const ${bodyConst} = ${rendered.text};`);
|
|
2997
|
+
block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
if (entry.query) {
|
|
3001
|
+
const rendered = renderSource(entry.query, rename);
|
|
3002
|
+
if (rendered?.warn) {
|
|
3003
|
+
block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
|
|
3004
|
+
} else if (rendered) {
|
|
3005
|
+
used = true;
|
|
3006
|
+
const queryConst = `${entry.baseName}QuerySchema`;
|
|
3007
|
+
block.push(`export const ${queryConst} = ${rendered.text};`);
|
|
3008
|
+
block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
if (block.length === 0) continue;
|
|
3012
|
+
decls.push(`// ${entry.routeName}`, ...block, "");
|
|
3013
|
+
if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
|
|
3014
|
+
}
|
|
3015
|
+
if (!used) return null;
|
|
3016
|
+
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
3017
|
+
if (acceptsRawZod) {
|
|
3018
|
+
const zodImport = config?.zodImport ?? "zod";
|
|
3019
|
+
lines.push(`import { z } from '${zodImport}';`);
|
|
3020
|
+
} else {
|
|
3021
|
+
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
3022
|
+
}
|
|
3023
|
+
lines.push(...importLines);
|
|
3024
|
+
lines.push("");
|
|
3025
|
+
const allNested = /* @__PURE__ */ new Map();
|
|
3026
|
+
for (const [n, t] of globalSchemas) allNested.set(n, t);
|
|
3027
|
+
for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
|
|
3028
|
+
if (allNested.size > 0) {
|
|
3029
|
+
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
3030
|
+
for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
|
|
3031
|
+
lines.push("");
|
|
3462
3032
|
}
|
|
3463
|
-
|
|
3033
|
+
lines.push(...decls);
|
|
3034
|
+
lines.push("/** Route name \u2192 body schema map. */");
|
|
3035
|
+
lines.push("export const formSchemas = {");
|
|
3036
|
+
lines.push(...mapEntries);
|
|
3037
|
+
lines.push("} as const;");
|
|
3038
|
+
lines.push("");
|
|
3039
|
+
return lines.join("\n");
|
|
3464
3040
|
}
|
|
3465
3041
|
|
|
3466
3042
|
// src/emit/emit-index.ts
|
|
3467
3043
|
var import_promises6 = require("fs/promises");
|
|
3468
|
-
var
|
|
3044
|
+
var import_node_path9 = require("path");
|
|
3469
3045
|
async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
3470
3046
|
await (0, import_promises6.mkdir)(outDir, { recursive: true });
|
|
3471
3047
|
const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
|
|
@@ -3478,12 +3054,12 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
|
3478
3054
|
const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
|
|
3479
3055
|
"\n"
|
|
3480
3056
|
);
|
|
3481
|
-
await (0, import_promises6.writeFile)((0,
|
|
3057
|
+
await (0, import_promises6.writeFile)((0, import_node_path9.join)(outDir, "index.d.ts"), content, "utf8");
|
|
3482
3058
|
}
|
|
3483
3059
|
|
|
3484
3060
|
// src/emit/emit-pages.ts
|
|
3485
3061
|
var import_promises7 = require("fs/promises");
|
|
3486
|
-
var
|
|
3062
|
+
var import_node_path10 = require("path");
|
|
3487
3063
|
async function emitPages(pages, outDir, _options = {}) {
|
|
3488
3064
|
await (0, import_promises7.mkdir)(outDir, { recursive: true });
|
|
3489
3065
|
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
@@ -3504,7 +3080,7 @@ ${augBody}
|
|
|
3504
3080
|
}
|
|
3505
3081
|
${sharedPropsBlock}}
|
|
3506
3082
|
`;
|
|
3507
|
-
await (0, import_promises7.writeFile)((0,
|
|
3083
|
+
await (0, import_promises7.writeFile)((0, import_node_path10.join)(outDir, "pages.d.ts"), content, "utf8");
|
|
3508
3084
|
}
|
|
3509
3085
|
function buildSharedPropsBlock(sharedProps) {
|
|
3510
3086
|
if (!sharedProps) return "";
|
|
@@ -3523,7 +3099,7 @@ ${propsBody}
|
|
|
3523
3099
|
`;
|
|
3524
3100
|
}
|
|
3525
3101
|
function buildAugmentationType(page, outDir) {
|
|
3526
|
-
let importPath = (0,
|
|
3102
|
+
let importPath = (0, import_node_path10.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
|
|
3527
3103
|
if (!importPath.startsWith(".")) {
|
|
3528
3104
|
importPath = `./${importPath}`;
|
|
3529
3105
|
}
|
|
@@ -3535,11 +3111,11 @@ function needsQuotes(name) {
|
|
|
3535
3111
|
|
|
3536
3112
|
// src/emit/emit-routes.ts
|
|
3537
3113
|
var import_promises8 = require("fs/promises");
|
|
3538
|
-
var
|
|
3114
|
+
var import_node_path11 = require("path");
|
|
3539
3115
|
async function emitRoutes(routes, outDir) {
|
|
3540
3116
|
await (0, import_promises8.mkdir)(outDir, { recursive: true });
|
|
3541
3117
|
const content = buildRoutesFile(routes);
|
|
3542
|
-
await (0, import_promises8.writeFile)((0,
|
|
3118
|
+
await (0, import_promises8.writeFile)((0, import_node_path11.join)(outDir, "routes.ts"), content, "utf8");
|
|
3543
3119
|
}
|
|
3544
3120
|
function buildRoutesFile(routes) {
|
|
3545
3121
|
if (routes.length === 0) {
|
|
@@ -3667,30 +3243,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
3667
3243
|
propsExport: pagesConfig.propsExport,
|
|
3668
3244
|
componentNameStrategy: pagesConfig.componentNameStrategy
|
|
3669
3245
|
});
|
|
3670
|
-
|
|
3671
|
-
if (config.app?.moduleEntry) {
|
|
3672
|
-
try {
|
|
3673
|
-
const tsconfigPath = config.app.tsconfig ?? (0, import_node_path11.join)(config.codegen.cwd, "tsconfig.json");
|
|
3674
|
-
let project;
|
|
3675
|
-
try {
|
|
3676
|
-
project = new import_ts_morph9.Project({
|
|
3677
|
-
tsConfigFilePath: tsconfigPath,
|
|
3678
|
-
skipAddingFilesFromTsConfig: true,
|
|
3679
|
-
skipLoadingLibFiles: true,
|
|
3680
|
-
skipFileDependencyResolution: true
|
|
3681
|
-
});
|
|
3682
|
-
} catch {
|
|
3683
|
-
project = new import_ts_morph9.Project({
|
|
3684
|
-
skipAddingFilesFromTsConfig: true,
|
|
3685
|
-
skipLoadingLibFiles: true,
|
|
3686
|
-
skipFileDependencyResolution: true,
|
|
3687
|
-
compilerOptions: { allowJs: true, strict: false }
|
|
3688
|
-
});
|
|
3689
|
-
}
|
|
3690
|
-
sharedProps = discoverSharedProps(project, config.app.moduleEntry);
|
|
3691
|
-
} catch {
|
|
3692
|
-
}
|
|
3693
|
-
}
|
|
3246
|
+
const sharedProps = discoverSharedPropsFromConfig(config);
|
|
3694
3247
|
await emitPages(pages, config.codegen.outDir, {
|
|
3695
3248
|
propsExport: pagesConfig.propsExport,
|
|
3696
3249
|
sharedProps
|
|
@@ -3714,8 +3267,8 @@ async function generate(config, inputRoutes = []) {
|
|
|
3714
3267
|
if (extensions.length > 0) {
|
|
3715
3268
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
3716
3269
|
for (const file of extraFiles) {
|
|
3717
|
-
const dest = (0,
|
|
3718
|
-
await (0, import_promises9.mkdir)((0,
|
|
3270
|
+
const dest = (0, import_node_path12.join)(config.codegen.outDir, file.path);
|
|
3271
|
+
await (0, import_promises9.mkdir)((0, import_node_path12.dirname)(dest), { recursive: true });
|
|
3719
3272
|
await (0, import_promises9.writeFile)(dest, file.contents, "utf8");
|
|
3720
3273
|
}
|
|
3721
3274
|
}
|
|
@@ -3724,7 +3277,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
3724
3277
|
// src/watch/lock-file.ts
|
|
3725
3278
|
var import_promises10 = require("fs/promises");
|
|
3726
3279
|
var import_promises11 = require("fs/promises");
|
|
3727
|
-
var
|
|
3280
|
+
var import_node_path13 = require("path");
|
|
3728
3281
|
var LOCK_FILE = ".watcher.lock";
|
|
3729
3282
|
function isProcessAlive(pid) {
|
|
3730
3283
|
try {
|
|
@@ -3736,7 +3289,7 @@ function isProcessAlive(pid) {
|
|
|
3736
3289
|
}
|
|
3737
3290
|
async function acquireLock(outDir) {
|
|
3738
3291
|
await (0, import_promises11.mkdir)(outDir, { recursive: true });
|
|
3739
|
-
const lockPath = (0,
|
|
3292
|
+
const lockPath = (0, import_node_path13.join)(outDir, LOCK_FILE);
|
|
3740
3293
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3741
3294
|
try {
|
|
3742
3295
|
const fd = await (0, import_promises10.open)(lockPath, "wx");
|
|
@@ -3776,7 +3329,7 @@ async function watch(config, onChange) {
|
|
|
3776
3329
|
if (lock === null) {
|
|
3777
3330
|
let holderPid = "unknown";
|
|
3778
3331
|
try {
|
|
3779
|
-
const raw = await (0, import_promises12.readFile)((0,
|
|
3332
|
+
const raw = await (0, import_promises12.readFile)((0, import_node_path14.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3780
3333
|
const data = JSON.parse(raw);
|
|
3781
3334
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3782
3335
|
} catch {
|
|
@@ -3804,7 +3357,7 @@ async function watch(config, onChange) {
|
|
|
3804
3357
|
}
|
|
3805
3358
|
let pagesDebounceTimer;
|
|
3806
3359
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3807
|
-
const pagesWatcher = import_chokidar.default.watch((0,
|
|
3360
|
+
const pagesWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, pagesGlob), {
|
|
3808
3361
|
ignoreInitial: true,
|
|
3809
3362
|
persistent: true,
|
|
3810
3363
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3830,7 +3383,7 @@ async function watch(config, onChange) {
|
|
|
3830
3383
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3831
3384
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3832
3385
|
let contractsDebounceTimer;
|
|
3833
|
-
const contractsWatcher = import_chokidar.default.watch((0,
|
|
3386
|
+
const contractsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.contracts.glob), {
|
|
3834
3387
|
ignoreInitial: true,
|
|
3835
3388
|
persistent: true,
|
|
3836
3389
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3860,7 +3413,7 @@ async function watch(config, onChange) {
|
|
|
3860
3413
|
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3861
3414
|
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3862
3415
|
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3863
|
-
const formsWatcher = import_chokidar.default.watch((0,
|
|
3416
|
+
const formsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.forms.watch), {
|
|
3864
3417
|
ignoreInitial: true,
|
|
3865
3418
|
persistent: true,
|
|
3866
3419
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|