@dudousxd/nestjs-codegen 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);
@@ -545,10 +453,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
545
453
  depth: 0
546
454
  };
547
455
  const root = buildObject(classDecl, sourceFile, ctx);
548
- for (const schemaName of ctx.recursiveSchemas) {
549
- ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
550
- }
551
- return { root, named: ctx.named, warnings: ctx.warnings };
456
+ return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
552
457
  }
553
458
  function buildObject(classDecl, classFile, ctx) {
554
459
  const props = classDecl.getProperties();
@@ -568,7 +473,7 @@ function buildProperty(prop, classFile, ctx) {
568
473
  const dec = (n) => decorators.get(n);
569
474
  const typeNode = prop.getTypeNode();
570
475
  const typeText = typeNode?.getText() ?? "unknown";
571
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
476
+ const isArrayType = !!typeNode && import_ts_morph2.Node.isArrayTypeNode(typeNode);
572
477
  const typeRefName = resolveTypeFactoryName(dec("Type"));
573
478
  if (has("ValidateNested") || typeRefName) {
574
479
  const childName = typeRefName ?? singularClassName(typeText);
@@ -699,18 +604,27 @@ function baseFromType(typeText, isArrayType) {
699
604
  }
700
605
  }
701
606
  function buildNestedReference(className, fromFile, ctx) {
702
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
607
+ if (ctx.visiting.has(className)) {
703
608
  const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
704
609
  ctx.emittedClasses.set(className, reserved);
705
610
  ctx.recursiveSchemas.add(reserved);
706
611
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
707
612
  ctx.warnedDecorators.add(`recursive:${reserved}`);
708
- const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
613
+ const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
709
614
  ctx.warnings.push(msg);
710
615
  console.warn(`[nestjs-codegen] ${msg}`);
711
616
  }
712
617
  return { kind: "lazyRef", name: reserved };
713
618
  }
619
+ if (ctx.depth >= 8) {
620
+ if (!ctx.warnedDecorators.has(`deep:${className}`)) {
621
+ ctx.warnedDecorators.add(`deep:${className}`);
622
+ const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
623
+ ctx.warnings.push(msg);
624
+ console.warn(`[nestjs-codegen] ${msg}`);
625
+ }
626
+ return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
627
+ }
714
628
  const existing = ctx.emittedClasses.get(className);
715
629
  if (existing) return { kind: "ref", name: existing };
716
630
  const schemaName = aliasFor(className, ctx);
@@ -829,339 +743,11 @@ function inSchemaFromDecorator(decorator) {
829
743
  return null;
830
744
  }
831
745
 
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
746
  // src/discovery/filter-for.ts
1161
- var import_ts_morph5 = require("ts-morph");
747
+ var import_ts_morph4 = require("ts-morph");
1162
748
 
1163
749
  // src/discovery/filter-field-types.ts
1164
- var import_ts_morph4 = require("ts-morph");
750
+ var import_ts_morph3 = require("ts-morph");
1165
751
 
1166
752
  // src/discovery/enum-resolution.ts
1167
753
  function resolveEnumValues(name, sourceFile, project) {
@@ -1196,28 +782,28 @@ function markNullable(r, nullable) {
1196
782
  return nullable ? { ...r, nullable: true } : r;
1197
783
  }
1198
784
  function classifyTypeNode(typeNode, sourceFile, project, opts) {
1199
- if (import_ts_morph4.Node.isUnionTypeNode(typeNode)) {
785
+ if (import_ts_morph3.Node.isUnionTypeNode(typeNode)) {
1200
786
  let nullable = false;
1201
787
  const stringLits = [];
1202
788
  const numberLits = [];
1203
789
  const others = [];
1204
790
  for (const member of typeNode.getTypeNodes()) {
1205
791
  const kind = member.getKind();
1206
- if (kind === import_ts_morph4.SyntaxKind.NullKeyword || kind === import_ts_morph4.SyntaxKind.UndefinedKeyword) {
792
+ if (kind === import_ts_morph3.SyntaxKind.NullKeyword || kind === import_ts_morph3.SyntaxKind.UndefinedKeyword) {
1207
793
  nullable = true;
1208
794
  continue;
1209
795
  }
1210
- if (import_ts_morph4.Node.isLiteralTypeNode(member)) {
796
+ if (import_ts_morph3.Node.isLiteralTypeNode(member)) {
1211
797
  const lit = member.getLiteral();
1212
- if (import_ts_morph4.Node.isStringLiteral(lit)) {
798
+ if (import_ts_morph3.Node.isStringLiteral(lit)) {
1213
799
  stringLits.push(lit.getLiteralValue());
1214
800
  continue;
1215
801
  }
1216
- if (import_ts_morph4.Node.isNumericLiteral(lit)) {
802
+ if (import_ts_morph3.Node.isNumericLiteral(lit)) {
1217
803
  numberLits.push(lit.getText());
1218
804
  continue;
1219
805
  }
1220
- if (lit.getKind() === import_ts_morph4.SyntaxKind.NullKeyword) {
806
+ if (lit.getKind() === import_ts_morph3.SyntaxKind.NullKeyword) {
1221
807
  nullable = true;
1222
808
  continue;
1223
809
  }
@@ -1237,19 +823,19 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
1237
823
  return markNullable({ kind: "unknown" }, nullable);
1238
824
  }
1239
825
  switch (typeNode.getKind()) {
1240
- case import_ts_morph4.SyntaxKind.StringKeyword:
826
+ case import_ts_morph3.SyntaxKind.StringKeyword:
1241
827
  return { kind: "string" };
1242
- case import_ts_morph4.SyntaxKind.NumberKeyword:
828
+ case import_ts_morph3.SyntaxKind.NumberKeyword:
1243
829
  return { kind: "number" };
1244
- case import_ts_morph4.SyntaxKind.BooleanKeyword:
830
+ case import_ts_morph3.SyntaxKind.BooleanKeyword:
1245
831
  return { kind: "boolean" };
1246
- case import_ts_morph4.SyntaxKind.AnyKeyword:
1247
- case import_ts_morph4.SyntaxKind.UnknownKeyword:
832
+ case import_ts_morph3.SyntaxKind.AnyKeyword:
833
+ case import_ts_morph3.SyntaxKind.UnknownKeyword:
1248
834
  return { kind: "unknown" };
1249
835
  default:
1250
836
  break;
1251
837
  }
1252
- if (import_ts_morph4.Node.isTypeReference(typeNode)) {
838
+ if (import_ts_morph3.Node.isTypeReference(typeNode)) {
1253
839
  const refName = typeNode.getTypeName().getText();
1254
840
  if (refName === "Date") return { kind: "date" };
1255
841
  if (refName === "Record" || refName === "Object") return { kind: "json" };
@@ -1262,25 +848,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
1262
848
  if (typeRef) return { kind: "unknown", typeRef };
1263
849
  return { kind: "unknown" };
1264
850
  }
1265
- if (import_ts_morph4.Node.isTypeLiteral(typeNode)) return { kind: "json" };
851
+ if (import_ts_morph3.Node.isTypeLiteral(typeNode)) return { kind: "json" };
1266
852
  return { kind: "unknown" };
1267
853
  }
1268
854
  function enumFromDecoratorArgs(args, sourceFile, project) {
1269
855
  for (const arg of args) {
1270
- if (import_ts_morph4.Node.isArrowFunction(arg)) {
856
+ if (import_ts_morph3.Node.isArrowFunction(arg)) {
1271
857
  const body = arg.getBody();
1272
- if (import_ts_morph4.Node.isIdentifier(body)) {
858
+ if (import_ts_morph3.Node.isIdentifier(body)) {
1273
859
  const en = resolveEnumValues(body.getText(), sourceFile, project);
1274
860
  if (en) return en;
1275
861
  }
1276
862
  }
1277
- if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
863
+ if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1278
864
  const itemsProp = arg.getProperty("items");
1279
- if (itemsProp && import_ts_morph4.Node.isPropertyAssignment(itemsProp)) {
865
+ if (itemsProp && import_ts_morph3.Node.isPropertyAssignment(itemsProp)) {
1280
866
  const init = itemsProp.getInitializer();
1281
- if (init && import_ts_morph4.Node.isArrowFunction(init)) {
867
+ if (init && import_ts_morph3.Node.isArrowFunction(init)) {
1282
868
  const body = init.getBody();
1283
- if (import_ts_morph4.Node.isIdentifier(body)) {
869
+ if (import_ts_morph3.Node.isIdentifier(body)) {
1284
870
  const en = resolveEnumValues(body.getText(), sourceFile, project);
1285
871
  if (en) return en;
1286
872
  }
@@ -1303,7 +889,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1303
889
  return { kind: "string" };
1304
890
  }
1305
891
  for (const arg of args) {
1306
- if (import_ts_morph4.Node.isStringLiteral(arg)) {
892
+ if (import_ts_morph3.Node.isStringLiteral(arg)) {
1307
893
  const raw = arg.getLiteralValue();
1308
894
  const kind = classifyTypeKeyword(raw);
1309
895
  if (kind) {
@@ -1311,11 +897,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1311
897
  return { kind };
1312
898
  }
1313
899
  }
1314
- if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
900
+ if (import_ts_morph3.Node.isObjectLiteralExpression(arg)) {
1315
901
  const enumProp = arg.getProperty("enum");
1316
- if (enumProp && import_ts_morph4.Node.isPropertyAssignment(enumProp)) {
902
+ if (enumProp && import_ts_morph3.Node.isPropertyAssignment(enumProp)) {
1317
903
  const init = enumProp.getInitializer();
1318
- if (init && import_ts_morph4.Node.isIdentifier(init)) {
904
+ if (init && import_ts_morph3.Node.isIdentifier(init)) {
1319
905
  const en = resolveEnumValues(init.getText(), sourceFile, project);
1320
906
  if (en) {
1321
907
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -1324,9 +910,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
1324
910
  }
1325
911
  }
1326
912
  const typeProp = arg.getProperty("type");
1327
- if (typeProp && import_ts_morph4.Node.isPropertyAssignment(typeProp)) {
913
+ if (typeProp && import_ts_morph3.Node.isPropertyAssignment(typeProp)) {
1328
914
  const init = typeProp.getInitializer();
1329
- if (init && import_ts_morph4.Node.isStringLiteral(init)) {
915
+ if (init && import_ts_morph3.Node.isStringLiteral(init)) {
1330
916
  const kind = classifyTypeKeyword(init.getLiteralValue());
1331
917
  if (kind) return { kind };
1332
918
  }
@@ -1362,7 +948,7 @@ function toFilterFieldType(name, r) {
1362
948
 
1363
949
  // src/discovery/filter-for.ts
1364
950
  function classifyFilterForHint(typeInit) {
1365
- if (import_ts_morph5.Node.isStringLiteral(typeInit)) {
951
+ if (import_ts_morph4.Node.isStringLiteral(typeInit)) {
1366
952
  switch (typeInit.getLiteralValue()) {
1367
953
  case "string":
1368
954
  return { kind: "string" };
@@ -1376,10 +962,10 @@ function classifyFilterForHint(typeInit) {
1376
962
  return null;
1377
963
  }
1378
964
  }
1379
- if (import_ts_morph5.Node.isArrayLiteralExpression(typeInit)) {
965
+ if (import_ts_morph4.Node.isArrayLiteralExpression(typeInit)) {
1380
966
  const values = [];
1381
967
  for (const el of typeInit.getElements()) {
1382
- if (!import_ts_morph5.Node.isStringLiteral(el)) return null;
968
+ if (!import_ts_morph4.Node.isStringLiteral(el)) return null;
1383
969
  values.push(el.getLiteralValue());
1384
970
  }
1385
971
  if (values.length === 0) return null;
@@ -1414,11 +1000,11 @@ function extractFilterForHints(classDecl, project) {
1414
1000
  if (!filterForDec) continue;
1415
1001
  const args = filterForDec.getArguments();
1416
1002
  const keyArg = args[0];
1417
- const inputKey = keyArg && import_ts_morph5.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
1003
+ const inputKey = keyArg && import_ts_morph4.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
1418
1004
  const optsArg = args[1];
1419
- if (optsArg && import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) {
1005
+ if (optsArg && import_ts_morph4.Node.isObjectLiteralExpression(optsArg)) {
1420
1006
  const typeProp = optsArg.getProperty("type");
1421
- if (typeProp && import_ts_morph5.Node.isPropertyAssignment(typeProp)) {
1007
+ if (typeProp && import_ts_morph4.Node.isPropertyAssignment(typeProp)) {
1422
1008
  const typeInit = typeProp.getInitializer();
1423
1009
  if (typeInit) {
1424
1010
  const classified = classifyFilterForHint(typeInit);
@@ -1441,14 +1027,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
1441
1027
  const args = filterDecorator.getArguments();
1442
1028
  if (args.length === 0) continue;
1443
1029
  const filterClassArg = args[0];
1444
- if (!filterClassArg || !import_ts_morph5.Node.isIdentifier(filterClassArg)) continue;
1030
+ if (!filterClassArg || !import_ts_morph4.Node.isIdentifier(filterClassArg)) continue;
1445
1031
  let source = "query";
1446
1032
  const optionsArg = args[1];
1447
- if (optionsArg && import_ts_morph5.Node.isObjectLiteralExpression(optionsArg)) {
1033
+ if (optionsArg && import_ts_morph4.Node.isObjectLiteralExpression(optionsArg)) {
1448
1034
  const sourceProp = optionsArg.getProperty("source");
1449
- if (sourceProp && import_ts_morph5.Node.isPropertyAssignment(sourceProp)) {
1035
+ if (sourceProp && import_ts_morph4.Node.isPropertyAssignment(sourceProp)) {
1450
1036
  const init = sourceProp.getInitializer();
1451
- if (init && import_ts_morph5.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
1037
+ if (init && import_ts_morph4.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
1452
1038
  source = "body";
1453
1039
  }
1454
1040
  }
@@ -1511,22 +1097,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
1511
1097
  const args = dec.getArguments();
1512
1098
  if (args.length === 0) continue;
1513
1099
  const arg = args[0];
1514
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
1100
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
1515
1101
  const entityProp = arg.getProperty("entity");
1516
- if (entityProp && import_ts_morph5.Node.isPropertyAssignment(entityProp)) {
1102
+ if (entityProp && import_ts_morph4.Node.isPropertyAssignment(entityProp)) {
1517
1103
  const init = entityProp.getInitializer();
1518
- if (init && import_ts_morph5.Node.isArrowFunction(init)) {
1104
+ if (init && import_ts_morph4.Node.isArrowFunction(init)) {
1519
1105
  const body = init.getBody();
1520
- if (import_ts_morph5.Node.isIdentifier(body)) {
1106
+ if (import_ts_morph4.Node.isIdentifier(body)) {
1521
1107
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
1522
1108
  if (resolved?.kind === "class") return resolved.decl;
1523
1109
  }
1524
1110
  }
1525
1111
  }
1526
1112
  }
1527
- if (import_ts_morph5.Node.isArrowFunction(arg)) {
1113
+ if (import_ts_morph4.Node.isArrowFunction(arg)) {
1528
1114
  const body = arg.getBody();
1529
- if (import_ts_morph5.Node.isIdentifier(body)) {
1115
+ if (import_ts_morph4.Node.isIdentifier(body)) {
1530
1116
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
1531
1117
  if (resolved?.kind === "class") return resolved.decl;
1532
1118
  }
@@ -1550,11 +1136,11 @@ function extractFilterableEntityFields(filterClass, project) {
1550
1136
  const args = filterableDecorator.getArguments();
1551
1137
  if (args.length === 0) return [];
1552
1138
  const optionsArg = args[0];
1553
- if (!import_ts_morph5.Node.isObjectLiteralExpression(optionsArg)) return [];
1139
+ if (!import_ts_morph4.Node.isObjectLiteralExpression(optionsArg)) return [];
1554
1140
  const entityProp = optionsArg.getProperty("entity");
1555
- if (!entityProp || !import_ts_morph5.Node.isPropertyAssignment(entityProp)) return [];
1141
+ if (!entityProp || !import_ts_morph4.Node.isPropertyAssignment(entityProp)) return [];
1556
1142
  const entityInit = entityProp.getInitializer();
1557
- if (!entityInit || !import_ts_morph5.Node.isIdentifier(entityInit)) return [];
1143
+ if (!entityInit || !import_ts_morph4.Node.isIdentifier(entityInit)) return [];
1558
1144
  const entityName = entityInit.getText();
1559
1145
  const filterSourceFile = filterClass.getSourceFile();
1560
1146
  const resolvedEntity = findType(entityName, filterSourceFile, project);
@@ -1570,17 +1156,17 @@ function extractFilterableEntityFields(filterClass, project) {
1570
1156
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
1571
1157
  if (relationsDecorator) {
1572
1158
  const relArgs = relationsDecorator.getArguments();
1573
- if (relArgs.length > 0 && import_ts_morph5.Node.isObjectLiteralExpression(relArgs[0])) {
1159
+ if (relArgs.length > 0 && import_ts_morph4.Node.isObjectLiteralExpression(relArgs[0])) {
1574
1160
  for (const relProp of relArgs[0].getProperties()) {
1575
- if (!import_ts_morph5.Node.isPropertyAssignment(relProp)) continue;
1161
+ if (!import_ts_morph4.Node.isPropertyAssignment(relProp)) continue;
1576
1162
  const relInit = relProp.getInitializer();
1577
- if (!relInit || !import_ts_morph5.Node.isObjectLiteralExpression(relInit)) continue;
1163
+ if (!relInit || !import_ts_morph4.Node.isObjectLiteralExpression(relInit)) continue;
1578
1164
  const keysProp = relInit.getProperty("keys");
1579
- if (!keysProp || !import_ts_morph5.Node.isPropertyAssignment(keysProp)) continue;
1165
+ if (!keysProp || !import_ts_morph4.Node.isPropertyAssignment(keysProp)) continue;
1580
1166
  const keysInit = keysProp.getInitializer();
1581
- if (!keysInit || !import_ts_morph5.Node.isArrayLiteralExpression(keysInit)) continue;
1167
+ if (!keysInit || !import_ts_morph4.Node.isArrayLiteralExpression(keysInit)) continue;
1582
1168
  for (const el of keysInit.getElements()) {
1583
- if (import_ts_morph5.Node.isStringLiteral(el)) {
1169
+ if (import_ts_morph4.Node.isStringLiteral(el)) {
1584
1170
  fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
1585
1171
  }
1586
1172
  }
@@ -1590,267 +1176,65 @@ function extractFilterableEntityFields(filterClass, project) {
1590
1176
  return fields;
1591
1177
  }
1592
1178
 
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
- }
1179
+ // src/discovery/dto-type-resolver.ts
1180
+ var WRAPPER_TYPES = {
1181
+ // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
1182
+ // wrappers around related entities; the wire shape is just the referenced
1183
+ // entity. Unwrap to the type argument.
1184
+ Ref: "unwrap",
1185
+ Reference: "unwrap",
1186
+ LoadedReference: "unwrap",
1187
+ IdentifiedReference: "unwrap",
1188
+ // MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
1189
+ Opt: "unwrap",
1190
+ Loaded: "unwrap",
1191
+ // Promise<T> — unwrap
1192
+ Promise: "unwrap",
1193
+ // MikroORM Collection<T> serializes as an array of T on the wire.
1194
+ Collection: "arrayOf",
1195
+ // Array<T> generic form
1196
+ Array: "arrayOf"
1197
+ };
1198
+ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
1199
+ "Record",
1200
+ "Omit",
1201
+ "Pick",
1202
+ "Partial",
1203
+ "Required",
1204
+ "Readonly",
1205
+ "Map",
1206
+ "Set"
1207
+ ]);
1788
1208
  function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1789
1209
  if (depth <= 0) return "unknown";
1790
- if (import_ts_morph6.Node.isArrayTypeNode(typeNode)) {
1210
+ if (import_ts_morph5.Node.isArrayTypeNode(typeNode)) {
1791
1211
  const elementType = typeNode.getElementTypeNode();
1792
1212
  return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
1793
1213
  }
1794
- if (import_ts_morph6.Node.isUnionTypeNode(typeNode)) {
1214
+ if (import_ts_morph5.Node.isUnionTypeNode(typeNode)) {
1795
1215
  return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
1796
1216
  }
1797
- if (import_ts_morph6.Node.isIntersectionTypeNode(typeNode)) {
1217
+ if (import_ts_morph5.Node.isIntersectionTypeNode(typeNode)) {
1798
1218
  return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
1799
1219
  }
1800
- if (import_ts_morph6.Node.isParenthesizedTypeNode(typeNode)) {
1220
+ if (import_ts_morph5.Node.isParenthesizedTypeNode(typeNode)) {
1801
1221
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
1802
1222
  }
1803
- if (import_ts_morph6.Node.isTypeReference(typeNode)) {
1223
+ if (import_ts_morph5.Node.isTypeReference(typeNode)) {
1804
1224
  const typeName = typeNode.getTypeName();
1805
- const name = import_ts_morph6.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
1225
+ const name = import_ts_morph5.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
1806
1226
  if (name === "string" || name === "number" || name === "boolean") return name;
1807
1227
  if (name === "Date") return "string";
1808
1228
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
1809
1229
  if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
1810
1230
  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";
1231
+ const wrapperMode = WRAPPER_TYPES[name];
1232
+ if (wrapperMode) {
1233
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
1818
1234
  }
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)) {
1235
+ if (PASSTHROUGH_UTILITY.has(name)) {
1844
1236
  return typeNode.getText();
1845
1237
  }
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
1238
  const resolved = findType(name, sourceFile, project);
1855
1239
  if (resolved) {
1856
1240
  return expandTypeDecl(resolved, project, depth - 1);
@@ -1859,13 +1243,22 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1859
1243
  return "unknown";
1860
1244
  }
1861
1245
  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";
1246
+ if (kind === import_ts_morph5.SyntaxKind.StringKeyword) return "string";
1247
+ if (kind === import_ts_morph5.SyntaxKind.NumberKeyword) return "number";
1248
+ if (kind === import_ts_morph5.SyntaxKind.BooleanKeyword) return "boolean";
1249
+ if (kind === import_ts_morph5.SyntaxKind.UnknownKeyword) return "unknown";
1250
+ if (kind === import_ts_morph5.SyntaxKind.AnyKeyword) return "unknown";
1867
1251
  return typeNode.getText();
1868
1252
  }
1253
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
1254
+ const typeArgs = typeNode.getTypeArguments();
1255
+ const firstTypeArg = typeArgs[0];
1256
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1257
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
1258
+ return mode === "arrayOf" ? `Array<${inner}>` : inner;
1259
+ }
1260
+ return mode === "arrayOf" ? "Array<unknown>" : "unknown";
1261
+ }
1869
1262
  function expandTypeDecl(result, project, depth) {
1870
1263
  if (depth < 0) return "unknown";
1871
1264
  switch (result.kind) {
@@ -1931,7 +1324,7 @@ function extractParamsType(method, sourceFile, project) {
1931
1324
  const paramArgs = paramDecorator.getArguments();
1932
1325
  if (paramArgs.length === 0) continue;
1933
1326
  const nameArg = paramArgs[0];
1934
- if (!import_ts_morph6.Node.isStringLiteral(nameArg)) continue;
1327
+ if (!import_ts_morph5.Node.isStringLiteral(nameArg)) continue;
1935
1328
  const paramName = nameArg.getLiteralValue();
1936
1329
  const typeNode = param.getTypeNode();
1937
1330
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -1944,13 +1337,13 @@ function extractResponseType(method, sourceFile, project) {
1944
1337
  if (apiResponseDecorator) {
1945
1338
  const args = apiResponseDecorator.getArguments();
1946
1339
  const optsArg = args[0];
1947
- if (optsArg && import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) {
1340
+ if (optsArg && import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) {
1948
1341
  for (const prop of optsArg.getProperties()) {
1949
- if (!import_ts_morph6.Node.isPropertyAssignment(prop)) continue;
1342
+ if (!import_ts_morph5.Node.isPropertyAssignment(prop)) continue;
1950
1343
  if (prop.getName() !== "type") continue;
1951
1344
  const val = prop.getInitializer();
1952
1345
  if (!val) continue;
1953
- if (import_ts_morph6.Node.isArrayLiteralExpression(val)) {
1346
+ if (import_ts_morph5.Node.isArrayLiteralExpression(val)) {
1954
1347
  const elements = val.getElements();
1955
1348
  const firstEl = elements[0];
1956
1349
  if (elements.length > 0 && firstEl !== void 0) {
@@ -1970,7 +1363,7 @@ function extractResponseType(method, sourceFile, project) {
1970
1363
  return "unknown";
1971
1364
  }
1972
1365
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
1973
- if (!import_ts_morph6.Node.isIdentifier(node)) return "unknown";
1366
+ if (!import_ts_morph5.Node.isIdentifier(node)) return "unknown";
1974
1367
  const name = node.getText();
1975
1368
  const resolved = findType(name, sourceFile, project);
1976
1369
  if (resolved) {
@@ -2017,11 +1410,11 @@ function extractDtoContract(method, sourceFile, project) {
2017
1410
  if (apiResp) {
2018
1411
  const args = apiResp.getArguments();
2019
1412
  const optsArg = args[0];
2020
- if (optsArg && import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) {
1413
+ if (optsArg && import_ts_morph5.Node.isObjectLiteralExpression(optsArg)) {
2021
1414
  for (const prop of optsArg.getProperties()) {
2022
- if (import_ts_morph6.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
1415
+ if (import_ts_morph5.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
2023
1416
  const val = prop.getInitializer();
2024
- if (val && import_ts_morph6.Node.isIdentifier(val)) {
1417
+ if (val && import_ts_morph5.Node.isIdentifier(val)) {
2025
1418
  const name = val.getText();
2026
1419
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
2027
1420
  if (localDecl?.isExported()) {
@@ -2038,27 +1431,18 @@ function extractDtoContract(method, sourceFile, project) {
2038
1431
  }
2039
1432
  }
2040
1433
  }
2041
- let bodyZodText = null;
2042
- let queryZodText = null;
2043
1434
  let bodySchema = null;
2044
1435
  let querySchema = null;
2045
- const formNested = {};
2046
1436
  const formWarnings = [];
2047
1437
  const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
2048
1438
  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
1439
  bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
1440
+ formWarnings.push(...bodySchema.warnings);
2054
1441
  }
2055
1442
  const queryClass = resolveParamClass(method, "Query", sourceFile, project);
2056
1443
  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
1444
  querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
1445
+ formWarnings.push(...querySchema.warnings);
2062
1446
  }
2063
1447
  return {
2064
1448
  query,
@@ -2071,9 +1455,6 @@ function extractDtoContract(method, sourceFile, project) {
2071
1455
  filterFields: filterInfo?.fieldNames ?? null,
2072
1456
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
2073
1457
  filterSource: filterInfo?.source ?? null,
2074
- bodyZodText,
2075
- queryZodText,
2076
- formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
2077
1458
  formWarnings,
2078
1459
  bodySchema,
2079
1460
  querySchema
@@ -2093,6 +1474,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
2093
1474
  }
2094
1475
  return null;
2095
1476
  }
1477
+
1478
+ // src/discovery/zod-ast-to-ts.ts
1479
+ var import_ts_morph6 = require("ts-morph");
1480
+ function zodAstToTs(node) {
1481
+ if (!import_ts_morph6.Node.isCallExpression(node)) return "unknown";
1482
+ const expr = node.getExpression();
1483
+ if (import_ts_morph6.Node.isPropertyAccessExpression(expr)) {
1484
+ const methodName = expr.getName();
1485
+ const receiver = expr.getExpression();
1486
+ if (methodName === "optional") {
1487
+ return `${zodAstToTs(receiver)} | undefined`;
1488
+ }
1489
+ if (methodName === "nullable") {
1490
+ return `${zodAstToTs(receiver)} | null`;
1491
+ }
1492
+ const args = node.getArguments();
1493
+ switch (methodName) {
1494
+ case "string":
1495
+ return "string";
1496
+ case "number":
1497
+ return "number";
1498
+ case "boolean":
1499
+ return "boolean";
1500
+ case "unknown":
1501
+ return "unknown";
1502
+ case "any":
1503
+ return "unknown";
1504
+ case "literal": {
1505
+ const lit = args[0];
1506
+ if (!lit) return "unknown";
1507
+ if (import_ts_morph6.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
1508
+ if (import_ts_morph6.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
1509
+ if (lit.getKind() === import_ts_morph6.SyntaxKind.TrueKeyword) return "true";
1510
+ if (lit.getKind() === import_ts_morph6.SyntaxKind.FalseKeyword) return "false";
1511
+ return "unknown";
1512
+ }
1513
+ case "enum": {
1514
+ const arrArg = args[0];
1515
+ if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
1516
+ const members = arrArg.getElements().map(
1517
+ (el) => import_ts_morph6.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
1518
+ );
1519
+ return members.join(" | ");
1520
+ }
1521
+ case "array": {
1522
+ const inner = args[0];
1523
+ if (!inner) return "unknown";
1524
+ return `Array<${zodAstToTs(inner)}>`;
1525
+ }
1526
+ case "object": {
1527
+ const objArg = args[0];
1528
+ if (!objArg || !import_ts_morph6.Node.isObjectLiteralExpression(objArg)) return "unknown";
1529
+ const lines = [];
1530
+ for (const prop of objArg.getProperties()) {
1531
+ if (!import_ts_morph6.Node.isPropertyAssignment(prop)) continue;
1532
+ const key = prop.getName();
1533
+ const valNode = prop.getInitializer();
1534
+ if (!valNode) continue;
1535
+ const tsType = zodAstToTs(valNode);
1536
+ const isOpt = isOptionalChain(valNode);
1537
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
1538
+ }
1539
+ return `{ ${lines.join("; ")} }`;
1540
+ }
1541
+ case "union": {
1542
+ const arrArg = args[0];
1543
+ if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
1544
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
1545
+ }
1546
+ case "record": {
1547
+ const valArg = args.length === 1 ? args[0] : args[1];
1548
+ if (!valArg) return "unknown";
1549
+ return `Record<string, ${zodAstToTs(valArg)}>`;
1550
+ }
1551
+ case "tuple": {
1552
+ const arrArg = args[0];
1553
+ if (!arrArg || !import_ts_morph6.Node.isArrayLiteralExpression(arrArg)) return "unknown";
1554
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
1555
+ }
1556
+ default:
1557
+ return "unknown";
1558
+ }
1559
+ }
1560
+ return "unknown";
1561
+ }
1562
+ function isOptionalChain(node) {
1563
+ if (!import_ts_morph6.Node.isCallExpression(node)) return false;
1564
+ const expr = node.getExpression();
1565
+ return import_ts_morph6.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
1566
+ }
1567
+ function parseDefineContractCall(callExpr) {
1568
+ if (!import_ts_morph6.Node.isCallExpression(callExpr)) return null;
1569
+ const callee = callExpr.getExpression();
1570
+ const calleeName = import_ts_morph6.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph6.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
1571
+ if (calleeName !== "defineContract") return null;
1572
+ const args = callExpr.getArguments();
1573
+ const optsArg = args[0];
1574
+ if (!optsArg || !import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) return null;
1575
+ let query = null;
1576
+ let body = null;
1577
+ let response = "unknown";
1578
+ let bodyZodText = null;
1579
+ let queryZodText = null;
1580
+ for (const prop of optsArg.getProperties()) {
1581
+ if (!import_ts_morph6.Node.isPropertyAssignment(prop)) continue;
1582
+ const propName = prop.getName();
1583
+ const val = prop.getInitializer();
1584
+ if (!val) continue;
1585
+ if (propName === "query") {
1586
+ query = zodAstToTs(val);
1587
+ queryZodText = val.getText();
1588
+ } else if (propName === "body") {
1589
+ body = zodAstToTs(val);
1590
+ bodyZodText = val.getText();
1591
+ } else if (propName === "response") {
1592
+ response = zodAstToTs(val);
1593
+ }
1594
+ }
1595
+ return { query, body, response, bodyZodText, queryZodText };
1596
+ }
1597
+
1598
+ // src/discovery/contracts-fast.ts
1599
+ async function discoverContractsFast(opts) {
1600
+ const { cwd, glob, tsconfig } = opts;
1601
+ const tsconfigPath = tsconfig ? (0, import_node_path3.resolve)(tsconfig) : (0, import_node_path3.join)(cwd, "tsconfig.json");
1602
+ let project;
1603
+ try {
1604
+ project = new import_ts_morph7.Project({
1605
+ tsConfigFilePath: tsconfigPath,
1606
+ skipAddingFilesFromTsConfig: true,
1607
+ skipLoadingLibFiles: true,
1608
+ skipFileDependencyResolution: true
1609
+ });
1610
+ } catch {
1611
+ project = new import_ts_morph7.Project({
1612
+ skipAddingFilesFromTsConfig: true,
1613
+ skipLoadingLibFiles: true,
1614
+ skipFileDependencyResolution: true,
1615
+ compilerOptions: {
1616
+ allowJs: true,
1617
+ resolveJsonModule: false,
1618
+ strict: false
1619
+ }
1620
+ });
1621
+ }
1622
+ const files = await (0, import_fast_glob.default)(glob, { cwd, absolute: true, onlyFiles: true });
1623
+ for (const f of files) {
1624
+ project.addSourceFileAtPath(f);
1625
+ }
1626
+ const routes = [];
1627
+ setDiscoveryContext(project, {
1628
+ projectRoot: cwd,
1629
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
1630
+ });
1631
+ for (const sourceFile of project.getSourceFiles()) {
1632
+ routes.push(...extractFromSourceFile(sourceFile, project));
1633
+ }
1634
+ return routes;
1635
+ }
1636
+ function decoratorStringArg(decoratorExpr) {
1637
+ if (!decoratorExpr) return void 0;
1638
+ if (import_ts_morph7.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
1639
+ if (import_ts_morph7.Node.isArrayLiteralExpression(decoratorExpr)) {
1640
+ const first = decoratorExpr.getElements()[0];
1641
+ if (first && import_ts_morph7.Node.isStringLiteral(first)) return first.getLiteralValue();
1642
+ }
1643
+ return void 0;
1644
+ }
1645
+ function deriveClassSegment(className) {
1646
+ const noSuffix = className.replace(/Controller$/, "");
1647
+ if (!noSuffix) {
1648
+ throw new Error(
1649
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
1650
+ );
1651
+ }
1652
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
1653
+ }
1654
+ function resolveRouteName(className, methodName, classAs, methodAs) {
1655
+ const classPortion = classAs ?? deriveClassSegment(className);
1656
+ const methodPortion = methodAs ?? methodName;
1657
+ return `${classPortion}.${methodPortion}`;
1658
+ }
1659
+ function joinPaths(prefix, suffix) {
1660
+ if (!prefix && !suffix) return "/";
1661
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
1662
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
1663
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1664
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
1665
+ const combined = p + s;
1666
+ return combined === "" ? "/" : combined;
1667
+ }
1668
+ function extractParams(path) {
1669
+ const matches = path.matchAll(/:(\w+)/g);
1670
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
1671
+ }
2096
1672
  var HTTP_METHOD_DECORATORS = {
2097
1673
  Get: "GET",
2098
1674
  Post: "POST",
@@ -2103,176 +1679,186 @@ var HTTP_METHOD_DECORATORS = {
2103
1679
  Head: "HEAD",
2104
1680
  All: "ALL"
2105
1681
  };
1682
+ function resolveVerb(method) {
1683
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
1684
+ const httpDecorator = method.getDecorator(decoratorName);
1685
+ if (httpDecorator) {
1686
+ const httpArgs = httpDecorator.getArguments();
1687
+ const pathArg = httpArgs[0];
1688
+ return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
1689
+ }
1690
+ }
1691
+ return null;
1692
+ }
1693
+ function readAsDecorator(node, label) {
1694
+ const asDecorator = node.getDecorator("As");
1695
+ if (!asDecorator) return void 0;
1696
+ const asName = decoratorStringArg(asDecorator.getArguments()[0]);
1697
+ if (!asName) {
1698
+ throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
1699
+ }
1700
+ return asName;
1701
+ }
1702
+ function buildRoute(args) {
1703
+ const {
1704
+ className,
1705
+ methodName,
1706
+ resolvedMethod,
1707
+ combinedPath,
1708
+ classAs,
1709
+ methodAs,
1710
+ sourceFile,
1711
+ seenNames,
1712
+ contractSource
1713
+ } = args;
1714
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
1715
+ const qualifiedRef = `${className}.${methodName}`;
1716
+ const existing = seenNames.get(routeName);
1717
+ if (existing !== void 0) {
1718
+ throw new Error(
1719
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
1720
+ );
1721
+ }
1722
+ seenNames.set(routeName, qualifiedRef);
1723
+ return {
1724
+ method: resolvedMethod,
1725
+ path: combinedPath,
1726
+ name: routeName,
1727
+ params: extractParams(combinedPath),
1728
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
1729
+ contract: { contractSource }
1730
+ };
1731
+ }
1732
+ function extractContractRoute(args) {
1733
+ const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
1734
+ const firstDecoratorArg = applyContractDecorator.getArguments()[0];
1735
+ if (!firstDecoratorArg) return null;
1736
+ let contractDef = null;
1737
+ let bodyZodRef = null;
1738
+ let queryZodRef = null;
1739
+ if (import_ts_morph7.Node.isCallExpression(firstDecoratorArg)) {
1740
+ contractDef = parseDefineContractCall(firstDecoratorArg);
1741
+ } else if (import_ts_morph7.Node.isIdentifier(firstDecoratorArg)) {
1742
+ const identName = firstDecoratorArg.getText();
1743
+ const varDecl = sourceFile.getVariableDeclaration(identName);
1744
+ if (!varDecl) {
1745
+ console.warn(
1746
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
1747
+ );
1748
+ return null;
1749
+ }
1750
+ const initializer = varDecl.getInitializer();
1751
+ if (!initializer) return null;
1752
+ contractDef = parseDefineContractCall(initializer);
1753
+ if (contractDef && varDecl.isExported()) {
1754
+ const filePath = sourceFile.getFilePath();
1755
+ if (contractDef.body !== null) {
1756
+ bodyZodRef = { name: `${identName}.body`, filePath };
1757
+ }
1758
+ if (contractDef.query !== null) {
1759
+ queryZodRef = { name: `${identName}.query`, filePath };
1760
+ }
1761
+ }
1762
+ } else {
1763
+ console.warn(
1764
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
1765
+ );
1766
+ return null;
1767
+ }
1768
+ if (!contractDef) return null;
1769
+ if (!verb) return null;
1770
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
1771
+ const methodName = method.getName();
1772
+ const classAs = readAsDecorator(cls, `class ${className}`);
1773
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
1774
+ return buildRoute({
1775
+ className,
1776
+ methodName,
1777
+ resolvedMethod: verb.httpMethod,
1778
+ combinedPath: resolvedPath,
1779
+ classAs,
1780
+ methodAs,
1781
+ sourceFile,
1782
+ seenNames,
1783
+ contractSource: {
1784
+ query: contractDef.query,
1785
+ body: contractDef.body,
1786
+ response: contractDef.response,
1787
+ // Path A: capture both the importable ref and the raw text. The emitter
1788
+ // prefers inlining the text (client-safe — re-exporting from a controller
1789
+ // would drag server-only deps into the client bundle).
1790
+ bodyZodRef,
1791
+ bodyZodText: contractDef.bodyZodText,
1792
+ queryZodRef,
1793
+ queryZodText: contractDef.queryZodText
1794
+ }
1795
+ });
1796
+ }
1797
+ function extractDtoRoute(args) {
1798
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
1799
+ if (!verb) return null;
1800
+ const combined = joinPaths(prefix, verb.handlerPath);
1801
+ const methodName = method.getName();
1802
+ const classAs = readAsDecorator(cls, `class ${className}`);
1803
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
1804
+ const dtoContract = extractDtoContract(method, sourceFile, project);
1805
+ return buildRoute({
1806
+ className,
1807
+ methodName,
1808
+ resolvedMethod: verb.httpMethod,
1809
+ combinedPath: combined,
1810
+ classAs,
1811
+ methodAs,
1812
+ sourceFile,
1813
+ seenNames,
1814
+ contractSource: {
1815
+ query: dtoContract?.query ?? null,
1816
+ body: dtoContract?.body ?? null,
1817
+ response: dtoContract?.response ?? "unknown",
1818
+ queryRef: dtoContract?.queryRef ?? null,
1819
+ bodyRef: dtoContract?.bodyRef ?? null,
1820
+ responseRef: dtoContract?.responseRef ?? null,
1821
+ filterFields: dtoContract?.filterFields ?? null,
1822
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
1823
+ filterSource: dtoContract?.filterSource ?? null,
1824
+ formWarnings: dtoContract?.formWarnings ?? [],
1825
+ bodySchema: dtoContract?.bodySchema ?? null,
1826
+ querySchema: dtoContract?.querySchema ?? null
1827
+ }
1828
+ });
1829
+ }
2106
1830
  function extractFromSourceFile(sourceFile, project) {
2107
1831
  const routes = [];
2108
1832
  const seenNames = /* @__PURE__ */ new Map();
2109
- const classes = sourceFile.getClasses();
2110
- for (const cls of classes) {
1833
+ for (const cls of sourceFile.getClasses()) {
2111
1834
  const controllerDecorator = cls.getDecorator("Controller");
2112
1835
  if (!controllerDecorator) continue;
2113
- const controllerArgs = controllerDecorator.getArguments();
2114
- const firstArg3 = controllerArgs[0];
2115
- const prefix = decoratorStringArg(firstArg3) ?? "";
1836
+ const firstArg2 = controllerDecorator.getArguments()[0];
1837
+ const prefix = decoratorStringArg(firstArg2) ?? "";
2116
1838
  const className = cls.getName() ?? "Unknown";
2117
1839
  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
- }
1840
+ const verb = resolveVerb(method);
2130
1841
  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
- }
1842
+ const route = applyContractDecorator ? extractContractRoute({
1843
+ cls,
1844
+ method,
1845
+ applyContractDecorator,
1846
+ verb,
1847
+ prefix,
1848
+ className,
1849
+ sourceFile,
1850
+ seenNames
1851
+ }) : extractDtoRoute({
1852
+ cls,
1853
+ method,
1854
+ verb,
1855
+ prefix,
1856
+ className,
1857
+ sourceFile,
1858
+ project,
1859
+ seenNames
1860
+ });
1861
+ if (route) routes.push(route);
2276
1862
  }
2277
1863
  }
2278
1864
  return routes;
@@ -2280,8 +1866,7 @@ function extractFromSourceFile(sourceFile, project) {
2280
1866
 
2281
1867
  // src/generate.ts
2282
1868
  var import_promises9 = require("fs/promises");
2283
- var import_node_path11 = require("path");
2284
- var import_ts_morph9 = require("ts-morph");
1869
+ var import_node_path12 = require("path");
2285
1870
 
2286
1871
  // src/discovery/pages.ts
2287
1872
  var import_promises2 = require("fs/promises");
@@ -2336,7 +1921,8 @@ function extractPropsSource(source, exportName) {
2336
1921
  }
2337
1922
 
2338
1923
  // src/discovery/shared-props.ts
2339
- var import_ts_morph7 = require("ts-morph");
1924
+ var import_node_path5 = require("path");
1925
+ var import_ts_morph8 = require("ts-morph");
2340
1926
  function discoverSharedProps(project, moduleEntry) {
2341
1927
  try {
2342
1928
  let sourceFile = project.getSourceFile(moduleEntry);
@@ -2356,14 +1942,39 @@ function discoverSharedProps(project, moduleEntry) {
2356
1942
  return null;
2357
1943
  }
2358
1944
  }
1945
+ function discoverSharedPropsFromConfig(config) {
1946
+ if (!config.app?.moduleEntry) return null;
1947
+ try {
1948
+ const tsconfigPath = config.app.tsconfig ?? (0, import_node_path5.join)(config.codegen.cwd, "tsconfig.json");
1949
+ let project;
1950
+ try {
1951
+ project = new import_ts_morph8.Project({
1952
+ tsConfigFilePath: tsconfigPath,
1953
+ skipAddingFilesFromTsConfig: true,
1954
+ skipLoadingLibFiles: true,
1955
+ skipFileDependencyResolution: true
1956
+ });
1957
+ } catch {
1958
+ project = new import_ts_morph8.Project({
1959
+ skipAddingFilesFromTsConfig: true,
1960
+ skipLoadingLibFiles: true,
1961
+ skipFileDependencyResolution: true,
1962
+ compilerOptions: { allowJs: true, strict: false }
1963
+ });
1964
+ }
1965
+ return discoverSharedProps(project, config.app.moduleEntry);
1966
+ } catch {
1967
+ return null;
1968
+ }
1969
+ }
2359
1970
  function findForRootCall(sourceFile) {
2360
- const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph7.SyntaxKind.CallExpression);
1971
+ const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph8.SyntaxKind.CallExpression);
2361
1972
  for (const call of callExpressions) {
2362
1973
  const expr = call.getExpression();
2363
- if (!import_ts_morph7.Node.isPropertyAccessExpression(expr)) continue;
1974
+ if (!import_ts_morph8.Node.isPropertyAccessExpression(expr)) continue;
2364
1975
  const methodName = expr.getName();
2365
1976
  const objectExpr = expr.getExpression();
2366
- if (methodName === "forRoot" && import_ts_morph7.Node.isIdentifier(objectExpr)) {
1977
+ if (methodName === "forRoot" && import_ts_morph8.Node.isIdentifier(objectExpr)) {
2367
1978
  const name = objectExpr.getText();
2368
1979
  if (name === "InertiaModule") {
2369
1980
  return call;
@@ -2373,19 +1984,19 @@ function findForRootCall(sourceFile) {
2373
1984
  return null;
2374
1985
  }
2375
1986
  function findShareInitializer(forRootCall) {
2376
- if (!import_ts_morph7.Node.isCallExpression(forRootCall)) return null;
1987
+ if (!import_ts_morph8.Node.isCallExpression(forRootCall)) return null;
2377
1988
  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") {
1989
+ const firstArg2 = args[0];
1990
+ if (!firstArg2 || !import_ts_morph8.Node.isObjectLiteralExpression(firstArg2)) return null;
1991
+ for (const prop of firstArg2.getProperties()) {
1992
+ if (import_ts_morph8.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
2382
1993
  return prop.getInitializer() ?? null;
2383
1994
  }
2384
1995
  }
2385
1996
  return null;
2386
1997
  }
2387
1998
  function extractShareType(node, sourceFile, project) {
2388
- if (import_ts_morph7.Node.isIdentifier(node)) {
1999
+ if (import_ts_morph8.Node.isIdentifier(node)) {
2389
2000
  const ref = resolveIdentifierToImportRef(node, sourceFile, project);
2390
2001
  if (ref) {
2391
2002
  return {
@@ -2395,22 +2006,22 @@ function extractShareType(node, sourceFile, project) {
2395
2006
  };
2396
2007
  }
2397
2008
  }
2398
- if (import_ts_morph7.Node.isArrowFunction(node)) {
2009
+ if (import_ts_morph8.Node.isArrowFunction(node)) {
2399
2010
  const result = extractFromFunctionLike(node, sourceFile);
2400
2011
  return result ? { ...result, isImportRef: false } : null;
2401
2012
  }
2402
- if (import_ts_morph7.Node.isFunctionExpression(node)) {
2013
+ if (import_ts_morph8.Node.isFunctionExpression(node)) {
2403
2014
  const result = extractFromFunctionLike(node, sourceFile);
2404
2015
  return result ? { ...result, isImportRef: false } : null;
2405
2016
  }
2406
- if (import_ts_morph7.Node.isObjectLiteralExpression(node)) {
2017
+ if (import_ts_morph8.Node.isObjectLiteralExpression(node)) {
2407
2018
  const result = extractFromObjectLiteral(node);
2408
2019
  return result ? { ...result, isImportRef: false } : null;
2409
2020
  }
2410
2021
  return null;
2411
2022
  }
2412
2023
  function resolveIdentifierToImportRef(id, sourceFile, project) {
2413
- if (!import_ts_morph7.Node.isIdentifier(id)) return null;
2024
+ if (!import_ts_morph8.Node.isIdentifier(id)) return null;
2414
2025
  const name = id.getText();
2415
2026
  const localFunc = sourceFile.getFunction(name);
2416
2027
  if (localFunc?.isExported()) {
@@ -2444,49 +2055,49 @@ function resolveIdentifierToImportRef(id, sourceFile, project) {
2444
2055
  return null;
2445
2056
  }
2446
2057
  function extractFromFunctionLike(node, _sourceFile) {
2447
- const returnTypeNode = import_ts_morph7.Node.isArrowFunction(node) || import_ts_morph7.Node.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
2058
+ const returnTypeNode = import_ts_morph8.Node.isArrowFunction(node) || import_ts_morph8.Node.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
2448
2059
  if (returnTypeNode) {
2449
2060
  return extractFromReturnTypeAnnotation(returnTypeNode);
2450
2061
  }
2451
- if (import_ts_morph7.Node.isArrowFunction(node)) {
2062
+ if (import_ts_morph8.Node.isArrowFunction(node)) {
2452
2063
  const body = node.getBody();
2453
- if (import_ts_morph7.Node.isParenthesizedExpression(body)) {
2064
+ if (import_ts_morph8.Node.isParenthesizedExpression(body)) {
2454
2065
  const inner = body.getExpression();
2455
- if (import_ts_morph7.Node.isObjectLiteralExpression(inner)) {
2066
+ if (import_ts_morph8.Node.isObjectLiteralExpression(inner)) {
2456
2067
  return extractFromObjectLiteral(inner);
2457
2068
  }
2458
2069
  }
2459
- if (import_ts_morph7.Node.isObjectLiteralExpression(body)) {
2070
+ if (import_ts_morph8.Node.isObjectLiteralExpression(body)) {
2460
2071
  return extractFromObjectLiteral(body);
2461
2072
  }
2462
- if (import_ts_morph7.Node.isBlock(body)) {
2073
+ if (import_ts_morph8.Node.isBlock(body)) {
2463
2074
  return extractFromBlockReturn(body);
2464
2075
  }
2465
2076
  }
2466
- if (import_ts_morph7.Node.isFunctionExpression(node)) {
2077
+ if (import_ts_morph8.Node.isFunctionExpression(node)) {
2467
2078
  const body = node.getBody();
2468
- if (import_ts_morph7.Node.isBlock(body)) {
2079
+ if (import_ts_morph8.Node.isBlock(body)) {
2469
2080
  return extractFromBlockReturn(body);
2470
2081
  }
2471
2082
  }
2472
2083
  return null;
2473
2084
  }
2474
2085
  function extractFromReturnTypeAnnotation(typeNode) {
2475
- if (import_ts_morph7.Node.isTypeReference(typeNode)) {
2086
+ if (import_ts_morph8.Node.isTypeReference(typeNode)) {
2476
2087
  const typeName = typeNode.getTypeName();
2477
- if (import_ts_morph7.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
2088
+ if (import_ts_morph8.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
2478
2089
  const typeArgs = typeNode.getTypeArguments();
2479
- const firstArg3 = typeArgs[0];
2480
- if (firstArg3) {
2481
- return extractFromReturnTypeAnnotation(firstArg3);
2090
+ const firstArg2 = typeArgs[0];
2091
+ if (firstArg2) {
2092
+ return extractFromReturnTypeAnnotation(firstArg2);
2482
2093
  }
2483
2094
  return null;
2484
2095
  }
2485
2096
  }
2486
- if (import_ts_morph7.Node.isTypeLiteral(typeNode)) {
2097
+ if (import_ts_morph8.Node.isTypeLiteral(typeNode)) {
2487
2098
  const properties = [];
2488
2099
  for (const member of typeNode.getMembers()) {
2489
- if (import_ts_morph7.Node.isPropertySignature(member)) {
2100
+ if (import_ts_morph8.Node.isPropertySignature(member)) {
2490
2101
  const name = member.getName();
2491
2102
  const memberTypeNode = member.getTypeNode();
2492
2103
  const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
@@ -2500,19 +2111,19 @@ function extractFromReturnTypeAnnotation(typeNode) {
2500
2111
  return null;
2501
2112
  }
2502
2113
  function extractFromBlockReturn(block) {
2503
- if (!import_ts_morph7.Node.isBlock(block)) return null;
2114
+ if (!import_ts_morph8.Node.isBlock(block)) return null;
2504
2115
  const statements = block.getStatements();
2505
2116
  for (let i = statements.length - 1; i >= 0; i--) {
2506
2117
  const stmt = statements[i];
2507
- if (!import_ts_morph7.Node.isReturnStatement(stmt)) continue;
2118
+ if (!import_ts_morph8.Node.isReturnStatement(stmt)) continue;
2508
2119
  const expr = stmt.getExpression();
2509
2120
  if (!expr) continue;
2510
- if (import_ts_morph7.Node.isObjectLiteralExpression(expr)) {
2121
+ if (import_ts_morph8.Node.isObjectLiteralExpression(expr)) {
2511
2122
  return extractFromObjectLiteral(expr);
2512
2123
  }
2513
- if (import_ts_morph7.Node.isParenthesizedExpression(expr)) {
2124
+ if (import_ts_morph8.Node.isParenthesizedExpression(expr)) {
2514
2125
  const inner = expr.getExpression();
2515
- if (import_ts_morph7.Node.isObjectLiteralExpression(inner)) {
2126
+ if (import_ts_morph8.Node.isObjectLiteralExpression(inner)) {
2516
2127
  return extractFromObjectLiteral(inner);
2517
2128
  }
2518
2129
  }
@@ -2521,10 +2132,10 @@ function extractFromBlockReturn(block) {
2521
2132
  return null;
2522
2133
  }
2523
2134
  function extractFromObjectLiteral(objLiteral) {
2524
- if (!import_ts_morph7.Node.isObjectLiteralExpression(objLiteral)) return null;
2135
+ if (!import_ts_morph8.Node.isObjectLiteralExpression(objLiteral)) return null;
2525
2136
  const properties = [];
2526
2137
  for (const prop of objLiteral.getProperties()) {
2527
- if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
2138
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
2528
2139
  const name = prop.getName();
2529
2140
  const initializer = prop.getInitializer();
2530
2141
  if (!initializer) continue;
@@ -2536,21 +2147,21 @@ function extractFromObjectLiteral(objLiteral) {
2536
2147
  return { typeString, properties, isImportRef: false };
2537
2148
  }
2538
2149
  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))
2150
+ if (import_ts_morph8.Node.isStringLiteral(node)) return "string";
2151
+ if (import_ts_morph8.Node.isTemplateExpression(node) || import_ts_morph8.Node.isNoSubstitutionTemplateLiteral(node))
2541
2152
  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) {
2153
+ if (import_ts_morph8.Node.isNumericLiteral(node)) return "number";
2154
+ if (node.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword || node.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) {
2544
2155
  return "boolean";
2545
2156
  }
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)) {
2157
+ if (node.getKind() === import_ts_morph8.SyntaxKind.NullKeyword) return "null";
2158
+ if (import_ts_morph8.Node.isIdentifier(node) && node.getText() === "undefined") return "undefined";
2159
+ if (import_ts_morph8.Node.isObjectLiteralExpression(node)) {
2549
2160
  const props = node.getProperties();
2550
2161
  if (props.length === 0) return "Record<string, unknown>";
2551
2162
  const entries = [];
2552
2163
  for (const prop of props) {
2553
- if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
2164
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
2554
2165
  const key = prop.getName();
2555
2166
  const init = prop.getInitializer();
2556
2167
  if (!init) continue;
@@ -2559,23 +2170,23 @@ function inferExpressionType(node) {
2559
2170
  if (entries.length === 0) return "Record<string, unknown>";
2560
2171
  return `{ ${entries.join("; ")} }`;
2561
2172
  }
2562
- if (import_ts_morph7.Node.isArrayLiteralExpression(node)) {
2173
+ if (import_ts_morph8.Node.isArrayLiteralExpression(node)) {
2563
2174
  const elements = node.getElements();
2564
2175
  if (elements.length === 0) return "Array<unknown>";
2565
2176
  const first = elements[0];
2566
2177
  if (first) return `Array<${inferExpressionType(first)}>`;
2567
2178
  return "Array<unknown>";
2568
2179
  }
2569
- if (import_ts_morph7.Node.isConditionalExpression(node)) {
2180
+ if (import_ts_morph8.Node.isConditionalExpression(node)) {
2570
2181
  const whenTrue = inferExpressionType(node.getWhenTrue());
2571
2182
  const whenFalse = inferExpressionType(node.getWhenFalse());
2572
2183
  if (whenTrue === whenFalse) return whenTrue;
2573
2184
  return `${whenTrue} | ${whenFalse}`;
2574
2185
  }
2575
- if (import_ts_morph7.Node.isParenthesizedExpression(node)) {
2186
+ if (import_ts_morph8.Node.isParenthesizedExpression(node)) {
2576
2187
  return inferExpressionType(node.getExpression());
2577
2188
  }
2578
- if (import_ts_morph7.Node.isAsExpression(node)) {
2189
+ if (import_ts_morph8.Node.isAsExpression(node)) {
2579
2190
  const typeNode = node.getTypeNode();
2580
2191
  if (typeNode) return typeNode.getText();
2581
2192
  }
@@ -2584,25 +2195,14 @@ function inferExpressionType(node) {
2584
2195
 
2585
2196
  // src/emit/emit-api.ts
2586
2197
  var import_promises3 = require("fs/promises");
2587
- var import_node_path5 = require("path");
2198
+ var import_node_path6 = require("path");
2588
2199
 
2589
2200
  // src/extension/registry.ts
2590
- var import_ts_morph8 = require("ts-morph");
2201
+ var import_ts_morph9 = require("ts-morph");
2591
2202
  function resolveApiSlots(extensions) {
2592
- let transport;
2593
- let transportOwner;
2594
- let layer;
2595
- let layerOwner;
2596
- 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
- }
2203
+ let layer;
2204
+ let layerOwner;
2205
+ for (const ext of extensions) {
2606
2206
  if (ext.apiClientLayer) {
2607
2207
  if (layer) {
2608
2208
  throw new CodegenError(
@@ -2614,11 +2214,22 @@ function resolveApiSlots(extensions) {
2614
2214
  }
2615
2215
  }
2616
2216
  return {
2617
- ...transport ? { transport } : {},
2618
2217
  ...layer ? { layer } : {}
2619
2218
  };
2620
2219
  }
2621
2220
  var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
2221
+ function mergeExclusive(target, incoming, {
2222
+ owner,
2223
+ describe
2224
+ }) {
2225
+ for (const [key, value] of incoming) {
2226
+ const prev = target.get(key);
2227
+ if (prev !== void 0) {
2228
+ throw new CodegenError(describe(key, prev.owner, owner));
2229
+ }
2230
+ target.set(key, { value, owner });
2231
+ }
2232
+ }
2622
2233
  function createExtensionContext(config, getRoutes) {
2623
2234
  let project;
2624
2235
  return {
@@ -2630,7 +2241,7 @@ function createExtensionContext(config, getRoutes) {
2630
2241
  },
2631
2242
  project() {
2632
2243
  if (!project) {
2633
- project = new import_ts_morph8.Project({
2244
+ project = new import_ts_morph9.Project({
2634
2245
  skipAddingFilesFromTsConfig: true,
2635
2246
  skipLoadingLibFiles: true,
2636
2247
  skipFileDependencyResolution: true,
@@ -2663,29 +2274,36 @@ async function collectEmittedFiles(extensions, ctx) {
2663
2274
  `Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
2664
2275
  );
2665
2276
  }
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);
2277
+ mergeExclusive(owners, [[key, file]], {
2278
+ owner: ext.name,
2279
+ describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
2280
+ });
2673
2281
  files.push(file);
2674
2282
  }
2675
2283
  }
2676
2284
  return files;
2677
2285
  }
2678
2286
 
2287
+ // src/extension/types.ts
2288
+ function requestShape(route) {
2289
+ const cs = route.contract?.contractSource;
2290
+ const isGet = route.method.toUpperCase() === "GET";
2291
+ const isQuery = isGet || !!cs?.filterFields?.length;
2292
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
2293
+ const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
2294
+ return { isGet, isQuery, hasBody, hasQuery };
2295
+ }
2296
+
2679
2297
  // src/emit/emit-api.ts
2680
2298
  async function emitApi(routes, outDir, opts = {}) {
2681
2299
  await (0, import_promises3.mkdir)(outDir, { recursive: true });
2682
2300
  const content = buildApiFile(routes, outDir, opts);
2683
- await (0, import_promises3.writeFile)((0, import_node_path5.join)(outDir, "api.ts"), content, "utf8");
2301
+ await (0, import_promises3.writeFile)((0, import_node_path6.join)(outDir, "api.ts"), content, "utf8");
2684
2302
  }
2685
2303
  function splitName(name) {
2686
2304
  return name.split(".");
2687
2305
  }
2688
- function toObjectKey3(segment) {
2306
+ function toObjectKey(segment) {
2689
2307
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
2690
2308
  return segment;
2691
2309
  }
@@ -2778,7 +2396,7 @@ function emitFilterQueryType(c) {
2778
2396
  }
2779
2397
  function buildResponseType(c, outDir) {
2780
2398
  if (c.controllerRef) {
2781
- let relPath = (0, import_node_path5.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
2399
+ let relPath = (0, import_node_path6.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
2782
2400
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
2783
2401
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
2784
2402
  }
@@ -2792,7 +2410,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
2792
2410
  const pad = " ".repeat(indent);
2793
2411
  const lines = [];
2794
2412
  for (const [key, node] of tree) {
2795
- const objKey = toObjectKey3(key);
2413
+ const objKey = toObjectKey(key);
2796
2414
  if (node.kind === "leaf") {
2797
2415
  const c = node;
2798
2416
  const method = c.method.toUpperCase();
@@ -2818,15 +2436,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
2818
2436
  return lines;
2819
2437
  }
2820
2438
  function buildRequestModel(c) {
2821
- const isGet = c.method.toUpperCase() === "GET";
2822
2439
  const m = c.method.toLowerCase();
2823
2440
  const flat = JSON.stringify(c.name);
2824
2441
  const path = JSON.stringify(c.path);
2825
2442
  const TA = buildRouterTypeAccess(c.name);
2826
2443
  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";
2444
+ const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
2830
2445
  const fields = [];
2831
2446
  if (withParams) fields.push(`params: ${TA}['params']`);
2832
2447
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -2848,7 +2463,6 @@ function buildRequestModel(c) {
2848
2463
  urlExpr,
2849
2464
  optsExpr,
2850
2465
  responseType: `${TA}['response']`,
2851
- bodyType: `${TA}['body']`,
2852
2466
  queryKeyExpr: `[${flat}, input] as const`
2853
2467
  };
2854
2468
  }
@@ -2896,7 +2510,7 @@ function emitApiObjectBlock(tree, indent, p) {
2896
2510
  const pad = " ".repeat(indent);
2897
2511
  const lines = [];
2898
2512
  for (const [key, node] of tree) {
2899
- const objKey = toObjectKey3(key);
2513
+ const objKey = toObjectKey(key);
2900
2514
  if (node.kind === "branch") {
2901
2515
  lines.push(`${pad}${objKey}: {`);
2902
2516
  lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
@@ -2904,30 +2518,28 @@ function emitApiObjectBlock(tree, indent, p) {
2904
2518
  continue;
2905
2519
  }
2906
2520
  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 } : {}
2521
+ const leaf = {
2522
+ route: node.route,
2523
+ request: req,
2524
+ requestExpr: renderFetcherRequest(req)
2914
2525
  };
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));
2526
+ const owned = /* @__PURE__ */ new Map();
2527
+ if (p.layer) {
2528
+ mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
2529
+ owner: p.layer.name,
2530
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
2531
+ });
2532
+ }
2919
2533
  for (const ext of p.memberExts) {
2920
2534
  const extra = ext.apiMembers?.(leaf, p.ctx);
2921
2535
  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
- }
2536
+ mergeExclusive(owned, Object.entries(extra), {
2537
+ owner: ext.name,
2538
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
2539
+ });
2930
2540
  }
2541
+ const members = {};
2542
+ for (const [name, { value }] of owned) members[name] = value;
2931
2543
  lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
2932
2544
  }
2933
2545
  return lines;
@@ -2936,10 +2548,82 @@ function buildRouterTypeAccess(name) {
2936
2548
  const segments = splitName(name);
2937
2549
  return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
2938
2550
  }
2551
+ var RESOLVER_HELPERS = [
2552
+ // --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
2553
+ "type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
2554
+ " ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
2555
+ " : P extends keyof R ? R[P] : never;",
2556
+ "",
2557
+ // --- ResolveByName: resolve a field from a dot-path name ---
2558
+ "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
2559
+ "",
2560
+ // --- ResolveByPath: scan all leaves for matching method + url ---
2561
+ // Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
2562
+ "type _LeafValues<T> = T extends { method: string; url: string }",
2563
+ " ? T",
2564
+ " : T extends object ? _LeafValues<T[keyof T]> : never;",
2565
+ "",
2566
+ "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
2567
+ " ? L extends { method: M; url: U }",
2568
+ " ? Field extends keyof L ? L[Field] : never",
2569
+ " : never",
2570
+ " : never;",
2571
+ ""
2572
+ ];
2573
+ var ROUTE_NAMESPACE = [
2574
+ "export namespace Route {",
2575
+ ' export type Response<K extends string> = ResolveByName<K, "response">;',
2576
+ ' export type Body<K extends string> = ResolveByName<K, "body">;',
2577
+ ' export type Query<K extends string> = ResolveByName<K, "query">;',
2578
+ ' export type Params<K extends string> = ResolveByName<K, "params">;',
2579
+ ' export type Error<K extends string> = ResolveByName<K, "error">;',
2580
+ ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
2581
+ " export type Request<K extends string> = {",
2582
+ " body: Body<K>;",
2583
+ " query: Query<K>;",
2584
+ " params: Params<K>;",
2585
+ " };",
2586
+ "}",
2587
+ ""
2588
+ ];
2589
+ var PATH_NAMESPACE = [
2590
+ "export namespace Path {",
2591
+ ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
2592
+ ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
2593
+ ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
2594
+ ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
2595
+ ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
2596
+ ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
2597
+ "}",
2598
+ ""
2599
+ ];
2600
+ var EMPTY_ROUTE_NAMESPACE = [
2601
+ "export namespace Route {",
2602
+ " export type Response<K extends string> = never;",
2603
+ " export type Body<K extends string> = never;",
2604
+ " export type Query<K extends string> = never;",
2605
+ " export type Params<K extends string> = never;",
2606
+ " export type Error<K extends string> = never;",
2607
+ " export type FilterFields<K extends string> = never;",
2608
+ " export type Request<K extends string> = { body: never; query: never; params: never };",
2609
+ "}",
2610
+ ""
2611
+ ];
2612
+ var EMPTY_PATH_NAMESPACE = [
2613
+ "export namespace Path {",
2614
+ " export type Response<M extends string, U extends string> = never;",
2615
+ " export type Body<M extends string, U extends string> = never;",
2616
+ " export type Query<M extends string, U extends string> = never;",
2617
+ " export type Params<M extends string, U extends string> = never;",
2618
+ " export type Error<M extends string, U extends string> = never;",
2619
+ " export type FilterFields<M extends string, U extends string> = never;",
2620
+ "}",
2621
+ ""
2622
+ ];
2939
2623
  function buildApiFile(routes, outDir, opts = {}) {
2940
2624
  const fetcherImportPath = opts.fetcherImportPath;
2941
2625
  const extensions = opts.extensions ?? [];
2942
- const { transport, layer } = resolveApiSlots(extensions);
2626
+ const { layer } = resolveApiSlots(extensions);
2943
2627
  const memberExts = extensions.filter((e) => e.apiMembers);
2944
2628
  const headerExts = extensions.filter((e) => e.apiHeader);
2945
2629
  const contracted = routes.filter((r) => r.contract);
@@ -2984,7 +2668,6 @@ function buildApiFile(routes, outDir, opts = {}) {
2984
2668
  seenImports.add(imp);
2985
2669
  extImports.push(imp);
2986
2670
  };
2987
- for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
2988
2671
  for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
2989
2672
  for (const ext of headerExts) {
2990
2673
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
@@ -3000,8 +2683,8 @@ function buildApiFile(routes, outDir, opts = {}) {
3000
2683
  const emittedNames = /* @__PURE__ */ new Set();
3001
2684
  for (const [filePath, names] of importsByFile) {
3002
2685
  let relPath;
3003
- if ((0, import_node_path5.isAbsolute)(filePath)) {
3004
- relPath = (0, import_node_path5.relative)(outDir, filePath).replace(/\.ts$/, "");
2686
+ if ((0, import_node_path6.isAbsolute)(filePath)) {
2687
+ relPath = (0, import_node_path6.relative)(outDir, filePath).replace(/\.ts$/, "");
3005
2688
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
3006
2689
  } else {
3007
2690
  relPath = filePath;
@@ -3029,27 +2712,8 @@ function buildApiFile(routes, outDir, opts = {}) {
3029
2712
  lines.push("}");
3030
2713
  lines.push("export type Api = ReturnType<typeof createApi>;");
3031
2714
  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("");
2715
+ lines.push(...EMPTY_ROUTE_NAMESPACE);
2716
+ lines.push(...EMPTY_PATH_NAMESPACE);
3053
2717
  return lines.join("\n");
3054
2718
  }
3055
2719
  const tree = /* @__PURE__ */ new Map();
@@ -3067,7 +2731,8 @@ function buildApiFile(routes, outDir, opts = {}) {
3067
2731
  path: r.path,
3068
2732
  params: r.params,
3069
2733
  controllerRef: r.controllerRef,
3070
- contractSource: c.contractSource
2734
+ contractSource: c.contractSource,
2735
+ route: r
3071
2736
  };
3072
2737
  insertIntoTree(tree, segments, leaf, name);
3073
2738
  }
@@ -3080,7 +2745,6 @@ function buildApiFile(routes, outDir, opts = {}) {
3080
2745
  lines.push(" return {");
3081
2746
  lines.push(
3082
2747
  ...emitApiObjectBlock(tree, 4, {
3083
- ...transport ? { transport } : {},
3084
2748
  ...layer ? { layer } : {},
3085
2749
  memberExts,
3086
2750
  ctx
@@ -3091,61 +2755,9 @@ function buildApiFile(routes, outDir, opts = {}) {
3091
2755
  lines.push("");
3092
2756
  lines.push("export type Api = ReturnType<typeof createApi>;");
3093
2757
  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("");
2758
+ lines.push(...RESOLVER_HELPERS);
2759
+ lines.push(...ROUTE_NAMESPACE);
2760
+ lines.push(...PATH_NAMESPACE);
3149
2761
  for (const ext of headerExts) {
3150
2762
  const statements = ext.apiHeader?.(ctx)?.statements;
3151
2763
  if (statements?.length) {
@@ -3157,7 +2769,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3157
2769
 
3158
2770
  // src/emit/emit-cache.ts
3159
2771
  var import_promises4 = require("fs/promises");
3160
- var import_node_path6 = require("path");
2772
+ var import_node_path7 = require("path");
3161
2773
  async function emitCache(pages, outDir) {
3162
2774
  await (0, import_promises4.mkdir)(outDir, { recursive: true });
3163
2775
  const entries = await Promise.all(
@@ -3171,95 +2783,21 @@ async function emitCache(pages, outDir) {
3171
2783
  })
3172
2784
  );
3173
2785
  const cache = { pages: entries };
3174
- await (0, import_promises4.writeFile)((0, import_node_path6.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
2786
+ await (0, import_promises4.writeFile)((0, import_node_path7.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
3175
2787
  `, "utf8");
3176
2788
  }
3177
2789
 
3178
2790
  // src/emit/emit-forms.ts
3179
2791
  var import_promises5 = require("fs/promises");
3180
- var import_node_path7 = require("path");
2792
+ var import_node_path8 = require("path");
3181
2793
  async function emitForms(routes, outDir, config, adapter) {
3182
2794
  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;
2795
+ const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
2796
+ if (content === null) return false;
3192
2797
  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");
2798
+ await (0, import_promises5.writeFile)((0, import_node_path8.join)(outDir, "forms.ts"), content, "utf8");
3195
2799
  return true;
3196
2800
  }
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
2801
  function pascal(segment) {
3264
2802
  return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
3265
2803
  }
@@ -3269,124 +2807,16 @@ function deriveBaseName(routeName) {
3269
2807
  const full = segments.map(pascal).join("");
3270
2808
  return { method, full };
3271
2809
  }
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
2810
  function relImport(outDir, filePath) {
3304
- let relPath = (0, import_node_path7.relative)(outDir, filePath).replace(/\.ts$/, "");
2811
+ let relPath = (0, import_node_path8.relative)(outDir, filePath).replace(/\.ts$/, "");
3305
2812
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
3306
2813
  return relPath;
3307
2814
  }
3308
2815
  function refRootIdentifier(refName) {
3309
2816
  return refName.split(".")[0] ?? refName;
3310
2817
  }
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");
2818
+ function hasSource(src) {
2819
+ return !!(src.schema || src.zodText || src.zodRef);
3390
2820
  }
3391
2821
  function applyRenames(text, renames) {
3392
2822
  if (!renames || renames.size === 0) return text;
@@ -3452,20 +2882,182 @@ function planNestedSchemas(entries) {
3452
2882
  }
3453
2883
  return { globalSchemas, renamesByEntry };
3454
2884
  }
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}`;
2885
+ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
2886
+ const acceptsRawZod = adapter.acceptsRawZodSource === true;
2887
+ const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
2888
+ const methodNameCounts = /* @__PURE__ */ new Map();
2889
+ const candidates = [];
2890
+ for (const route of sorted) {
2891
+ const cs = route.contract.contractSource;
2892
+ const body = {
2893
+ schema: cs.bodySchema ?? null,
2894
+ zodText: cs.bodyZodText ?? null,
2895
+ zodRef: cs.bodyZodRef ?? null
2896
+ };
2897
+ const query = {
2898
+ schema: cs.querySchema ?? null,
2899
+ zodText: cs.queryZodText ?? null,
2900
+ zodRef: cs.queryZodRef ?? null
2901
+ };
2902
+ if (!hasSource(body) && !hasSource(query)) continue;
2903
+ const { method, full } = deriveBaseName(route.name);
2904
+ methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
2905
+ candidates.push({
2906
+ routeName: route.name,
2907
+ baseName: full,
2908
+ // resolved below
2909
+ body: hasSource(body) ? body : void 0,
2910
+ query: hasSource(query) ? query : void 0,
2911
+ nestedSchemas: cs.formNestedSchemas ?? null,
2912
+ warnings: cs.formWarnings ?? []
2913
+ });
2914
+ }
2915
+ const entries = candidates.map((c) => {
2916
+ const { method, full } = deriveBaseName(c.routeName);
2917
+ const collision = (methodNameCounts.get(method) ?? 0) > 1;
2918
+ return { ...c, baseName: collision ? full : method };
2919
+ });
2920
+ if (entries.length === 0) return null;
2921
+ const importsByFile = /* @__PURE__ */ new Map();
2922
+ const refAlias = /* @__PURE__ */ new Map();
2923
+ for (const entry of entries) {
2924
+ for (const src of [entry.body, entry.query]) {
2925
+ if (src?.zodRef && !src.zodText && !src.schema) {
2926
+ const root = refRootIdentifier(src.zodRef.name);
2927
+ const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
2928
+ set.add(root);
2929
+ importsByFile.set(src.zodRef.filePath, set);
2930
+ }
2931
+ }
2932
+ }
2933
+ const importLines = [];
2934
+ if (importsByFile.size > 0) {
2935
+ const emitted = /* @__PURE__ */ new Set();
2936
+ for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
2937
+ const relPath = relImport(outDir, filePath);
2938
+ const specifiers = [];
2939
+ for (const root of [...roots].sort()) {
2940
+ if (emitted.has(root)) {
2941
+ const alias = `${root}_${emitted.size}`;
2942
+ specifiers.push(`${root} as ${alias}`);
2943
+ emitted.add(alias);
2944
+ refAlias.set(`${filePath}\0${root}`, alias);
2945
+ } else {
2946
+ specifiers.push(root);
2947
+ emitted.add(root);
2948
+ refAlias.set(`${filePath}\0${root}`, root);
2949
+ }
2950
+ }
2951
+ importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
2952
+ }
2953
+ }
2954
+ const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
2955
+ const irNamed = /* @__PURE__ */ new Map();
2956
+ const irTypeAliases = /* @__PURE__ */ new Map();
2957
+ const irAnnotations = /* @__PURE__ */ new Map();
2958
+ const decls = [];
2959
+ const mapEntries = [];
2960
+ let used = false;
2961
+ const renderSource = (src, rename) => {
2962
+ if (src.schema) {
2963
+ const r = adapter.renderModule(src.schema);
2964
+ for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
2965
+ if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
2966
+ if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
2967
+ return { text: r.schemaText };
2968
+ }
2969
+ if (src.zodText) {
2970
+ if (!acceptsRawZod) {
2971
+ return {
2972
+ text: "",
2973
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
2974
+ };
2975
+ }
2976
+ return { text: applyRenames(src.zodText, rename) };
2977
+ }
2978
+ if (src.zodRef) {
2979
+ if (!acceptsRawZod) {
2980
+ return {
2981
+ text: "",
2982
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
2983
+ };
2984
+ }
2985
+ const root = refRootIdentifier(src.zodRef.name);
2986
+ const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
2987
+ const member = src.zodRef.name.slice(root.length);
2988
+ return { text: `${alias}${member}` };
2989
+ }
2990
+ return null;
2991
+ };
2992
+ for (const entry of entries) {
2993
+ const block = [];
2994
+ const rename = renamesByEntry.get(entry) ?? null;
2995
+ let bodyConst;
2996
+ if (entry.warnings && entry.warnings.length > 0) {
2997
+ for (const w of entry.warnings) block.push(`// warning: ${w}`);
2998
+ }
2999
+ if (entry.body) {
3000
+ const rendered = renderSource(entry.body, rename);
3001
+ if (rendered?.warn) {
3002
+ block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
3003
+ } else if (rendered) {
3004
+ used = true;
3005
+ bodyConst = `${entry.baseName}BodySchema`;
3006
+ block.push(`export const ${bodyConst} = ${rendered.text};`);
3007
+ block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
3008
+ }
3009
+ }
3010
+ if (entry.query) {
3011
+ const rendered = renderSource(entry.query, rename);
3012
+ if (rendered?.warn) {
3013
+ block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
3014
+ } else if (rendered) {
3015
+ used = true;
3016
+ const queryConst = `${entry.baseName}QuerySchema`;
3017
+ block.push(`export const ${queryConst} = ${rendered.text};`);
3018
+ block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
3019
+ }
3020
+ }
3021
+ if (block.length === 0) continue;
3022
+ decls.push(`// ${entry.routeName}`, ...block, "");
3023
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
3462
3024
  }
3463
- return "z.unknown()";
3025
+ if (!used) return null;
3026
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
3027
+ if (acceptsRawZod) {
3028
+ const zodImport = config?.zodImport ?? "zod";
3029
+ lines.push(`import { z } from '${zodImport}';`);
3030
+ } else {
3031
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
3032
+ }
3033
+ lines.push(...importLines);
3034
+ lines.push("");
3035
+ const allNested = /* @__PURE__ */ new Map();
3036
+ for (const [n, t] of globalSchemas) allNested.set(n, t);
3037
+ for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
3038
+ if (allNested.size > 0) {
3039
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
3040
+ for (const [n, alias] of irTypeAliases) {
3041
+ if (allNested.has(n)) lines.push(`${alias};`);
3042
+ }
3043
+ for (const [n, t] of allNested) {
3044
+ const annotation = irAnnotations.get(n);
3045
+ lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
3046
+ }
3047
+ lines.push("");
3048
+ }
3049
+ lines.push(...decls);
3050
+ lines.push("/** Route name \u2192 body schema map. */");
3051
+ lines.push("export const formSchemas = {");
3052
+ lines.push(...mapEntries);
3053
+ lines.push("} as const;");
3054
+ lines.push("");
3055
+ return lines.join("\n");
3464
3056
  }
3465
3057
 
3466
3058
  // src/emit/emit-index.ts
3467
3059
  var import_promises6 = require("fs/promises");
3468
- var import_node_path8 = require("path");
3060
+ var import_node_path9 = require("path");
3469
3061
  async function emitIndex(outDir, hasContracts = false, hasForms = false) {
3470
3062
  await (0, import_promises6.mkdir)(outDir, { recursive: true });
3471
3063
  const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
@@ -3478,12 +3070,12 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
3478
3070
  const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
3479
3071
  "\n"
3480
3072
  );
3481
- await (0, import_promises6.writeFile)((0, import_node_path8.join)(outDir, "index.d.ts"), content, "utf8");
3073
+ await (0, import_promises6.writeFile)((0, import_node_path9.join)(outDir, "index.d.ts"), content, "utf8");
3482
3074
  }
3483
3075
 
3484
3076
  // src/emit/emit-pages.ts
3485
3077
  var import_promises7 = require("fs/promises");
3486
- var import_node_path9 = require("path");
3078
+ var import_node_path10 = require("path");
3487
3079
  async function emitPages(pages, outDir, _options = {}) {
3488
3080
  await (0, import_promises7.mkdir)(outDir, { recursive: true });
3489
3081
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -3504,7 +3096,7 @@ ${augBody}
3504
3096
  }
3505
3097
  ${sharedPropsBlock}}
3506
3098
  `;
3507
- await (0, import_promises7.writeFile)((0, import_node_path9.join)(outDir, "pages.d.ts"), content, "utf8");
3099
+ await (0, import_promises7.writeFile)((0, import_node_path10.join)(outDir, "pages.d.ts"), content, "utf8");
3508
3100
  }
3509
3101
  function buildSharedPropsBlock(sharedProps) {
3510
3102
  if (!sharedProps) return "";
@@ -3523,7 +3115,7 @@ ${propsBody}
3523
3115
  `;
3524
3116
  }
3525
3117
  function buildAugmentationType(page, outDir) {
3526
- let importPath = (0, import_node_path9.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
3118
+ let importPath = (0, import_node_path10.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
3527
3119
  if (!importPath.startsWith(".")) {
3528
3120
  importPath = `./${importPath}`;
3529
3121
  }
@@ -3535,11 +3127,11 @@ function needsQuotes(name) {
3535
3127
 
3536
3128
  // src/emit/emit-routes.ts
3537
3129
  var import_promises8 = require("fs/promises");
3538
- var import_node_path10 = require("path");
3130
+ var import_node_path11 = require("path");
3539
3131
  async function emitRoutes(routes, outDir) {
3540
3132
  await (0, import_promises8.mkdir)(outDir, { recursive: true });
3541
3133
  const content = buildRoutesFile(routes);
3542
- await (0, import_promises8.writeFile)((0, import_node_path10.join)(outDir, "routes.ts"), content, "utf8");
3134
+ await (0, import_promises8.writeFile)((0, import_node_path11.join)(outDir, "routes.ts"), content, "utf8");
3543
3135
  }
3544
3136
  function buildRoutesFile(routes) {
3545
3137
  if (routes.length === 0) {
@@ -3667,30 +3259,7 @@ async function generate(config, inputRoutes = []) {
3667
3259
  propsExport: pagesConfig.propsExport,
3668
3260
  componentNameStrategy: pagesConfig.componentNameStrategy
3669
3261
  });
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
- }
3262
+ const sharedProps = discoverSharedPropsFromConfig(config);
3694
3263
  await emitPages(pages, config.codegen.outDir, {
3695
3264
  propsExport: pagesConfig.propsExport,
3696
3265
  sharedProps
@@ -3714,8 +3283,8 @@ async function generate(config, inputRoutes = []) {
3714
3283
  if (extensions.length > 0) {
3715
3284
  const extraFiles = await collectEmittedFiles(extensions, ctx);
3716
3285
  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 });
3286
+ const dest = (0, import_node_path12.join)(config.codegen.outDir, file.path);
3287
+ await (0, import_promises9.mkdir)((0, import_node_path12.dirname)(dest), { recursive: true });
3719
3288
  await (0, import_promises9.writeFile)(dest, file.contents, "utf8");
3720
3289
  }
3721
3290
  }
@@ -3724,7 +3293,7 @@ async function generate(config, inputRoutes = []) {
3724
3293
  // src/watch/lock-file.ts
3725
3294
  var import_promises10 = require("fs/promises");
3726
3295
  var import_promises11 = require("fs/promises");
3727
- var import_node_path12 = require("path");
3296
+ var import_node_path13 = require("path");
3728
3297
  var LOCK_FILE = ".watcher.lock";
3729
3298
  function isProcessAlive(pid) {
3730
3299
  try {
@@ -3736,7 +3305,7 @@ function isProcessAlive(pid) {
3736
3305
  }
3737
3306
  async function acquireLock(outDir) {
3738
3307
  await (0, import_promises11.mkdir)(outDir, { recursive: true });
3739
- const lockPath = (0, import_node_path12.join)(outDir, LOCK_FILE);
3308
+ const lockPath = (0, import_node_path13.join)(outDir, LOCK_FILE);
3740
3309
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3741
3310
  try {
3742
3311
  const fd = await (0, import_promises10.open)(lockPath, "wx");
@@ -3776,7 +3345,7 @@ async function watch(config, onChange) {
3776
3345
  if (lock === null) {
3777
3346
  let holderPid = "unknown";
3778
3347
  try {
3779
- const raw = await (0, import_promises12.readFile)((0, import_node_path13.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3348
+ const raw = await (0, import_promises12.readFile)((0, import_node_path14.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3780
3349
  const data = JSON.parse(raw);
3781
3350
  if (data.pid !== void 0) holderPid = String(data.pid);
3782
3351
  } catch {
@@ -3804,7 +3373,7 @@ async function watch(config, onChange) {
3804
3373
  }
3805
3374
  let pagesDebounceTimer;
3806
3375
  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), {
3376
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, pagesGlob), {
3808
3377
  ignoreInitial: true,
3809
3378
  persistent: true,
3810
3379
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3830,7 +3399,7 @@ async function watch(config, onChange) {
3830
3399
  pagesWatcher.on("change", schedulePagesRegenerate);
3831
3400
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3832
3401
  let contractsDebounceTimer;
3833
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.contracts.glob), {
3402
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.contracts.glob), {
3834
3403
  ignoreInitial: true,
3835
3404
  persistent: true,
3836
3405
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3860,7 +3429,7 @@ async function watch(config, onChange) {
3860
3429
  contractsWatcher.on("add", scheduleContractsRegenerate);
3861
3430
  contractsWatcher.on("change", scheduleContractsRegenerate);
3862
3431
  contractsWatcher.on("unlink", scheduleContractsRegenerate);
3863
- const formsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.forms.watch), {
3432
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.forms.watch), {
3864
3433
  ignoreInitial: true,
3865
3434
  persistent: true,
3866
3435
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }