@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.
@@ -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
- const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-inertia");
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
- validation: resolveAdapter(userConfig.validation ?? "zod"),
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 import_node_path13 = require("path");
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 import_ts_morph6 = require("ts-morph");
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 _ctx = { projectRoot: "", tsconfigPaths: null };
268
- function setDiscoveryContext(ctx) {
269
- const prev = _ctx;
270
- _ctx = ctx;
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 restoreDiscoveryContext(ctx) {
274
- _ctx = ctx;
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, _project) {
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 baseUrl = _projectRoot();
333
- const tsconfigPaths = _tsconfigPaths();
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 = _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 import_ts_morph5 = require("ts-morph");
741
+ var import_ts_morph4 = require("ts-morph");
1162
742
 
1163
743
  // src/discovery/filter-field-types.ts
1164
- var import_ts_morph4 = require("ts-morph");
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 (import_ts_morph4.Node.isUnionTypeNode(typeNode)) {
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 === import_ts_morph4.SyntaxKind.NullKeyword || kind === import_ts_morph4.SyntaxKind.UndefinedKeyword) {
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 (import_ts_morph4.Node.isLiteralTypeNode(member)) {
790
+ if (import_ts_morph3.Node.isLiteralTypeNode(member)) {
1211
791
  const lit = member.getLiteral();
1212
- if (import_ts_morph4.Node.isStringLiteral(lit)) {
792
+ if (import_ts_morph3.Node.isStringLiteral(lit)) {
1213
793
  stringLits.push(lit.getLiteralValue());
1214
794
  continue;
1215
795
  }
1216
- if (import_ts_morph4.Node.isNumericLiteral(lit)) {
796
+ if (import_ts_morph3.Node.isNumericLiteral(lit)) {
1217
797
  numberLits.push(lit.getText());
1218
798
  continue;
1219
799
  }
1220
- if (lit.getKind() === import_ts_morph4.SyntaxKind.NullKeyword) {
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 import_ts_morph4.SyntaxKind.StringKeyword:
820
+ case import_ts_morph3.SyntaxKind.StringKeyword:
1241
821
  return { kind: "string" };
1242
- case import_ts_morph4.SyntaxKind.NumberKeyword:
822
+ case import_ts_morph3.SyntaxKind.NumberKeyword:
1243
823
  return { kind: "number" };
1244
- case import_ts_morph4.SyntaxKind.BooleanKeyword:
824
+ case import_ts_morph3.SyntaxKind.BooleanKeyword:
1245
825
  return { kind: "boolean" };
1246
- case import_ts_morph4.SyntaxKind.AnyKeyword:
1247
- case import_ts_morph4.SyntaxKind.UnknownKeyword:
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 (import_ts_morph4.Node.isTypeReference(typeNode)) {
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 (import_ts_morph4.Node.isTypeLiteral(typeNode)) return { kind: "json" };
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 (import_ts_morph4.Node.isArrowFunction(arg)) {
850
+ if (import_ts_morph3.Node.isArrowFunction(arg)) {
1271
851
  const body = arg.getBody();
1272
- if (import_ts_morph4.Node.isIdentifier(body)) {
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 (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
857
+ if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1278
858
  const itemsProp = arg.getProperty("items");
1279
- if (itemsProp && import_ts_morph4.Node.isPropertyAssignment(itemsProp)) {
859
+ if (itemsProp && import_ts_morph3.Node.isPropertyAssignment(itemsProp)) {
1280
860
  const init = itemsProp.getInitializer();
1281
- if (init && import_ts_morph4.Node.isArrowFunction(init)) {
861
+ if (init && import_ts_morph3.Node.isArrowFunction(init)) {
1282
862
  const body = init.getBody();
1283
- if (import_ts_morph4.Node.isIdentifier(body)) {
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 (import_ts_morph4.Node.isStringLiteral(arg)) {
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 (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
894
+ if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1315
895
  const enumProp = arg.getProperty("enum");
1316
- if (enumProp && import_ts_morph4.Node.isPropertyAssignment(enumProp)) {
896
+ if (enumProp && import_ts_morph3.Node.isPropertyAssignment(enumProp)) {
1317
897
  const init = enumProp.getInitializer();
1318
- if (init && import_ts_morph4.Node.isIdentifier(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 && import_ts_morph4.Node.isPropertyAssignment(typeProp)) {
907
+ if (typeProp && import_ts_morph3.Node.isPropertyAssignment(typeProp)) {
1328
908
  const init = typeProp.getInitializer();
1329
- if (init && import_ts_morph4.Node.isStringLiteral(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 (import_ts_morph5.Node.isStringLiteral(typeInit)) {
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 (import_ts_morph5.Node.isArrayLiteralExpression(typeInit)) {
959
+ if (import_ts_morph4.Node.isArrayLiteralExpression(typeInit)) {
1380
960
  const values = [];
1381
961
  for (const el of typeInit.getElements()) {
1382
- if (!import_ts_morph5.Node.isStringLiteral(el)) return null;
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 && import_ts_morph5.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
997
+ const inputKey = keyArg && import_ts_morph4.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
1418
998
  const optsArg = args[1];
1419
- if (optsArg && import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) {
999
+ if (optsArg && import_ts_morph4.Node.isObjectLiteralExpression(optsArg)) {
1420
1000
  const typeProp = optsArg.getProperty("type");
1421
- if (typeProp && import_ts_morph5.Node.isPropertyAssignment(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 || !import_ts_morph5.Node.isIdentifier(filterClassArg)) continue;
1024
+ if (!filterClassArg || !import_ts_morph4.Node.isIdentifier(filterClassArg)) continue;
1445
1025
  let source = "query";
1446
1026
  const optionsArg = args[1];
1447
- if (optionsArg && import_ts_morph5.Node.isObjectLiteralExpression(optionsArg)) {
1027
+ if (optionsArg && import_ts_morph4.Node.isObjectLiteralExpression(optionsArg)) {
1448
1028
  const sourceProp = optionsArg.getProperty("source");
1449
- if (sourceProp && import_ts_morph5.Node.isPropertyAssignment(sourceProp)) {
1029
+ if (sourceProp && import_ts_morph4.Node.isPropertyAssignment(sourceProp)) {
1450
1030
  const init = sourceProp.getInitializer();
1451
- if (init && import_ts_morph5.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
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 (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
1094
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
1515
1095
  const entityProp = arg.getProperty("entity");
1516
- if (entityProp && import_ts_morph5.Node.isPropertyAssignment(entityProp)) {
1096
+ if (entityProp && import_ts_morph4.Node.isPropertyAssignment(entityProp)) {
1517
1097
  const init = entityProp.getInitializer();
1518
- if (init && import_ts_morph5.Node.isArrowFunction(init)) {
1098
+ if (init && import_ts_morph4.Node.isArrowFunction(init)) {
1519
1099
  const body = init.getBody();
1520
- if (import_ts_morph5.Node.isIdentifier(body)) {
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 (import_ts_morph5.Node.isArrowFunction(arg)) {
1107
+ if (import_ts_morph4.Node.isArrowFunction(arg)) {
1528
1108
  const body = arg.getBody();
1529
- if (import_ts_morph5.Node.isIdentifier(body)) {
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 (!import_ts_morph5.Node.isObjectLiteralExpression(optionsArg)) return [];
1133
+ if (!import_ts_morph4.Node.isObjectLiteralExpression(optionsArg)) return [];
1554
1134
  const entityProp = optionsArg.getProperty("entity");
1555
- if (!entityProp || !import_ts_morph5.Node.isPropertyAssignment(entityProp)) return [];
1135
+ if (!entityProp || !import_ts_morph4.Node.isPropertyAssignment(entityProp)) return [];
1556
1136
  const entityInit = entityProp.getInitializer();
1557
- if (!entityInit || !import_ts_morph5.Node.isIdentifier(entityInit)) return [];
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 && import_ts_morph5.Node.isObjectLiteralExpression(relArgs[0])) {
1153
+ if (relArgs.length > 0 && import_ts_morph4.Node.isObjectLiteralExpression(relArgs[0])) {
1574
1154
  for (const relProp of relArgs[0].getProperties()) {
1575
- if (!import_ts_morph5.Node.isPropertyAssignment(relProp)) continue;
1155
+ if (!import_ts_morph4.Node.isPropertyAssignment(relProp)) continue;
1576
1156
  const relInit = relProp.getInitializer();
1577
- if (!relInit || !import_ts_morph5.Node.isObjectLiteralExpression(relInit)) continue;
1157
+ if (!relInit || !import_ts_morph4.Node.isObjectLiteralExpression(relInit)) continue;
1578
1158
  const keysProp = relInit.getProperty("keys");
1579
- if (!keysProp || !import_ts_morph5.Node.isPropertyAssignment(keysProp)) continue;
1159
+ if (!keysProp || !import_ts_morph4.Node.isPropertyAssignment(keysProp)) continue;
1580
1160
  const keysInit = keysProp.getInitializer();
1581
- if (!keysInit || !import_ts_morph5.Node.isArrayLiteralExpression(keysInit)) continue;
1161
+ if (!keysInit || !import_ts_morph4.Node.isArrayLiteralExpression(keysInit)) continue;
1582
1162
  for (const el of keysInit.getElements()) {
1583
- if (import_ts_morph5.Node.isStringLiteral(el)) {
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/contracts-fast.ts
1594
- async function discoverContractsFast(opts) {
1595
- const { cwd, glob, tsconfig } = opts;
1596
- const tsconfigPath = tsconfig ? (0, import_node_path3.resolve)(tsconfig) : (0, import_node_path3.join)(cwd, "tsconfig.json");
1597
- let project;
1598
- try {
1599
- project = new import_ts_morph6.Project({
1600
- tsConfigFilePath: tsconfigPath,
1601
- skipAddingFilesFromTsConfig: true,
1602
- skipLoadingLibFiles: true,
1603
- skipFileDependencyResolution: true
1604
- });
1605
- } catch {
1606
- project = new import_ts_morph6.Project({
1607
- skipAddingFilesFromTsConfig: true,
1608
- skipLoadingLibFiles: true,
1609
- skipFileDependencyResolution: true,
1610
- compilerOptions: {
1611
- allowJs: true,
1612
- resolveJsonModule: false,
1613
- strict: false
1614
- }
1615
- });
1616
- }
1617
- const files = await (0, import_fast_glob.default)(glob, { cwd, absolute: true, onlyFiles: true });
1618
- for (const f of files) {
1619
- project.addSourceFileAtPath(f);
1620
- }
1621
- const routes = [];
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 (import_ts_morph6.Node.isArrayTypeNode(typeNode)) {
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 (import_ts_morph6.Node.isUnionTypeNode(typeNode)) {
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 (import_ts_morph6.Node.isIntersectionTypeNode(typeNode)) {
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 (import_ts_morph6.Node.isParenthesizedTypeNode(typeNode)) {
1214
+ if (import_ts_morph5.Node.isParenthesizedTypeNode(typeNode)) {
1801
1215
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
1802
1216
  }
1803
- if (import_ts_morph6.Node.isTypeReference(typeNode)) {
1217
+ if (import_ts_morph5.Node.isTypeReference(typeNode)) {
1804
1218
  const typeName = typeNode.getTypeName();
1805
- const name = import_ts_morph6.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
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
- if (name === "Ref" || name === "Reference" || name === "LoadedReference" || name === "IdentifiedReference") {
1812
- const typeArgs = typeNode.getTypeArguments();
1813
- const firstTypeArg = typeArgs[0];
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 === "Collection") {
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 === import_ts_morph6.SyntaxKind.StringKeyword) return "string";
1863
- if (kind === import_ts_morph6.SyntaxKind.NumberKeyword) return "number";
1864
- if (kind === import_ts_morph6.SyntaxKind.BooleanKeyword) return "boolean";
1865
- if (kind === import_ts_morph6.SyntaxKind.UnknownKeyword) return "unknown";
1866
- if (kind === import_ts_morph6.SyntaxKind.AnyKeyword) return "unknown";
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 (!import_ts_morph6.Node.isStringLiteral(nameArg)) continue;
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 && import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) {
1334
+ if (optsArg && import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) {
1948
1335
  for (const prop of optsArg.getProperties()) {
1949
- if (!import_ts_morph6.Node.isPropertyAssignment(prop)) continue;
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 (import_ts_morph6.Node.isArrayLiteralExpression(val)) {
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 (!import_ts_morph6.Node.isIdentifier(node)) return "unknown";
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 && import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) {
1407
+ if (optsArg && import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) {
2021
1408
  for (const prop of optsArg.getProperties()) {
2022
- if (import_ts_morph6.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
1409
+ if (import_ts_morph5.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
2023
1410
  const val = prop.getInitializer();
2024
- if (val && import_ts_morph6.Node.isIdentifier(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 classes = sourceFile.getClasses();
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 controllerArgs = controllerDecorator.getArguments();
2114
- const firstArg3 = controllerArgs[0];
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
- let httpMethod;
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
- if (applyContractDecorator) {
2132
- const decoratorArgs = applyContractDecorator.getArguments();
2133
- const firstDecoratorArg = decoratorArgs[0];
2134
- if (!firstDecoratorArg) continue;
2135
- let contractDef = null;
2136
- let bodyZodRef = null;
2137
- let queryZodRef = null;
2138
- if (import_ts_morph6.Node.isCallExpression(firstDecoratorArg)) {
2139
- contractDef = parseDefineContractCall(firstDecoratorArg);
2140
- } else if (import_ts_morph6.Node.isIdentifier(firstDecoratorArg)) {
2141
- const identName = firstDecoratorArg.getText();
2142
- const varDecl = sourceFile.getVariableDeclaration(identName);
2143
- if (!varDecl) {
2144
- console.warn(
2145
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
2146
- );
2147
- continue;
2148
- }
2149
- const initializer = varDecl.getInitializer();
2150
- if (!initializer) continue;
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 import_node_path11 = require("path");
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 import_ts_morph7 = require("ts-morph");
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(import_ts_morph7.SyntaxKind.CallExpression);
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 (!import_ts_morph7.Node.isPropertyAccessExpression(expr)) continue;
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" && import_ts_morph7.Node.isIdentifier(objectExpr)) {
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 (!import_ts_morph7.Node.isCallExpression(forRootCall)) return null;
1981
+ if (!import_ts_morph8.Node.isCallExpression(forRootCall)) return null;
2377
1982
  const args = forRootCall.getArguments();
2378
- const firstArg3 = args[0];
2379
- if (!firstArg3 || !import_ts_morph7.Node.isObjectLiteralExpression(firstArg3)) return null;
2380
- for (const prop of firstArg3.getProperties()) {
2381
- if (import_ts_morph7.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
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 (import_ts_morph7.Node.isIdentifier(node)) {
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 (import_ts_morph7.Node.isArrowFunction(node)) {
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 (import_ts_morph7.Node.isFunctionExpression(node)) {
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 (import_ts_morph7.Node.isObjectLiteralExpression(node)) {
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 (!import_ts_morph7.Node.isIdentifier(id)) return null;
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 = import_ts_morph7.Node.isArrowFunction(node) || import_ts_morph7.Node.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
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 (import_ts_morph7.Node.isArrowFunction(node)) {
2056
+ if (import_ts_morph8.Node.isArrowFunction(node)) {
2452
2057
  const body = node.getBody();
2453
- if (import_ts_morph7.Node.isParenthesizedExpression(body)) {
2058
+ if (import_ts_morph8.Node.isParenthesizedExpression(body)) {
2454
2059
  const inner = body.getExpression();
2455
- if (import_ts_morph7.Node.isObjectLiteralExpression(inner)) {
2060
+ if (import_ts_morph8.Node.isObjectLiteralExpression(inner)) {
2456
2061
  return extractFromObjectLiteral(inner);
2457
2062
  }
2458
2063
  }
2459
- if (import_ts_morph7.Node.isObjectLiteralExpression(body)) {
2064
+ if (import_ts_morph8.Node.isObjectLiteralExpression(body)) {
2460
2065
  return extractFromObjectLiteral(body);
2461
2066
  }
2462
- if (import_ts_morph7.Node.isBlock(body)) {
2067
+ if (import_ts_morph8.Node.isBlock(body)) {
2463
2068
  return extractFromBlockReturn(body);
2464
2069
  }
2465
2070
  }
2466
- if (import_ts_morph7.Node.isFunctionExpression(node)) {
2071
+ if (import_ts_morph8.Node.isFunctionExpression(node)) {
2467
2072
  const body = node.getBody();
2468
- if (import_ts_morph7.Node.isBlock(body)) {
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 (import_ts_morph7.Node.isTypeReference(typeNode)) {
2080
+ if (import_ts_morph8.Node.isTypeReference(typeNode)) {
2476
2081
  const typeName = typeNode.getTypeName();
2477
- if (import_ts_morph7.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
2082
+ if (import_ts_morph8.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
2478
2083
  const typeArgs = typeNode.getTypeArguments();
2479
- const firstArg3 = typeArgs[0];
2480
- if (firstArg3) {
2481
- return extractFromReturnTypeAnnotation(firstArg3);
2084
+ const firstArg2 = typeArgs[0];
2085
+ if (firstArg2) {
2086
+ return extractFromReturnTypeAnnotation(firstArg2);
2482
2087
  }
2483
2088
  return null;
2484
2089
  }
2485
2090
  }
2486
- if (import_ts_morph7.Node.isTypeLiteral(typeNode)) {
2091
+ if (import_ts_morph8.Node.isTypeLiteral(typeNode)) {
2487
2092
  const properties = [];
2488
2093
  for (const member of typeNode.getMembers()) {
2489
- if (import_ts_morph7.Node.isPropertySignature(member)) {
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 (!import_ts_morph7.Node.isBlock(block)) return null;
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 (!import_ts_morph7.Node.isReturnStatement(stmt)) continue;
2112
+ if (!import_ts_morph8.Node.isReturnStatement(stmt)) continue;
2508
2113
  const expr = stmt.getExpression();
2509
2114
  if (!expr) continue;
2510
- if (import_ts_morph7.Node.isObjectLiteralExpression(expr)) {
2115
+ if (import_ts_morph8.Node.isObjectLiteralExpression(expr)) {
2511
2116
  return extractFromObjectLiteral(expr);
2512
2117
  }
2513
- if (import_ts_morph7.Node.isParenthesizedExpression(expr)) {
2118
+ if (import_ts_morph8.Node.isParenthesizedExpression(expr)) {
2514
2119
  const inner = expr.getExpression();
2515
- if (import_ts_morph7.Node.isObjectLiteralExpression(inner)) {
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 (!import_ts_morph7.Node.isObjectLiteralExpression(objLiteral)) return null;
2129
+ if (!import_ts_morph8.Node.isObjectLiteralExpression(objLiteral)) return null;
2525
2130
  const properties = [];
2526
2131
  for (const prop of objLiteral.getProperties()) {
2527
- if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
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 (import_ts_morph7.Node.isStringLiteral(node)) return "string";
2540
- if (import_ts_morph7.Node.isTemplateExpression(node) || import_ts_morph7.Node.isNoSubstitutionTemplateLiteral(node))
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 (import_ts_morph7.Node.isNumericLiteral(node)) return "number";
2543
- if (node.getKind() === import_ts_morph7.SyntaxKind.TrueKeyword || node.getKind() === import_ts_morph7.SyntaxKind.FalseKeyword) {
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() === import_ts_morph7.SyntaxKind.NullKeyword) return "null";
2547
- if (import_ts_morph7.Node.isIdentifier(node) && node.getText() === "undefined") return "undefined";
2548
- if (import_ts_morph7.Node.isObjectLiteralExpression(node)) {
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 (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
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 (import_ts_morph7.Node.isArrayLiteralExpression(node)) {
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 (import_ts_morph7.Node.isConditionalExpression(node)) {
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 (import_ts_morph7.Node.isParenthesizedExpression(node)) {
2180
+ if (import_ts_morph8.Node.isParenthesizedExpression(node)) {
2576
2181
  return inferExpressionType(node.getExpression());
2577
2182
  }
2578
- if (import_ts_morph7.Node.isAsExpression(node)) {
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 import_node_path5 = require("path");
2192
+ var import_node_path6 = require("path");
2588
2193
 
2589
2194
  // src/extension/registry.ts
2590
- var import_ts_morph8 = require("ts-morph");
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 import_ts_morph8.Project({
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
- const prev = owners.get(key);
2667
- if (prev !== void 0) {
2668
- throw new CodegenError(
2669
- `Output file "${file.path}" is emitted by both "${prev}" and "${ext.name}". Two extensions cannot write the same file.`
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, import_node_path5.join)(outDir, "api.ts"), content, "utf8");
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 toObjectKey3(segment) {
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, import_node_path5.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
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 = toObjectKey3(key);
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 hasBody = !!c.contractSource.bodyRef || c.contractSource.body != null && c.contractSource.body !== "never";
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 = toObjectKey3(key);
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 route = {
2908
- method: node.method,
2909
- path: node.path,
2910
- name: node.name,
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 leaf = { route, request: req, requestExpr: "" };
2916
- leaf.requestExpr = p.transport ? p.transport.renderRequest(leaf, p.ctx) : renderFetcherRequest(req);
2917
- const members = {};
2918
- if (p.layer) Object.assign(members, p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx));
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
- for (const [name, value] of Object.entries(extra)) {
2923
- if (name in members) {
2924
- throw new Error(
2925
- `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict at "${ext.name}").`
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 { transport, layer } = resolveApiSlots(extensions);
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, import_node_path5.isAbsolute)(filePath)) {
3004
- relPath = (0, import_node_path5.relative)(outDir, filePath).replace(/\.ts$/, "");
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("export namespace Route {");
3033
- lines.push(" export type Response<K extends string> = never;");
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("type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`");
3095
- lines.push(" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never");
3096
- lines.push(" : P extends keyof R ? R[P] : never;");
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 import_node_path6 = require("path");
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, import_node_path6.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
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 import_node_path7 = require("path");
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
- if (adapter && adapter.name !== "zod") {
3184
- const content2 = buildFormsFileWithAdapter(routes, adapter);
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
- const content = buildFormsFile(entries, outDir, config);
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, import_node_path7.relative)(outDir, filePath).replace(/\.ts$/, "");
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 buildFormsFile(entries, outDir, config) {
3312
- const zodImport = config?.zodImport ?? "zod";
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 renderSchema(src, outDir, refAlias) {
3456
- if (src.text) return src.text;
3457
- if (src.ref) {
3458
- const root = refRootIdentifier(src.ref.name);
3459
- const alias = refAlias.get(`${src.ref.filePath}\0${root}`) ?? root;
3460
- const member = src.ref.name.slice(root.length);
3461
- return `${alias}${member}`;
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
- return "z.unknown()";
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 import_node_path8 = require("path");
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, import_node_path8.join)(outDir, "index.d.ts"), content, "utf8");
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 import_node_path9 = require("path");
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, import_node_path9.join)(outDir, "pages.d.ts"), content, "utf8");
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, import_node_path9.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
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 import_node_path10 = require("path");
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, import_node_path10.join)(outDir, "routes.ts"), content, "utf8");
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
- let sharedProps = null;
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, import_node_path11.join)(config.codegen.outDir, file.path);
3718
- await (0, import_promises9.mkdir)((0, import_node_path11.dirname)(dest), { recursive: true });
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 import_node_path12 = require("path");
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, import_node_path12.join)(outDir, LOCK_FILE);
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, import_node_path13.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
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, import_node_path13.join)(config.codegen.cwd, pagesGlob), {
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, import_node_path13.join)(config.codegen.cwd, config.contracts.glob), {
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, import_node_path13.join)(config.codegen.cwd, config.forms.watch), {
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 }