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