@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.
package/dist/index.js CHANGED
@@ -22,112 +22,9 @@ var CodegenError = class extends Error {
22
22
  }
23
23
  };
24
24
 
25
- // src/adapters/zod.ts
26
- function toObjectKey(name) {
27
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
28
- }
29
- function messageSuffix(messageRaw2) {
30
- return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
31
- }
32
- function renderStringChecks(checks) {
33
- let out = "";
34
- for (const c of checks) {
35
- switch (c.check) {
36
- case "email":
37
- out += `.email(${messageSuffix(c.messageRaw)})`;
38
- break;
39
- case "url":
40
- out += `.url(${messageSuffix(c.messageRaw)})`;
41
- break;
42
- case "uuid":
43
- out += `.uuid(${messageSuffix(c.messageRaw)})`;
44
- break;
45
- case "regex":
46
- out += `.regex(${c.pattern})`;
47
- break;
48
- case "min":
49
- out += `.min(${c.value})`;
50
- break;
51
- case "max":
52
- out += `.max(${c.value})`;
53
- break;
54
- }
55
- }
56
- return out;
57
- }
58
- function render(node, ctx) {
59
- switch (node.kind) {
60
- case "string":
61
- return `z.string()${renderStringChecks(node.checks)}`;
62
- case "number": {
63
- let out = "z.number()";
64
- for (const c of node.checks) {
65
- if (c.check === "int") out += ".int()";
66
- else if (c.check === "positive") out += ".positive()";
67
- else if (c.check === "negative") out += ".negative()";
68
- else if (c.check === "min") out += `.min(${c.value})`;
69
- else if (c.check === "max") out += `.max(${c.value})`;
70
- }
71
- return out;
72
- }
73
- case "boolean":
74
- return "z.boolean()";
75
- case "date":
76
- return "z.coerce.date()";
77
- case "unknown":
78
- return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
79
- case "instanceof":
80
- return `z.instanceof(${node.ctor})`;
81
- case "enum":
82
- return `z.enum([${node.literals.join(", ")}])`;
83
- case "literal":
84
- return `z.literal(${node.raw})`;
85
- case "union":
86
- return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
87
- case "object": {
88
- if (node.fields.length === 0) {
89
- return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
90
- }
91
- const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
92
- return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
93
- }
94
- case "array":
95
- return `z.array(${render(node.element, ctx)})`;
96
- case "optional":
97
- return `${render(node.inner, ctx)}.optional()`;
98
- case "ref":
99
- return node.name;
100
- case "lazyRef":
101
- return `z.lazy(() => ${node.name})`;
102
- case "annotated": {
103
- const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
104
- return `${render(node.inner, ctx)} ${comments}`;
105
- }
106
- }
107
- }
108
- var zodAdapter = {
109
- name: "zod",
110
- importStatements(usage) {
111
- return usage.used ? ["import { z } from 'zod';"] : [];
112
- },
113
- render,
114
- inferType(schemaConst) {
115
- return `z.infer<typeof ${schemaConst}>`;
116
- },
117
- renderModule(mod) {
118
- const ctx = { named: mod.named };
119
- const namedNestedSchemas = /* @__PURE__ */ new Map();
120
- for (const [name, node] of mod.named) {
121
- namedNestedSchemas.set(name, render(node, ctx));
122
- }
123
- return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
124
- }
125
- };
126
-
127
25
  // src/adapters/registry.ts
128
26
  function resolveAdapter(option) {
129
27
  if (typeof option !== "string") return option;
130
- if (option === "zod") return zodAdapter;
131
28
  const pkg = `@dudousxd/nestjs-codegen-${option}`;
132
29
  const named = `${option}Adapter`;
133
30
  throw new ConfigError(
@@ -180,8 +77,21 @@ If this is intentional, move the file inside your project directory.`
180
77
  function resolveConfig(userConfig, cwd) {
181
78
  return applyDefaults(userConfig, cwd ?? process.cwd());
182
79
  }
80
+ function validateUserConfig(userConfig) {
81
+ if (userConfig.validation == null) {
82
+ throw new ConfigError(
83
+ "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
84
+ );
85
+ }
86
+ if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
87
+ throw new ConfigError(
88
+ "Config validation failed: `pages.glob` must be a string when `pages` is set"
89
+ );
90
+ }
91
+ }
183
92
  function applyDefaults(userConfig, cwd) {
184
- const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-inertia");
93
+ validateUserConfig(userConfig);
94
+ const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
185
95
  const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
186
96
  let app = null;
187
97
  if (userConfig.app) {
@@ -199,7 +109,8 @@ function applyDefaults(userConfig, cwd) {
199
109
  }
200
110
  return {
201
111
  extensions: userConfig.extensions ?? [],
202
- validation: resolveAdapter(userConfig.validation ?? "zod"),
112
+ // Non-null: validateUserConfig() above throws when `validation` is absent.
113
+ validation: resolveAdapter(userConfig.validation),
203
114
  pages: userConfig.pages ? {
204
115
  glob: userConfig.pages.glob,
205
116
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
@@ -253,18 +164,12 @@ Run \`nestjs-codegen init\` to create a starter config.`
253
164
  `Config file must have a default export. Did you forget \`export default defineConfig({...})\`? (${configPath})`
254
165
  );
255
166
  }
256
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
257
- throw new ConfigError(
258
- `Config validation failed: \`pages.glob\` must be a string when \`pages\` is set (${configPath})`
259
- );
260
- }
261
167
  return applyDefaults(userConfig, resolvedCwd);
262
168
  }
263
169
 
264
170
  // src/generate.ts
265
171
  import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
266
- import { dirname as dirname2, join as join9 } from "path";
267
- import { Project as Project2 } from "ts-morph";
172
+ import { dirname as dirname2, join as join10 } from "path";
268
173
 
269
174
  // src/discovery/pages.ts
270
175
  import { readFile } from "fs/promises";
@@ -319,7 +224,8 @@ function extractPropsSource(source, exportName) {
319
224
  }
320
225
 
321
226
  // src/discovery/shared-props.ts
322
- import { Node, SyntaxKind } from "ts-morph";
227
+ import { join as join3 } from "path";
228
+ import { Node, Project, SyntaxKind } from "ts-morph";
323
229
  function discoverSharedProps(project, moduleEntry) {
324
230
  try {
325
231
  let sourceFile = project.getSourceFile(moduleEntry);
@@ -339,6 +245,31 @@ function discoverSharedProps(project, moduleEntry) {
339
245
  return null;
340
246
  }
341
247
  }
248
+ function discoverSharedPropsFromConfig(config) {
249
+ if (!config.app?.moduleEntry) return null;
250
+ try {
251
+ const tsconfigPath = config.app.tsconfig ?? join3(config.codegen.cwd, "tsconfig.json");
252
+ let project;
253
+ try {
254
+ project = new Project({
255
+ tsConfigFilePath: tsconfigPath,
256
+ skipAddingFilesFromTsConfig: true,
257
+ skipLoadingLibFiles: true,
258
+ skipFileDependencyResolution: true
259
+ });
260
+ } catch {
261
+ project = new Project({
262
+ skipAddingFilesFromTsConfig: true,
263
+ skipLoadingLibFiles: true,
264
+ skipFileDependencyResolution: true,
265
+ compilerOptions: { allowJs: true, strict: false }
266
+ });
267
+ }
268
+ return discoverSharedProps(project, config.app.moduleEntry);
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
342
273
  function findForRootCall(sourceFile) {
343
274
  const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
344
275
  for (const call of callExpressions) {
@@ -358,9 +289,9 @@ function findForRootCall(sourceFile) {
358
289
  function findShareInitializer(forRootCall) {
359
290
  if (!Node.isCallExpression(forRootCall)) return null;
360
291
  const args = forRootCall.getArguments();
361
- const firstArg3 = args[0];
362
- if (!firstArg3 || !Node.isObjectLiteralExpression(firstArg3)) return null;
363
- for (const prop of firstArg3.getProperties()) {
292
+ const firstArg2 = args[0];
293
+ if (!firstArg2 || !Node.isObjectLiteralExpression(firstArg2)) return null;
294
+ for (const prop of firstArg2.getProperties()) {
364
295
  if (Node.isPropertyAssignment(prop) && prop.getName() === "share") {
365
296
  return prop.getInitializer() ?? null;
366
297
  }
@@ -459,9 +390,9 @@ function extractFromReturnTypeAnnotation(typeNode) {
459
390
  const typeName = typeNode.getTypeName();
460
391
  if (Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
461
392
  const typeArgs = typeNode.getTypeArguments();
462
- const firstArg3 = typeArgs[0];
463
- if (firstArg3) {
464
- return extractFromReturnTypeAnnotation(firstArg3);
393
+ const firstArg2 = typeArgs[0];
394
+ if (firstArg2) {
395
+ return extractFromReturnTypeAnnotation(firstArg2);
465
396
  }
466
397
  return null;
467
398
  }
@@ -567,25 +498,14 @@ function inferExpressionType(node) {
567
498
 
568
499
  // src/emit/emit-api.ts
569
500
  import { mkdir, writeFile } from "fs/promises";
570
- import { isAbsolute as isAbsolute2, join as join3, relative as relative3 } from "path";
501
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
571
502
 
572
503
  // src/extension/registry.ts
573
- import { Project } from "ts-morph";
504
+ import { Project as Project2 } from "ts-morph";
574
505
  function resolveApiSlots(extensions) {
575
- let transport;
576
- let transportOwner;
577
506
  let layer;
578
507
  let layerOwner;
579
508
  for (const ext of extensions) {
580
- if (ext.apiTransport) {
581
- if (transport) {
582
- throw new CodegenError(
583
- `api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
584
- );
585
- }
586
- transport = ext.apiTransport;
587
- transportOwner = ext.name;
588
- }
589
509
  if (ext.apiClientLayer) {
590
510
  if (layer) {
591
511
  throw new CodegenError(
@@ -597,11 +517,22 @@ function resolveApiSlots(extensions) {
597
517
  }
598
518
  }
599
519
  return {
600
- ...transport ? { transport } : {},
601
520
  ...layer ? { layer } : {}
602
521
  };
603
522
  }
604
523
  var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
524
+ function mergeExclusive(target, incoming, {
525
+ owner,
526
+ describe
527
+ }) {
528
+ for (const [key, value] of incoming) {
529
+ const prev = target.get(key);
530
+ if (prev !== void 0) {
531
+ throw new CodegenError(describe(key, prev.owner, owner));
532
+ }
533
+ target.set(key, { value, owner });
534
+ }
535
+ }
605
536
  function createExtensionContext(config, getRoutes) {
606
537
  let project;
607
538
  return {
@@ -613,7 +544,7 @@ function createExtensionContext(config, getRoutes) {
613
544
  },
614
545
  project() {
615
546
  if (!project) {
616
- project = new Project({
547
+ project = new Project2({
617
548
  skipAddingFilesFromTsConfig: true,
618
549
  skipLoadingLibFiles: true,
619
550
  skipFileDependencyResolution: true,
@@ -646,29 +577,36 @@ async function collectEmittedFiles(extensions, ctx) {
646
577
  `Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
647
578
  );
648
579
  }
649
- const prev = owners.get(key);
650
- if (prev !== void 0) {
651
- throw new CodegenError(
652
- `Output file "${file.path}" is emitted by both "${prev}" and "${ext.name}". Two extensions cannot write the same file.`
653
- );
654
- }
655
- owners.set(key, ext.name);
580
+ mergeExclusive(owners, [[key, file]], {
581
+ owner: ext.name,
582
+ describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
583
+ });
656
584
  files.push(file);
657
585
  }
658
586
  }
659
587
  return files;
660
588
  }
661
589
 
590
+ // src/extension/types.ts
591
+ function requestShape(route) {
592
+ const cs = route.contract?.contractSource;
593
+ const isGet = route.method.toUpperCase() === "GET";
594
+ const isQuery = isGet || !!cs?.filterFields?.length;
595
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
596
+ const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
597
+ return { isGet, isQuery, hasBody, hasQuery };
598
+ }
599
+
662
600
  // src/emit/emit-api.ts
663
601
  async function emitApi(routes, outDir, opts = {}) {
664
602
  await mkdir(outDir, { recursive: true });
665
603
  const content = buildApiFile(routes, outDir, opts);
666
- await writeFile(join3(outDir, "api.ts"), content, "utf8");
604
+ await writeFile(join4(outDir, "api.ts"), content, "utf8");
667
605
  }
668
606
  function splitName(name) {
669
607
  return name.split(".");
670
608
  }
671
- function toObjectKey2(segment) {
609
+ function toObjectKey(segment) {
672
610
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
673
611
  return segment;
674
612
  }
@@ -775,7 +713,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
775
713
  const pad = " ".repeat(indent);
776
714
  const lines = [];
777
715
  for (const [key, node] of tree) {
778
- const objKey = toObjectKey2(key);
716
+ const objKey = toObjectKey(key);
779
717
  if (node.kind === "leaf") {
780
718
  const c = node;
781
719
  const method = c.method.toUpperCase();
@@ -801,15 +739,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
801
739
  return lines;
802
740
  }
803
741
  function buildRequestModel(c) {
804
- const isGet = c.method.toUpperCase() === "GET";
805
742
  const m = c.method.toLowerCase();
806
743
  const flat = JSON.stringify(c.name);
807
744
  const path = JSON.stringify(c.path);
808
745
  const TA = buildRouterTypeAccess(c.name);
809
746
  const withParams = hasPathParams(c.params);
810
- const hasBody = !!c.contractSource.bodyRef || c.contractSource.body != null && c.contractSource.body !== "never";
811
- const isQuery = isGet || !!c.contractSource.filterFields?.length;
812
- const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
747
+ const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
813
748
  const fields = [];
814
749
  if (withParams) fields.push(`params: ${TA}['params']`);
815
750
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -831,7 +766,6 @@ function buildRequestModel(c) {
831
766
  urlExpr,
832
767
  optsExpr,
833
768
  responseType: `${TA}['response']`,
834
- bodyType: `${TA}['body']`,
835
769
  queryKeyExpr: `[${flat}, input] as const`
836
770
  };
837
771
  }
@@ -879,7 +813,7 @@ function emitApiObjectBlock(tree, indent, p) {
879
813
  const pad = " ".repeat(indent);
880
814
  const lines = [];
881
815
  for (const [key, node] of tree) {
882
- const objKey = toObjectKey2(key);
816
+ const objKey = toObjectKey(key);
883
817
  if (node.kind === "branch") {
884
818
  lines.push(`${pad}${objKey}: {`);
885
819
  lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
@@ -887,30 +821,28 @@ function emitApiObjectBlock(tree, indent, p) {
887
821
  continue;
888
822
  }
889
823
  const req = buildRequestModel(node);
890
- const route = {
891
- method: node.method,
892
- path: node.path,
893
- name: node.name,
894
- params: node.params,
895
- contract: { contractSource: node.contractSource },
896
- ...node.controllerRef ? { controllerRef: node.controllerRef } : {}
824
+ const leaf = {
825
+ route: node.route,
826
+ request: req,
827
+ requestExpr: renderFetcherRequest(req)
897
828
  };
898
- const leaf = { route, request: req, requestExpr: "" };
899
- leaf.requestExpr = p.transport ? p.transport.renderRequest(leaf, p.ctx) : renderFetcherRequest(req);
900
- const members = {};
901
- if (p.layer) Object.assign(members, p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx));
829
+ const owned = /* @__PURE__ */ new Map();
830
+ if (p.layer) {
831
+ mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
832
+ owner: p.layer.name,
833
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
834
+ });
835
+ }
902
836
  for (const ext of p.memberExts) {
903
837
  const extra = ext.apiMembers?.(leaf, p.ctx);
904
838
  if (!extra) continue;
905
- for (const [name, value] of Object.entries(extra)) {
906
- if (name in members) {
907
- throw new Error(
908
- `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict at "${ext.name}").`
909
- );
910
- }
911
- members[name] = value;
912
- }
839
+ mergeExclusive(owned, Object.entries(extra), {
840
+ owner: ext.name,
841
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
842
+ });
913
843
  }
844
+ const members = {};
845
+ for (const [name, { value }] of owned) members[name] = value;
914
846
  lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
915
847
  }
916
848
  return lines;
@@ -919,10 +851,82 @@ function buildRouterTypeAccess(name) {
919
851
  const segments = splitName(name);
920
852
  return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
921
853
  }
854
+ var RESOLVER_HELPERS = [
855
+ // --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
856
+ "type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
857
+ " ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
858
+ " : P extends keyof R ? R[P] : never;",
859
+ "",
860
+ // --- ResolveByName: resolve a field from a dot-path name ---
861
+ "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
862
+ "",
863
+ // --- ResolveByPath: scan all leaves for matching method + url ---
864
+ // Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
865
+ "type _LeafValues<T> = T extends { method: string; url: string }",
866
+ " ? T",
867
+ " : T extends object ? _LeafValues<T[keyof T]> : never;",
868
+ "",
869
+ "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
870
+ " ? L extends { method: M; url: U }",
871
+ " ? Field extends keyof L ? L[Field] : never",
872
+ " : never",
873
+ " : never;",
874
+ ""
875
+ ];
876
+ var ROUTE_NAMESPACE = [
877
+ "export namespace Route {",
878
+ ' export type Response<K extends string> = ResolveByName<K, "response">;',
879
+ ' export type Body<K extends string> = ResolveByName<K, "body">;',
880
+ ' export type Query<K extends string> = ResolveByName<K, "query">;',
881
+ ' export type Params<K extends string> = ResolveByName<K, "params">;',
882
+ ' export type Error<K extends string> = ResolveByName<K, "error">;',
883
+ ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
884
+ " export type Request<K extends string> = {",
885
+ " body: Body<K>;",
886
+ " query: Query<K>;",
887
+ " params: Params<K>;",
888
+ " };",
889
+ "}",
890
+ ""
891
+ ];
892
+ var PATH_NAMESPACE = [
893
+ "export namespace Path {",
894
+ ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
895
+ ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
896
+ ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
897
+ ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
898
+ ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
899
+ ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
900
+ "}",
901
+ ""
902
+ ];
903
+ var EMPTY_ROUTE_NAMESPACE = [
904
+ "export namespace Route {",
905
+ " export type Response<K extends string> = never;",
906
+ " export type Body<K extends string> = never;",
907
+ " export type Query<K extends string> = never;",
908
+ " export type Params<K extends string> = never;",
909
+ " export type Error<K extends string> = never;",
910
+ " export type FilterFields<K extends string> = never;",
911
+ " export type Request<K extends string> = { body: never; query: never; params: never };",
912
+ "}",
913
+ ""
914
+ ];
915
+ var EMPTY_PATH_NAMESPACE = [
916
+ "export namespace Path {",
917
+ " export type Response<M extends string, U extends string> = never;",
918
+ " export type Body<M extends string, U extends string> = never;",
919
+ " export type Query<M extends string, U extends string> = never;",
920
+ " export type Params<M extends string, U extends string> = never;",
921
+ " export type Error<M extends string, U extends string> = never;",
922
+ " export type FilterFields<M extends string, U extends string> = never;",
923
+ "}",
924
+ ""
925
+ ];
922
926
  function buildApiFile(routes, outDir, opts = {}) {
923
927
  const fetcherImportPath = opts.fetcherImportPath;
924
928
  const extensions = opts.extensions ?? [];
925
- const { transport, layer } = resolveApiSlots(extensions);
929
+ const { layer } = resolveApiSlots(extensions);
926
930
  const memberExts = extensions.filter((e) => e.apiMembers);
927
931
  const headerExts = extensions.filter((e) => e.apiHeader);
928
932
  const contracted = routes.filter((r) => r.contract);
@@ -967,7 +971,6 @@ function buildApiFile(routes, outDir, opts = {}) {
967
971
  seenImports.add(imp);
968
972
  extImports.push(imp);
969
973
  };
970
- for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
971
974
  for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
972
975
  for (const ext of headerExts) {
973
976
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
@@ -1012,27 +1015,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1012
1015
  lines.push("}");
1013
1016
  lines.push("export type Api = ReturnType<typeof createApi>;");
1014
1017
  lines.push("");
1015
- lines.push("export namespace Route {");
1016
- lines.push(" export type Response<K extends string> = never;");
1017
- lines.push(" export type Body<K extends string> = never;");
1018
- lines.push(" export type Query<K extends string> = never;");
1019
- lines.push(" export type Params<K extends string> = never;");
1020
- lines.push(" export type Error<K extends string> = never;");
1021
- lines.push(" export type FilterFields<K extends string> = never;");
1022
- lines.push(
1023
- " export type Request<K extends string> = { body: never; query: never; params: never };"
1024
- );
1025
- lines.push("}");
1026
- lines.push("");
1027
- lines.push("export namespace Path {");
1028
- lines.push(" export type Response<M extends string, U extends string> = never;");
1029
- lines.push(" export type Body<M extends string, U extends string> = never;");
1030
- lines.push(" export type Query<M extends string, U extends string> = never;");
1031
- lines.push(" export type Params<M extends string, U extends string> = never;");
1032
- lines.push(" export type Error<M extends string, U extends string> = never;");
1033
- lines.push(" export type FilterFields<M extends string, U extends string> = never;");
1034
- lines.push("}");
1035
- lines.push("");
1018
+ lines.push(...EMPTY_ROUTE_NAMESPACE);
1019
+ lines.push(...EMPTY_PATH_NAMESPACE);
1036
1020
  return lines.join("\n");
1037
1021
  }
1038
1022
  const tree = /* @__PURE__ */ new Map();
@@ -1050,7 +1034,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1050
1034
  path: r.path,
1051
1035
  params: r.params,
1052
1036
  controllerRef: r.controllerRef,
1053
- contractSource: c.contractSource
1037
+ contractSource: c.contractSource,
1038
+ route: r
1054
1039
  };
1055
1040
  insertIntoTree(tree, segments, leaf, name);
1056
1041
  }
@@ -1063,7 +1048,6 @@ function buildApiFile(routes, outDir, opts = {}) {
1063
1048
  lines.push(" return {");
1064
1049
  lines.push(
1065
1050
  ...emitApiObjectBlock(tree, 4, {
1066
- ...transport ? { transport } : {},
1067
1051
  ...layer ? { layer } : {},
1068
1052
  memberExts,
1069
1053
  ctx
@@ -1074,61 +1058,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1074
1058
  lines.push("");
1075
1059
  lines.push("export type Api = ReturnType<typeof createApi>;");
1076
1060
  lines.push("");
1077
- lines.push("type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`");
1078
- lines.push(" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never");
1079
- lines.push(" : P extends keyof R ? R[P] : never;");
1080
- lines.push("");
1081
- lines.push(
1082
- "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
1083
- );
1084
- lines.push("");
1085
- lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
1086
- lines.push(" ? T");
1087
- lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
1088
- lines.push("");
1089
- lines.push(
1090
- "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
1091
- );
1092
- lines.push(" ? L extends { method: M; url: U }");
1093
- lines.push(" ? Field extends keyof L ? L[Field] : never");
1094
- lines.push(" : never");
1095
- lines.push(" : never;");
1096
- lines.push("");
1097
- lines.push("export namespace Route {");
1098
- lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
1099
- lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
1100
- lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
1101
- lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
1102
- lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
1103
- lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
1104
- lines.push(" export type Request<K extends string> = {");
1105
- lines.push(" body: Body<K>;");
1106
- lines.push(" query: Query<K>;");
1107
- lines.push(" params: Params<K>;");
1108
- lines.push(" };");
1109
- lines.push("}");
1110
- lines.push("");
1111
- lines.push("export namespace Path {");
1112
- lines.push(
1113
- ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
1114
- );
1115
- lines.push(
1116
- ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
1117
- );
1118
- lines.push(
1119
- ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
1120
- );
1121
- lines.push(
1122
- ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
1123
- );
1124
- lines.push(
1125
- ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
1126
- );
1127
- lines.push(
1128
- ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
1129
- );
1130
- lines.push("}");
1131
- lines.push("");
1061
+ lines.push(...RESOLVER_HELPERS);
1062
+ lines.push(...ROUTE_NAMESPACE);
1063
+ lines.push(...PATH_NAMESPACE);
1132
1064
  for (const ext of headerExts) {
1133
1065
  const statements = ext.apiHeader?.(ctx)?.statements;
1134
1066
  if (statements?.length) {
@@ -1140,7 +1072,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1140
1072
 
1141
1073
  // src/emit/emit-cache.ts
1142
1074
  import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
1143
- import { join as join4 } from "path";
1075
+ import { join as join5 } from "path";
1144
1076
  async function emitCache(pages, outDir) {
1145
1077
  await mkdir2(outDir, { recursive: true });
1146
1078
  const entries = await Promise.all(
@@ -1154,95 +1086,21 @@ async function emitCache(pages, outDir) {
1154
1086
  })
1155
1087
  );
1156
1088
  const cache = { pages: entries };
1157
- await writeFile2(join4(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1089
+ await writeFile2(join5(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1158
1090
  `, "utf8");
1159
1091
  }
1160
1092
 
1161
1093
  // src/emit/emit-forms.ts
1162
1094
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1163
- import { join as join5, relative as relative4 } from "path";
1095
+ import { join as join6, relative as relative4 } from "path";
1164
1096
  async function emitForms(routes, outDir, config, adapter) {
1165
1097
  if (config && config.enabled === false) return false;
1166
- if (adapter && adapter.name !== "zod") {
1167
- const content2 = buildFormsFileWithAdapter(routes, adapter);
1168
- if (content2 === null) return false;
1169
- await mkdir3(outDir, { recursive: true });
1170
- await writeFile3(join5(outDir, "forms.ts"), content2, "utf8");
1171
- return true;
1172
- }
1173
- const entries = collectFormEntries(routes);
1174
- if (entries.length === 0) return false;
1098
+ const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
1099
+ if (content === null) return false;
1175
1100
  await mkdir3(outDir, { recursive: true });
1176
- const content = buildFormsFile(entries, outDir, config);
1177
- await writeFile3(join5(outDir, "forms.ts"), content, "utf8");
1101
+ await writeFile3(join6(outDir, "forms.ts"), content, "utf8");
1178
1102
  return true;
1179
1103
  }
1180
- function buildFormsFileWithAdapter(routes, adapter) {
1181
- const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1182
- const methodNameCounts = /* @__PURE__ */ new Map();
1183
- for (const route of sorted) {
1184
- const cs = route.contract.contractSource;
1185
- if (!cs.bodySchema && !cs.querySchema) continue;
1186
- methodNameCounts.set(
1187
- deriveBaseName(route.name).method,
1188
- (methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
1189
- );
1190
- }
1191
- const named = /* @__PURE__ */ new Map();
1192
- const decls = [];
1193
- const mapEntries = [];
1194
- let used = false;
1195
- for (const route of sorted) {
1196
- const cs = route.contract.contractSource;
1197
- const { method, full } = deriveBaseName(route.name);
1198
- const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
1199
- const block = [];
1200
- if (cs.bodyZodText && !cs.bodySchema) {
1201
- block.push(
1202
- `// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1203
- );
1204
- }
1205
- let bodyConst;
1206
- if (cs.bodySchema) {
1207
- used = true;
1208
- const r = adapter.renderModule(cs.bodySchema);
1209
- for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
1210
- bodyConst = `${base}BodySchema`;
1211
- block.push(`export const ${bodyConst} = ${r.schemaText};`);
1212
- block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
1213
- }
1214
- if (cs.querySchema) {
1215
- used = true;
1216
- const r = adapter.renderModule(cs.querySchema);
1217
- for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
1218
- const queryConst = `${base}QuerySchema`;
1219
- block.push(`export const ${queryConst} = ${r.schemaText};`);
1220
- block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
1221
- }
1222
- if (block.length === 0) continue;
1223
- decls.push(`// ${route.name}`, ...block, "");
1224
- if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
1225
- }
1226
- if (!used) return null;
1227
- const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1228
- for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1229
- lines.push("");
1230
- if (named.size > 0) {
1231
- lines.push("// Hoisted nested schemas (shared across endpoints).");
1232
- for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
1233
- lines.push("");
1234
- }
1235
- lines.push(...decls);
1236
- lines.push("/** Route name \u2192 body schema map. */");
1237
- lines.push("export const formSchemas = {");
1238
- lines.push(...mapEntries);
1239
- lines.push("} as const;");
1240
- lines.push("");
1241
- return lines.join("\n");
1242
- }
1243
- function hasSchema(src) {
1244
- return !!src && (src.ref !== null || src.text !== null);
1245
- }
1246
1104
  function pascal(segment) {
1247
1105
  return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1248
1106
  }
@@ -1252,37 +1110,6 @@ function deriveBaseName(routeName) {
1252
1110
  const full = segments.map(pascal).join("");
1253
1111
  return { method, full };
1254
1112
  }
1255
- function collectFormEntries(routes) {
1256
- const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1257
- const methodNameCounts = /* @__PURE__ */ new Map();
1258
- const candidates = [];
1259
- for (const route of sorted) {
1260
- const cs = route.contract.contractSource;
1261
- const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
1262
- const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
1263
- if (!hasSchema(body) && !hasSchema(query)) continue;
1264
- const { method, full } = deriveBaseName(route.name);
1265
- methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1266
- candidates.push({ route, method, full });
1267
- }
1268
- const entries = [];
1269
- for (const { route, method, full } of candidates) {
1270
- const cs = route.contract.contractSource;
1271
- const collision = (methodNameCounts.get(method) ?? 0) > 1;
1272
- const baseName = collision ? full : method;
1273
- const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
1274
- const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
1275
- entries.push({
1276
- routeName: route.name,
1277
- baseName,
1278
- body: hasSchema(body) ? body : void 0,
1279
- query: hasSchema(query) ? query : void 0,
1280
- nestedSchemas: cs.formNestedSchemas ?? null,
1281
- warnings: cs.formWarnings ?? []
1282
- });
1283
- }
1284
- return entries;
1285
- }
1286
1113
  function relImport(outDir, filePath) {
1287
1114
  let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
1288
1115
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
@@ -1291,85 +1118,8 @@ function relImport(outDir, filePath) {
1291
1118
  function refRootIdentifier(refName) {
1292
1119
  return refName.split(".")[0] ?? refName;
1293
1120
  }
1294
- function buildFormsFile(entries, outDir, config) {
1295
- const zodImport = config?.zodImport ?? "zod";
1296
- const lines = [
1297
- "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1298
- `import { z } from '${zodImport}';`
1299
- ];
1300
- const importsByFile = /* @__PURE__ */ new Map();
1301
- const refAlias = /* @__PURE__ */ new Map();
1302
- for (const entry of entries) {
1303
- for (const src of [entry.body, entry.query]) {
1304
- if (src?.ref && !src.text) {
1305
- const root = refRootIdentifier(src.ref.name);
1306
- const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
1307
- set.add(root);
1308
- importsByFile.set(src.ref.filePath, set);
1309
- }
1310
- }
1311
- }
1312
- if (importsByFile.size > 0) {
1313
- const emitted = /* @__PURE__ */ new Set();
1314
- for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1315
- const relPath = relImport(outDir, filePath);
1316
- const specifiers = [];
1317
- for (const root of [...roots].sort()) {
1318
- if (emitted.has(root)) {
1319
- const alias = `${root}_${emitted.size}`;
1320
- specifiers.push(`${root} as ${alias}`);
1321
- emitted.add(alias);
1322
- refAlias.set(`${filePath}\0${root}`, alias);
1323
- } else {
1324
- specifiers.push(root);
1325
- emitted.add(root);
1326
- refAlias.set(`${filePath}\0${root}`, root);
1327
- }
1328
- }
1329
- lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1330
- }
1331
- }
1332
- lines.push("");
1333
- const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1334
- if (globalSchemas.size > 0) {
1335
- lines.push("// Hoisted nested schemas (shared across endpoints).");
1336
- for (const [name, text] of globalSchemas) {
1337
- lines.push(`const ${name} = ${text};`);
1338
- }
1339
- lines.push("");
1340
- }
1341
- const mapEntries = [];
1342
- for (const entry of entries) {
1343
- lines.push(`// ${entry.routeName}`);
1344
- if (entry.warnings && entry.warnings.length > 0) {
1345
- for (const w of entry.warnings) {
1346
- lines.push(`// warning: ${w}`);
1347
- }
1348
- }
1349
- const rename = renamesByEntry.get(entry) ?? null;
1350
- if (entry.body) {
1351
- const schemaName = `${entry.baseName}BodySchema`;
1352
- const typeName = `${entry.baseName}Body`;
1353
- const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
1354
- lines.push(`export const ${schemaName} = ${text};`);
1355
- lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
1356
- mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
1357
- }
1358
- if (entry.query) {
1359
- const schemaName = `${entry.baseName}QuerySchema`;
1360
- const typeName = `${entry.baseName}Query`;
1361
- const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
1362
- lines.push(`export const ${schemaName} = ${text};`);
1363
- lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
1364
- }
1365
- lines.push("");
1366
- }
1367
- lines.push("/** Route name \u2192 body schema map. */");
1368
- lines.push("export const formSchemas = {");
1369
- lines.push(...mapEntries);
1370
- lines.push("} as const;");
1371
- lines.push("");
1372
- return lines.join("\n");
1121
+ function hasSource(src) {
1122
+ return !!(src.schema || src.zodText || src.zodRef);
1373
1123
  }
1374
1124
  function applyRenames(text, renames) {
1375
1125
  if (!renames || renames.size === 0) return text;
@@ -1435,46 +1185,208 @@ function planNestedSchemas(entries) {
1435
1185
  }
1436
1186
  return { globalSchemas, renamesByEntry };
1437
1187
  }
1438
- function renderSchema(src, outDir, refAlias) {
1439
- if (src.text) return src.text;
1440
- if (src.ref) {
1441
- const root = refRootIdentifier(src.ref.name);
1442
- const alias = refAlias.get(`${src.ref.filePath}\0${root}`) ?? root;
1443
- const member = src.ref.name.slice(root.length);
1444
- return `${alias}${member}`;
1445
- }
1446
- return "z.unknown()";
1447
- }
1448
-
1449
- // src/emit/emit-index.ts
1450
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1451
- import { join as join6 } from "path";
1452
- async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1453
- await mkdir4(outDir, { recursive: true });
1454
- const exports = ["export * from './pages.js';", "export * from './routes.js';"];
1455
- if (hasContracts) {
1456
- exports.push("export * from './api.js';");
1188
+ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1189
+ const acceptsRawZod = adapter.acceptsRawZodSource === true;
1190
+ const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1191
+ const methodNameCounts = /* @__PURE__ */ new Map();
1192
+ const candidates = [];
1193
+ for (const route of sorted) {
1194
+ const cs = route.contract.contractSource;
1195
+ const body = {
1196
+ schema: cs.bodySchema ?? null,
1197
+ zodText: cs.bodyZodText ?? null,
1198
+ zodRef: cs.bodyZodRef ?? null
1199
+ };
1200
+ const query = {
1201
+ schema: cs.querySchema ?? null,
1202
+ zodText: cs.queryZodText ?? null,
1203
+ zodRef: cs.queryZodRef ?? null
1204
+ };
1205
+ if (!hasSource(body) && !hasSource(query)) continue;
1206
+ const { method, full } = deriveBaseName(route.name);
1207
+ methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1208
+ candidates.push({
1209
+ routeName: route.name,
1210
+ baseName: full,
1211
+ // resolved below
1212
+ body: hasSource(body) ? body : void 0,
1213
+ query: hasSource(query) ? query : void 0,
1214
+ nestedSchemas: cs.formNestedSchemas ?? null,
1215
+ warnings: cs.formWarnings ?? []
1216
+ });
1457
1217
  }
1458
- if (hasForms) {
1459
- exports.push("export * from './forms.js';");
1218
+ const entries = candidates.map((c) => {
1219
+ const { method, full } = deriveBaseName(c.routeName);
1220
+ const collision = (methodNameCounts.get(method) ?? 0) > 1;
1221
+ return { ...c, baseName: collision ? full : method };
1222
+ });
1223
+ if (entries.length === 0) return null;
1224
+ const importsByFile = /* @__PURE__ */ new Map();
1225
+ const refAlias = /* @__PURE__ */ new Map();
1226
+ for (const entry of entries) {
1227
+ for (const src of [entry.body, entry.query]) {
1228
+ if (src?.zodRef && !src.zodText && !src.schema) {
1229
+ const root = refRootIdentifier(src.zodRef.name);
1230
+ const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
1231
+ set.add(root);
1232
+ importsByFile.set(src.zodRef.filePath, set);
1233
+ }
1234
+ }
1460
1235
  }
1461
- const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
1462
- "\n"
1463
- );
1464
- await writeFile4(join6(outDir, "index.d.ts"), content, "utf8");
1465
- }
1466
-
1467
- // src/emit/emit-pages.ts
1468
- import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
1469
- import { join as join7, relative as relative5 } from "path";
1470
- async function emitPages(pages, outDir, _options = {}) {
1471
- await mkdir5(outDir, { recursive: true });
1472
- const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1473
- const augBody = pages.map((p) => {
1474
- const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
1475
- const valueType = buildAugmentationType(p, outDir);
1476
- return ` ${key}: ${valueType};`;
1477
- }).join("\n");
1236
+ const importLines = [];
1237
+ if (importsByFile.size > 0) {
1238
+ const emitted = /* @__PURE__ */ new Set();
1239
+ for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1240
+ const relPath = relImport(outDir, filePath);
1241
+ const specifiers = [];
1242
+ for (const root of [...roots].sort()) {
1243
+ if (emitted.has(root)) {
1244
+ const alias = `${root}_${emitted.size}`;
1245
+ specifiers.push(`${root} as ${alias}`);
1246
+ emitted.add(alias);
1247
+ refAlias.set(`${filePath}\0${root}`, alias);
1248
+ } else {
1249
+ specifiers.push(root);
1250
+ emitted.add(root);
1251
+ refAlias.set(`${filePath}\0${root}`, root);
1252
+ }
1253
+ }
1254
+ importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1255
+ }
1256
+ }
1257
+ const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1258
+ const irNamed = /* @__PURE__ */ new Map();
1259
+ const irTypeAliases = /* @__PURE__ */ new Map();
1260
+ const irAnnotations = /* @__PURE__ */ new Map();
1261
+ const decls = [];
1262
+ const mapEntries = [];
1263
+ let used = false;
1264
+ const renderSource = (src, rename) => {
1265
+ if (src.schema) {
1266
+ const r = adapter.renderModule(src.schema);
1267
+ for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1268
+ if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
1269
+ if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
1270
+ return { text: r.schemaText };
1271
+ }
1272
+ if (src.zodText) {
1273
+ if (!acceptsRawZod) {
1274
+ return {
1275
+ text: "",
1276
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1277
+ };
1278
+ }
1279
+ return { text: applyRenames(src.zodText, rename) };
1280
+ }
1281
+ if (src.zodRef) {
1282
+ if (!acceptsRawZod) {
1283
+ return {
1284
+ text: "",
1285
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1286
+ };
1287
+ }
1288
+ const root = refRootIdentifier(src.zodRef.name);
1289
+ const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
1290
+ const member = src.zodRef.name.slice(root.length);
1291
+ return { text: `${alias}${member}` };
1292
+ }
1293
+ return null;
1294
+ };
1295
+ for (const entry of entries) {
1296
+ const block = [];
1297
+ const rename = renamesByEntry.get(entry) ?? null;
1298
+ let bodyConst;
1299
+ if (entry.warnings && entry.warnings.length > 0) {
1300
+ for (const w of entry.warnings) block.push(`// warning: ${w}`);
1301
+ }
1302
+ if (entry.body) {
1303
+ const rendered = renderSource(entry.body, rename);
1304
+ if (rendered?.warn) {
1305
+ block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
1306
+ } else if (rendered) {
1307
+ used = true;
1308
+ bodyConst = `${entry.baseName}BodySchema`;
1309
+ block.push(`export const ${bodyConst} = ${rendered.text};`);
1310
+ block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
1311
+ }
1312
+ }
1313
+ if (entry.query) {
1314
+ const rendered = renderSource(entry.query, rename);
1315
+ if (rendered?.warn) {
1316
+ block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
1317
+ } else if (rendered) {
1318
+ used = true;
1319
+ const queryConst = `${entry.baseName}QuerySchema`;
1320
+ block.push(`export const ${queryConst} = ${rendered.text};`);
1321
+ block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
1322
+ }
1323
+ }
1324
+ if (block.length === 0) continue;
1325
+ decls.push(`// ${entry.routeName}`, ...block, "");
1326
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
1327
+ }
1328
+ if (!used) return null;
1329
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1330
+ if (acceptsRawZod) {
1331
+ const zodImport = config?.zodImport ?? "zod";
1332
+ lines.push(`import { z } from '${zodImport}';`);
1333
+ } else {
1334
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1335
+ }
1336
+ lines.push(...importLines);
1337
+ lines.push("");
1338
+ const allNested = /* @__PURE__ */ new Map();
1339
+ for (const [n, t] of globalSchemas) allNested.set(n, t);
1340
+ for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1341
+ if (allNested.size > 0) {
1342
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
1343
+ for (const [n, alias] of irTypeAliases) {
1344
+ if (allNested.has(n)) lines.push(`${alias};`);
1345
+ }
1346
+ for (const [n, t] of allNested) {
1347
+ const annotation = irAnnotations.get(n);
1348
+ lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
1349
+ }
1350
+ lines.push("");
1351
+ }
1352
+ lines.push(...decls);
1353
+ lines.push("/** Route name \u2192 body schema map. */");
1354
+ lines.push("export const formSchemas = {");
1355
+ lines.push(...mapEntries);
1356
+ lines.push("} as const;");
1357
+ lines.push("");
1358
+ return lines.join("\n");
1359
+ }
1360
+
1361
+ // src/emit/emit-index.ts
1362
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1363
+ import { join as join7 } from "path";
1364
+ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1365
+ await mkdir4(outDir, { recursive: true });
1366
+ const exports = ["export * from './pages.js';", "export * from './routes.js';"];
1367
+ if (hasContracts) {
1368
+ exports.push("export * from './api.js';");
1369
+ }
1370
+ if (hasForms) {
1371
+ exports.push("export * from './forms.js';");
1372
+ }
1373
+ const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
1374
+ "\n"
1375
+ );
1376
+ await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
1377
+ }
1378
+
1379
+ // src/emit/emit-pages.ts
1380
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
1381
+ import { join as join8, relative as relative5 } from "path";
1382
+ async function emitPages(pages, outDir, _options = {}) {
1383
+ await mkdir5(outDir, { recursive: true });
1384
+ const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1385
+ const augBody = pages.map((p) => {
1386
+ const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
1387
+ const valueType = buildAugmentationType(p, outDir);
1388
+ return ` ${key}: ${valueType};`;
1389
+ }).join("\n");
1478
1390
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
1479
1391
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
1480
1392
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
@@ -1487,7 +1399,7 @@ ${augBody}
1487
1399
  }
1488
1400
  ${sharedPropsBlock}}
1489
1401
  `;
1490
- await writeFile5(join7(outDir, "pages.d.ts"), content, "utf8");
1402
+ await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
1491
1403
  }
1492
1404
  function buildSharedPropsBlock(sharedProps) {
1493
1405
  if (!sharedProps) return "";
@@ -1518,11 +1430,11 @@ function needsQuotes(name) {
1518
1430
 
1519
1431
  // src/emit/emit-routes.ts
1520
1432
  import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1521
- import { join as join8 } from "path";
1433
+ import { join as join9 } from "path";
1522
1434
  async function emitRoutes(routes, outDir) {
1523
1435
  await mkdir6(outDir, { recursive: true });
1524
1436
  const content = buildRoutesFile(routes);
1525
- await writeFile6(join8(outDir, "routes.ts"), content, "utf8");
1437
+ await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
1526
1438
  }
1527
1439
  function buildRoutesFile(routes) {
1528
1440
  if (routes.length === 0) {
@@ -1650,30 +1562,7 @@ async function generate(config, inputRoutes = []) {
1650
1562
  propsExport: pagesConfig.propsExport,
1651
1563
  componentNameStrategy: pagesConfig.componentNameStrategy
1652
1564
  });
1653
- let sharedProps = null;
1654
- if (config.app?.moduleEntry) {
1655
- try {
1656
- const tsconfigPath = config.app.tsconfig ?? join9(config.codegen.cwd, "tsconfig.json");
1657
- let project;
1658
- try {
1659
- project = new Project2({
1660
- tsConfigFilePath: tsconfigPath,
1661
- skipAddingFilesFromTsConfig: true,
1662
- skipLoadingLibFiles: true,
1663
- skipFileDependencyResolution: true
1664
- });
1665
- } catch {
1666
- project = new Project2({
1667
- skipAddingFilesFromTsConfig: true,
1668
- skipLoadingLibFiles: true,
1669
- skipFileDependencyResolution: true,
1670
- compilerOptions: { allowJs: true, strict: false }
1671
- });
1672
- }
1673
- sharedProps = discoverSharedProps(project, config.app.moduleEntry);
1674
- } catch {
1675
- }
1676
- }
1565
+ const sharedProps = discoverSharedPropsFromConfig(config);
1677
1566
  await emitPages(pages, config.codegen.outDir, {
1678
1567
  propsExport: pagesConfig.propsExport,
1679
1568
  sharedProps
@@ -1697,7 +1586,7 @@ async function generate(config, inputRoutes = []) {
1697
1586
  if (extensions.length > 0) {
1698
1587
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1699
1588
  for (const file of extraFiles) {
1700
- const dest = join9(config.codegen.outDir, file.path);
1589
+ const dest = join10(config.codegen.outDir, file.path);
1701
1590
  await mkdir7(dirname2(dest), { recursive: true });
1702
1591
  await writeFile7(dest, file.contents, "utf8");
1703
1592
  }
@@ -1706,15 +1595,20 @@ async function generate(config, inputRoutes = []) {
1706
1595
 
1707
1596
  // src/watch/watcher.ts
1708
1597
  import { readFile as readFile3 } from "fs/promises";
1709
- import { join as join12 } from "path";
1598
+ import { join as join13 } from "path";
1710
1599
  import chokidar from "chokidar";
1711
1600
 
1712
1601
  // src/discovery/contracts-fast.ts
1713
- import { join as join10, resolve as resolve3 } from "path";
1602
+ import { join as join11, resolve as resolve3 } from "path";
1714
1603
  import fg2 from "fast-glob";
1715
1604
  import {
1716
- Node as Node7,
1717
- Project as Project3,
1605
+ Node as Node8,
1606
+ Project as Project3
1607
+ } from "ts-morph";
1608
+
1609
+ // src/discovery/dto-type-resolver.ts
1610
+ import {
1611
+ Node as Node6,
1718
1612
  SyntaxKind as SyntaxKind3
1719
1613
  } from "ts-morph";
1720
1614
 
@@ -1729,20 +1623,13 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
1729
1623
  import {
1730
1624
  Node as Node2
1731
1625
  } from "ts-morph";
1732
- var _ctx = { projectRoot: "", tsconfigPaths: null };
1733
- function setDiscoveryContext(ctx) {
1734
- const prev = _ctx;
1735
- _ctx = ctx;
1736
- return prev;
1626
+ var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1627
+ var _ctxByProject = /* @__PURE__ */ new WeakMap();
1628
+ function setDiscoveryContext(project, ctx) {
1629
+ _ctxByProject.set(project, ctx);
1737
1630
  }
1738
- function restoreDiscoveryContext(ctx) {
1739
- _ctx = ctx;
1740
- }
1741
- function _projectRoot() {
1742
- return _ctx.projectRoot;
1743
- }
1744
- function _tsconfigPaths() {
1745
- return _ctx.tsconfigPaths;
1631
+ function _ctxFor(project) {
1632
+ return _ctxByProject.get(project) ?? _EMPTY_CTX;
1746
1633
  }
1747
1634
  var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
1748
1635
  function dbg(...args) {
@@ -1784,7 +1671,7 @@ function findTypeInFile(name, file) {
1784
1671
  }
1785
1672
  return null;
1786
1673
  }
1787
- function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1674
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1788
1675
  if (moduleSpecifier.startsWith(".")) {
1789
1676
  const dir = dirname3(sourceFile.getFilePath());
1790
1677
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
@@ -1794,8 +1681,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1794
1681
  resolve2(dir, moduleSpecifier, "index.ts")
1795
1682
  ];
1796
1683
  }
1797
- const baseUrl = _projectRoot();
1798
- const tsconfigPaths = _tsconfigPaths();
1684
+ const ctx = _ctxFor(project);
1685
+ const baseUrl = ctx.projectRoot;
1686
+ const tsconfigPaths = ctx.tsconfigPaths;
1799
1687
  dbg(
1800
1688
  "resolveModuleSpecifier",
1801
1689
  moduleSpecifier,
@@ -1941,7 +1829,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1941
1829
  if (!namedImport) continue;
1942
1830
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
1943
1831
  if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
1944
- const tsconfigPaths = _tsconfigPaths();
1832
+ const tsconfigPaths = _ctxFor(project).tsconfigPaths;
1945
1833
  const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
1946
1834
  const prefix = p.replace("*", "");
1947
1835
  return moduleSpecifier.startsWith(prefix);
@@ -2010,10 +1898,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
2010
1898
  depth: 0
2011
1899
  };
2012
1900
  const root = buildObject(classDecl, sourceFile, ctx);
2013
- for (const schemaName of ctx.recursiveSchemas) {
2014
- ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
2015
- }
2016
- return { root, named: ctx.named, warnings: ctx.warnings };
1901
+ return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
2017
1902
  }
2018
1903
  function buildObject(classDecl, classFile, ctx) {
2019
1904
  const props = classDecl.getProperties();
@@ -2033,7 +1918,7 @@ function buildProperty(prop, classFile, ctx) {
2033
1918
  const dec = (n) => decorators.get(n);
2034
1919
  const typeNode = prop.getTypeNode();
2035
1920
  const typeText = typeNode?.getText() ?? "unknown";
2036
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
1921
+ const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
2037
1922
  const typeRefName = resolveTypeFactoryName(dec("Type"));
2038
1923
  if (has("ValidateNested") || typeRefName) {
2039
1924
  const childName = typeRefName ?? singularClassName(typeText);
@@ -2164,18 +2049,27 @@ function baseFromType(typeText, isArrayType) {
2164
2049
  }
2165
2050
  }
2166
2051
  function buildNestedReference(className, fromFile, ctx) {
2167
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2052
+ if (ctx.visiting.has(className)) {
2168
2053
  const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2169
2054
  ctx.emittedClasses.set(className, reserved);
2170
2055
  ctx.recursiveSchemas.add(reserved);
2171
2056
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2172
2057
  ctx.warnedDecorators.add(`recursive:${reserved}`);
2173
- const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
2058
+ const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
2174
2059
  ctx.warnings.push(msg);
2175
2060
  console.warn(`[nestjs-codegen] ${msg}`);
2176
2061
  }
2177
2062
  return { kind: "lazyRef", name: reserved };
2178
2063
  }
2064
+ if (ctx.depth >= 8) {
2065
+ if (!ctx.warnedDecorators.has(`deep:${className}`)) {
2066
+ ctx.warnedDecorators.add(`deep:${className}`);
2067
+ const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
2068
+ ctx.warnings.push(msg);
2069
+ console.warn(`[nestjs-codegen] ${msg}`);
2070
+ }
2071
+ return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2072
+ }
2179
2073
  const existing = ctx.emittedClasses.get(className);
2180
2074
  if (existing) return { kind: "ref", name: existing };
2181
2075
  const schemaName = aliasFor(className, ctx);
@@ -2294,434 +2188,104 @@ function inSchemaFromDecorator(decorator) {
2294
2188
  return null;
2295
2189
  }
2296
2190
 
2297
- // src/discovery/dto-to-zod.ts
2191
+ // src/discovery/filter-for.ts
2298
2192
  import {
2299
- Node as Node4
2193
+ Node as Node5
2300
2194
  } from "ts-morph";
2301
- var KNOWN_DECORATORS2 = /* @__PURE__ */ new Set([
2302
- "IsString",
2303
- "IsNumber",
2304
- "IsInt",
2305
- "IsBoolean",
2306
- "IsDate",
2307
- "IsEmail",
2308
- "IsUrl",
2309
- "IsUUID",
2310
- "MinLength",
2311
- "MaxLength",
2312
- "Length",
2313
- "Min",
2314
- "Max",
2315
- "IsPositive",
2316
- "IsNegative",
2317
- "Matches",
2318
- "IsEnum",
2319
- "IsIn",
2320
- "IsOptional",
2321
- "IsNotEmpty",
2322
- "IsArray",
2323
- "ValidateNested",
2324
- "Type",
2325
- "IsObject",
2326
- "Allow",
2327
- "IsDefined"
2328
- ]);
2329
- function extractZodFromDto(classDecl, sourceFile, project) {
2330
- const ctx = {
2331
- sourceFile,
2332
- project,
2333
- namedNestedSchemas: /* @__PURE__ */ new Map(),
2334
- warnings: [],
2335
- warnedDecorators: /* @__PURE__ */ new Set(),
2336
- emittedClasses: /* @__PURE__ */ new Map(),
2337
- visiting: /* @__PURE__ */ new Set(),
2338
- recursiveSchemas: /* @__PURE__ */ new Set(),
2339
- depth: 0
2340
- };
2341
- const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
2342
- for (const schemaName of ctx.recursiveSchemas) {
2343
- ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
2344
- }
2345
- return {
2346
- schemaText,
2347
- namedNestedSchemas: ctx.namedNestedSchemas,
2348
- warnings: ctx.warnings
2349
- };
2195
+
2196
+ // src/discovery/filter-field-types.ts
2197
+ import {
2198
+ Node as Node4,
2199
+ SyntaxKind as SyntaxKind2
2200
+ } from "ts-morph";
2201
+
2202
+ // src/discovery/enum-resolution.ts
2203
+ function resolveEnumValues(name, sourceFile, project) {
2204
+ const resolved = findType(name, sourceFile, project);
2205
+ if (!resolved || resolved.kind !== "enum") return null;
2206
+ let numeric = true;
2207
+ const values = resolved.members.map((m) => {
2208
+ const parsed = JSON.parse(m);
2209
+ if (typeof parsed === "string") numeric = false;
2210
+ return String(parsed);
2211
+ });
2212
+ if (values.length === 0) return null;
2213
+ return { values, numeric };
2350
2214
  }
2351
- function buildObjectSchema(classDecl, classFile, ctx) {
2352
- const props = classDecl.getProperties();
2353
- if (props.length === 0) {
2354
- return "z.object({}).passthrough()";
2355
- }
2356
- const fields = [];
2357
- for (const prop of props) {
2358
- const name = prop.getName();
2359
- const expr = buildPropertySchema(prop, classFile, ctx);
2360
- fields.push(`${toObjectKey3(name)}: ${expr}`);
2361
- }
2362
- return `z.object({ ${fields.join(", ")} })`;
2215
+
2216
+ // src/discovery/filter-field-types.ts
2217
+ var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2218
+ var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2219
+ var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2220
+ var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2221
+ var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2222
+ function classifyTypeKeyword(raw) {
2223
+ const t = raw.toLowerCase();
2224
+ if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2225
+ if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2226
+ if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2227
+ if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2228
+ if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2229
+ return null;
2363
2230
  }
2364
- function toObjectKey3(name) {
2365
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2231
+ function markNullable(r, nullable) {
2232
+ return nullable ? { ...r, nullable: true } : r;
2366
2233
  }
2367
- function buildPropertySchema(prop, classFile, ctx) {
2368
- const decorators = /* @__PURE__ */ new Map();
2369
- for (const d of prop.getDecorators()) decorators.set(d.getName(), d);
2370
- const has = (n) => decorators.has(n);
2371
- const dec = (n) => decorators.get(n);
2372
- const typeNode = prop.getTypeNode();
2373
- const typeText = typeNode?.getText() ?? "unknown";
2374
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
2375
- const comments = [];
2376
- const typeRefName = resolveTypeFactoryName2(dec("Type"));
2377
- if (has("ValidateNested") || typeRefName) {
2378
- const childName = typeRefName ?? singularClassName2(typeText);
2379
- if (childName) {
2380
- const childExpr = buildNestedReference2(childName, classFile, ctx);
2381
- const wrapArray = has("IsArray") || isArrayType;
2382
- let expr2 = wrapArray ? `z.array(${childExpr})` : childExpr;
2383
- expr2 = applyPresence2(expr2, decorators);
2384
- return expr2;
2385
- }
2386
- }
2387
- let base = baseFromType2(typeText, isArrayType, ctx, classFile);
2388
- const refinements = [];
2389
- if (has("IsString")) base = "z.string()";
2390
- if (has("IsBoolean")) base = "z.boolean()";
2391
- if (has("IsDate")) base = "z.coerce.date()";
2392
- if (has("IsNumber")) base = "z.number()";
2393
- if (has("IsInt")) base = "z.number().int()";
2394
- if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
2395
- if (has("Allow")) base = "z.unknown()";
2396
- if (has("IsEmail")) {
2397
- base = ensureStringBase(base);
2398
- refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
2399
- }
2400
- if (has("IsUrl")) {
2401
- base = ensureStringBase(base);
2402
- refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
2403
- }
2404
- if (has("IsUUID")) {
2405
- base = ensureStringBase(base);
2406
- refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
2407
- }
2408
- if (has("Matches")) {
2409
- const re = firstArgText2(dec("Matches"));
2410
- if (re) {
2411
- base = ensureStringBase(base);
2412
- refinements.push(`.regex(${re})`);
2234
+ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2235
+ if (Node4.isUnionTypeNode(typeNode)) {
2236
+ let nullable = false;
2237
+ const stringLits = [];
2238
+ const numberLits = [];
2239
+ const others = [];
2240
+ for (const member of typeNode.getTypeNodes()) {
2241
+ const kind = member.getKind();
2242
+ if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
2243
+ nullable = true;
2244
+ continue;
2245
+ }
2246
+ if (Node4.isLiteralTypeNode(member)) {
2247
+ const lit = member.getLiteral();
2248
+ if (Node4.isStringLiteral(lit)) {
2249
+ stringLits.push(lit.getLiteralValue());
2250
+ continue;
2251
+ }
2252
+ if (Node4.isNumericLiteral(lit)) {
2253
+ numberLits.push(lit.getText());
2254
+ continue;
2255
+ }
2256
+ if (lit.getKind() === SyntaxKind2.NullKeyword) {
2257
+ nullable = true;
2258
+ continue;
2259
+ }
2260
+ }
2261
+ others.push(member);
2413
2262
  }
2263
+ if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2264
+ return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2265
+ }
2266
+ if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2267
+ return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2268
+ }
2269
+ if (others.length === 1) {
2270
+ const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2271
+ return markNullable(inner, nullable || inner.nullable === true);
2272
+ }
2273
+ return markNullable({ kind: "unknown" }, nullable);
2414
2274
  }
2415
- if (has("MinLength")) {
2416
- const n = numericArg2(dec("MinLength"));
2417
- if (n !== null) refinements.push(`.min(${n})`);
2418
- }
2419
- if (has("MaxLength")) {
2420
- const n = numericArg2(dec("MaxLength"));
2421
- if (n !== null) refinements.push(`.max(${n})`);
2422
- }
2423
- if (has("Length")) {
2424
- const [min, max] = numericArgs2(dec("Length"));
2425
- if (min !== null) refinements.push(`.min(${min})`);
2426
- if (max !== null) refinements.push(`.max(${max})`);
2275
+ switch (typeNode.getKind()) {
2276
+ case SyntaxKind2.StringKeyword:
2277
+ return { kind: "string" };
2278
+ case SyntaxKind2.NumberKeyword:
2279
+ return { kind: "number" };
2280
+ case SyntaxKind2.BooleanKeyword:
2281
+ return { kind: "boolean" };
2282
+ case SyntaxKind2.AnyKeyword:
2283
+ case SyntaxKind2.UnknownKeyword:
2284
+ return { kind: "unknown" };
2285
+ default:
2286
+ break;
2427
2287
  }
2428
- if (has("Min")) {
2429
- const n = numericArg2(dec("Min"));
2430
- if (n !== null) refinements.push(`.min(${n})`);
2431
- }
2432
- if (has("Max")) {
2433
- const n = numericArg2(dec("Max"));
2434
- if (n !== null) refinements.push(`.max(${n})`);
2435
- }
2436
- if (has("IsPositive")) refinements.push(".positive()");
2437
- if (has("IsNegative")) refinements.push(".negative()");
2438
- if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
2439
- if (has("IsEnum")) {
2440
- const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
2441
- if (enumExpr) base = enumExpr;
2442
- }
2443
- if (has("IsIn")) {
2444
- const inExpr = inSchemaFromDecorator2(dec("IsIn"));
2445
- if (inExpr) base = inExpr;
2446
- }
2447
- for (const name of decorators.keys()) {
2448
- if (!KNOWN_DECORATORS2.has(name)) {
2449
- comments.push(`/* @${name}: not translatable to zod (server-only) */`);
2450
- if (!ctx.warnedDecorators.has(name)) {
2451
- ctx.warnedDecorators.add(name);
2452
- const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
2453
- ctx.warnings.push(msg);
2454
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2455
- }
2456
- }
2457
- }
2458
- let expr = base + refinements.join("");
2459
- if (isArrayType && !expr.startsWith("z.array(")) {
2460
- expr = `z.array(${expr})`;
2461
- }
2462
- expr = applyPresence2(expr, decorators);
2463
- if (comments.length > 0) {
2464
- expr = `${expr} ${comments.join(" ")}`;
2465
- }
2466
- return expr;
2467
- }
2468
- function applyPresence2(expr, decorators) {
2469
- if (decorators.has("IsDefined")) return expr;
2470
- if (decorators.has("IsOptional")) return `${expr}.optional()`;
2471
- return expr;
2472
- }
2473
- function baseFromType2(typeText, isArrayType, ctx, classFile) {
2474
- const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
2475
- switch (inner) {
2476
- case "string":
2477
- return "z.string()";
2478
- case "number":
2479
- return "z.number()";
2480
- case "boolean":
2481
- return "z.boolean()";
2482
- case "Date":
2483
- return "z.coerce.date()";
2484
- case "File":
2485
- case "Express.Multer.File":
2486
- return "z.instanceof(File)";
2487
- default:
2488
- return "z.unknown()";
2489
- }
2490
- }
2491
- function ensureStringBase(base) {
2492
- return isStringBase(base) ? base : "z.string()";
2493
- }
2494
- function isStringBase(base) {
2495
- return base.startsWith("z.string(");
2496
- }
2497
- function buildNestedReference2(className, fromFile, ctx) {
2498
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2499
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
2500
- ctx.emittedClasses.set(className, reserved);
2501
- ctx.recursiveSchemas.add(reserved);
2502
- if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2503
- ctx.warnedDecorators.add(`recursive:${reserved}`);
2504
- const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
2505
- ctx.warnings.push(msg);
2506
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2507
- }
2508
- return `z.lazy(() => ${reserved})`;
2509
- }
2510
- const existing = ctx.emittedClasses.get(className);
2511
- if (existing) return existing;
2512
- const schemaName = aliasFor2(className, ctx);
2513
- const resolved = findType(className, fromFile, ctx.project);
2514
- if (!resolved || resolved.kind !== "class") {
2515
- return "z.object({}).passthrough()";
2516
- }
2517
- ctx.emittedClasses.set(className, schemaName);
2518
- ctx.visiting.add(className);
2519
- ctx.depth += 1;
2520
- const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
2521
- ctx.depth -= 1;
2522
- ctx.visiting.delete(className);
2523
- ctx.namedNestedSchemas.set(schemaName, childText);
2524
- return schemaName;
2525
- }
2526
- function aliasFor2(className, ctx) {
2527
- const baseName = `${className}Schema`;
2528
- let candidate = baseName;
2529
- let i = 1;
2530
- const used = new Set(ctx.namedNestedSchemas.keys());
2531
- for (const v of ctx.emittedClasses.values()) used.add(v);
2532
- while (used.has(candidate)) {
2533
- candidate = `${baseName}_${i}`;
2534
- i += 1;
2535
- }
2536
- return candidate;
2537
- }
2538
- function firstArg2(decorator) {
2539
- return decorator?.getArguments()[0];
2540
- }
2541
- function firstArgText2(decorator) {
2542
- const arg = firstArg2(decorator);
2543
- return arg ? arg.getText() : null;
2544
- }
2545
- function numericArg2(decorator) {
2546
- const arg = firstArg2(decorator);
2547
- if (arg && Node4.isNumericLiteral(arg)) return arg.getText();
2548
- return null;
2549
- }
2550
- function numericArgs2(decorator) {
2551
- const args = decorator?.getArguments() ?? [];
2552
- const num = (n) => n && Node4.isNumericLiteral(n) ? n.getText() : null;
2553
- return [num(args[0]), num(args[1])];
2554
- }
2555
- function messageArg(decorator) {
2556
- const args = decorator?.getArguments() ?? [];
2557
- for (const arg of args) {
2558
- if (Node4.isObjectLiteralExpression(arg)) {
2559
- for (const prop of arg.getProperties()) {
2560
- if (Node4.isPropertyAssignment(prop) && prop.getName() === "message") {
2561
- const init = prop.getInitializer();
2562
- if (init && Node4.isStringLiteral(init)) {
2563
- return `{ message: ${init.getText()} }`;
2564
- }
2565
- }
2566
- }
2567
- }
2568
- }
2569
- return "";
2570
- }
2571
- function resolveTypeFactoryName2(decorator) {
2572
- const arg = firstArg2(decorator);
2573
- if (!arg) return null;
2574
- if (Node4.isArrowFunction(arg)) {
2575
- const body = arg.getBody();
2576
- if (Node4.isIdentifier(body)) return body.getText();
2577
- }
2578
- return null;
2579
- }
2580
- function singularClassName2(typeText) {
2581
- const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2582
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2583
- }
2584
- function enumSchemaFromDecorator2(decorator, classFile, ctx) {
2585
- const arg = firstArg2(decorator);
2586
- if (!arg) return null;
2587
- if (Node4.isIdentifier(arg)) {
2588
- const name = arg.getText();
2589
- const resolved = findType(name, classFile, ctx.project);
2590
- if (resolved && resolved.kind === "enum") {
2591
- return `z.enum([${resolved.members.join(", ")}])`;
2592
- }
2593
- 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().`;
2594
- if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
2595
- ctx.warnedDecorators.add(`IsEnum:${name}`);
2596
- ctx.warnings.push(msg);
2597
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2598
- }
2599
- return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
2600
- }
2601
- if (Node4.isObjectLiteralExpression(arg)) {
2602
- const values = [];
2603
- for (const p of arg.getProperties()) {
2604
- if (!Node4.isPropertyAssignment(p)) continue;
2605
- const init = p.getInitializer();
2606
- if (init && Node4.isStringLiteral(init)) values.push(init.getText());
2607
- }
2608
- if (values.length > 0) return `z.enum([${values.join(", ")}])`;
2609
- }
2610
- return null;
2611
- }
2612
- function inSchemaFromDecorator2(decorator) {
2613
- const arg = firstArg2(decorator);
2614
- if (arg && Node4.isArrayLiteralExpression(arg)) {
2615
- const elements = arg.getElements();
2616
- const allStrings = elements.every((e) => Node4.isStringLiteral(e));
2617
- if (allStrings && elements.length > 0) {
2618
- return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
2619
- }
2620
- if (elements.length > 0) {
2621
- return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
2622
- }
2623
- }
2624
- return null;
2625
- }
2626
-
2627
- // src/discovery/filter-for.ts
2628
- import {
2629
- Node as Node6
2630
- } from "ts-morph";
2631
-
2632
- // src/discovery/filter-field-types.ts
2633
- import {
2634
- Node as Node5,
2635
- SyntaxKind as SyntaxKind2
2636
- } from "ts-morph";
2637
-
2638
- // src/discovery/enum-resolution.ts
2639
- function resolveEnumValues(name, sourceFile, project) {
2640
- const resolved = findType(name, sourceFile, project);
2641
- if (!resolved || resolved.kind !== "enum") return null;
2642
- let numeric = true;
2643
- const values = resolved.members.map((m) => {
2644
- const parsed = JSON.parse(m);
2645
- if (typeof parsed === "string") numeric = false;
2646
- return String(parsed);
2647
- });
2648
- if (values.length === 0) return null;
2649
- return { values, numeric };
2650
- }
2651
-
2652
- // src/discovery/filter-field-types.ts
2653
- var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2654
- var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2655
- var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2656
- var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2657
- var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2658
- function classifyTypeKeyword(raw) {
2659
- const t = raw.toLowerCase();
2660
- if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2661
- if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2662
- if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2663
- if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2664
- if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2665
- return null;
2666
- }
2667
- function markNullable(r, nullable) {
2668
- return nullable ? { ...r, nullable: true } : r;
2669
- }
2670
- function classifyTypeNode(typeNode, sourceFile, project, opts) {
2671
- if (Node5.isUnionTypeNode(typeNode)) {
2672
- let nullable = false;
2673
- const stringLits = [];
2674
- const numberLits = [];
2675
- const others = [];
2676
- for (const member of typeNode.getTypeNodes()) {
2677
- const kind = member.getKind();
2678
- if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
2679
- nullable = true;
2680
- continue;
2681
- }
2682
- if (Node5.isLiteralTypeNode(member)) {
2683
- const lit = member.getLiteral();
2684
- if (Node5.isStringLiteral(lit)) {
2685
- stringLits.push(lit.getLiteralValue());
2686
- continue;
2687
- }
2688
- if (Node5.isNumericLiteral(lit)) {
2689
- numberLits.push(lit.getText());
2690
- continue;
2691
- }
2692
- if (lit.getKind() === SyntaxKind2.NullKeyword) {
2693
- nullable = true;
2694
- continue;
2695
- }
2696
- }
2697
- others.push(member);
2698
- }
2699
- if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2700
- return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2701
- }
2702
- if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2703
- return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2704
- }
2705
- if (others.length === 1) {
2706
- const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2707
- return markNullable(inner, nullable || inner.nullable === true);
2708
- }
2709
- return markNullable({ kind: "unknown" }, nullable);
2710
- }
2711
- switch (typeNode.getKind()) {
2712
- case SyntaxKind2.StringKeyword:
2713
- return { kind: "string" };
2714
- case SyntaxKind2.NumberKeyword:
2715
- return { kind: "number" };
2716
- case SyntaxKind2.BooleanKeyword:
2717
- return { kind: "boolean" };
2718
- case SyntaxKind2.AnyKeyword:
2719
- case SyntaxKind2.UnknownKeyword:
2720
- return { kind: "unknown" };
2721
- default:
2722
- break;
2723
- }
2724
- if (Node5.isTypeReference(typeNode)) {
2288
+ if (Node4.isTypeReference(typeNode)) {
2725
2289
  const refName = typeNode.getTypeName().getText();
2726
2290
  if (refName === "Date") return { kind: "date" };
2727
2291
  if (refName === "Record" || refName === "Object") return { kind: "json" };
@@ -2734,25 +2298,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2734
2298
  if (typeRef) return { kind: "unknown", typeRef };
2735
2299
  return { kind: "unknown" };
2736
2300
  }
2737
- if (Node5.isTypeLiteral(typeNode)) return { kind: "json" };
2301
+ if (Node4.isTypeLiteral(typeNode)) return { kind: "json" };
2738
2302
  return { kind: "unknown" };
2739
2303
  }
2740
2304
  function enumFromDecoratorArgs(args, sourceFile, project) {
2741
2305
  for (const arg of args) {
2742
- if (Node5.isArrowFunction(arg)) {
2306
+ if (Node4.isArrowFunction(arg)) {
2743
2307
  const body = arg.getBody();
2744
- if (Node5.isIdentifier(body)) {
2308
+ if (Node4.isIdentifier(body)) {
2745
2309
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2746
2310
  if (en) return en;
2747
2311
  }
2748
2312
  }
2749
- if (Node5.isObjectLiteralExpression(arg)) {
2313
+ if (Node4.isObjectLiteralExpression(arg)) {
2750
2314
  const itemsProp = arg.getProperty("items");
2751
- if (itemsProp && Node5.isPropertyAssignment(itemsProp)) {
2315
+ if (itemsProp && Node4.isPropertyAssignment(itemsProp)) {
2752
2316
  const init = itemsProp.getInitializer();
2753
- if (init && Node5.isArrowFunction(init)) {
2317
+ if (init && Node4.isArrowFunction(init)) {
2754
2318
  const body = init.getBody();
2755
- if (Node5.isIdentifier(body)) {
2319
+ if (Node4.isIdentifier(body)) {
2756
2320
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2757
2321
  if (en) return en;
2758
2322
  }
@@ -2775,7 +2339,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2775
2339
  return { kind: "string" };
2776
2340
  }
2777
2341
  for (const arg of args) {
2778
- if (Node5.isStringLiteral(arg)) {
2342
+ if (Node4.isStringLiteral(arg)) {
2779
2343
  const raw = arg.getLiteralValue();
2780
2344
  const kind = classifyTypeKeyword(raw);
2781
2345
  if (kind) {
@@ -2783,11 +2347,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2783
2347
  return { kind };
2784
2348
  }
2785
2349
  }
2786
- if (Node5.isObjectLiteralExpression(arg)) {
2350
+ if (Node4.isObjectLiteralExpression(arg)) {
2787
2351
  const enumProp = arg.getProperty("enum");
2788
- if (enumProp && Node5.isPropertyAssignment(enumProp)) {
2352
+ if (enumProp && Node4.isPropertyAssignment(enumProp)) {
2789
2353
  const init = enumProp.getInitializer();
2790
- if (init && Node5.isIdentifier(init)) {
2354
+ if (init && Node4.isIdentifier(init)) {
2791
2355
  const en = resolveEnumValues(init.getText(), sourceFile, project);
2792
2356
  if (en) {
2793
2357
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -2796,9 +2360,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2796
2360
  }
2797
2361
  }
2798
2362
  const typeProp = arg.getProperty("type");
2799
- if (typeProp && Node5.isPropertyAssignment(typeProp)) {
2363
+ if (typeProp && Node4.isPropertyAssignment(typeProp)) {
2800
2364
  const init = typeProp.getInitializer();
2801
- if (init && Node5.isStringLiteral(init)) {
2365
+ if (init && Node4.isStringLiteral(init)) {
2802
2366
  const kind = classifyTypeKeyword(init.getLiteralValue());
2803
2367
  if (kind) return { kind };
2804
2368
  }
@@ -2834,7 +2398,7 @@ function toFilterFieldType(name, r) {
2834
2398
 
2835
2399
  // src/discovery/filter-for.ts
2836
2400
  function classifyFilterForHint(typeInit) {
2837
- if (Node6.isStringLiteral(typeInit)) {
2401
+ if (Node5.isStringLiteral(typeInit)) {
2838
2402
  switch (typeInit.getLiteralValue()) {
2839
2403
  case "string":
2840
2404
  return { kind: "string" };
@@ -2848,10 +2412,10 @@ function classifyFilterForHint(typeInit) {
2848
2412
  return null;
2849
2413
  }
2850
2414
  }
2851
- if (Node6.isArrayLiteralExpression(typeInit)) {
2415
+ if (Node5.isArrayLiteralExpression(typeInit)) {
2852
2416
  const values = [];
2853
2417
  for (const el of typeInit.getElements()) {
2854
- if (!Node6.isStringLiteral(el)) return null;
2418
+ if (!Node5.isStringLiteral(el)) return null;
2855
2419
  values.push(el.getLiteralValue());
2856
2420
  }
2857
2421
  if (values.length === 0) return null;
@@ -2886,11 +2450,11 @@ function extractFilterForHints(classDecl, project) {
2886
2450
  if (!filterForDec) continue;
2887
2451
  const args = filterForDec.getArguments();
2888
2452
  const keyArg = args[0];
2889
- const inputKey = keyArg && Node6.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2453
+ const inputKey = keyArg && Node5.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2890
2454
  const optsArg = args[1];
2891
- if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
2455
+ if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
2892
2456
  const typeProp = optsArg.getProperty("type");
2893
- if (typeProp && Node6.isPropertyAssignment(typeProp)) {
2457
+ if (typeProp && Node5.isPropertyAssignment(typeProp)) {
2894
2458
  const typeInit = typeProp.getInitializer();
2895
2459
  if (typeInit) {
2896
2460
  const classified = classifyFilterForHint(typeInit);
@@ -2913,14 +2477,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
2913
2477
  const args = filterDecorator.getArguments();
2914
2478
  if (args.length === 0) continue;
2915
2479
  const filterClassArg = args[0];
2916
- if (!filterClassArg || !Node6.isIdentifier(filterClassArg)) continue;
2480
+ if (!filterClassArg || !Node5.isIdentifier(filterClassArg)) continue;
2917
2481
  let source = "query";
2918
2482
  const optionsArg = args[1];
2919
- if (optionsArg && Node6.isObjectLiteralExpression(optionsArg)) {
2483
+ if (optionsArg && Node5.isObjectLiteralExpression(optionsArg)) {
2920
2484
  const sourceProp = optionsArg.getProperty("source");
2921
- if (sourceProp && Node6.isPropertyAssignment(sourceProp)) {
2485
+ if (sourceProp && Node5.isPropertyAssignment(sourceProp)) {
2922
2486
  const init = sourceProp.getInitializer();
2923
- if (init && Node6.isStringLiteral(init) && init.getLiteralValue() === "body") {
2487
+ if (init && Node5.isStringLiteral(init) && init.getLiteralValue() === "body") {
2924
2488
  source = "body";
2925
2489
  }
2926
2490
  }
@@ -2983,22 +2547,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
2983
2547
  const args = dec.getArguments();
2984
2548
  if (args.length === 0) continue;
2985
2549
  const arg = args[0];
2986
- if (Node6.isObjectLiteralExpression(arg)) {
2550
+ if (Node5.isObjectLiteralExpression(arg)) {
2987
2551
  const entityProp = arg.getProperty("entity");
2988
- if (entityProp && Node6.isPropertyAssignment(entityProp)) {
2552
+ if (entityProp && Node5.isPropertyAssignment(entityProp)) {
2989
2553
  const init = entityProp.getInitializer();
2990
- if (init && Node6.isArrowFunction(init)) {
2554
+ if (init && Node5.isArrowFunction(init)) {
2991
2555
  const body = init.getBody();
2992
- if (Node6.isIdentifier(body)) {
2556
+ if (Node5.isIdentifier(body)) {
2993
2557
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
2994
2558
  if (resolved?.kind === "class") return resolved.decl;
2995
2559
  }
2996
2560
  }
2997
2561
  }
2998
2562
  }
2999
- if (Node6.isArrowFunction(arg)) {
2563
+ if (Node5.isArrowFunction(arg)) {
3000
2564
  const body = arg.getBody();
3001
- if (Node6.isIdentifier(body)) {
2565
+ if (Node5.isIdentifier(body)) {
3002
2566
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3003
2567
  if (resolved?.kind === "class") return resolved.decl;
3004
2568
  }
@@ -3022,11 +2586,11 @@ function extractFilterableEntityFields(filterClass, project) {
3022
2586
  const args = filterableDecorator.getArguments();
3023
2587
  if (args.length === 0) return [];
3024
2588
  const optionsArg = args[0];
3025
- if (!Node6.isObjectLiteralExpression(optionsArg)) return [];
2589
+ if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
3026
2590
  const entityProp = optionsArg.getProperty("entity");
3027
- if (!entityProp || !Node6.isPropertyAssignment(entityProp)) return [];
2591
+ if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
3028
2592
  const entityInit = entityProp.getInitializer();
3029
- if (!entityInit || !Node6.isIdentifier(entityInit)) return [];
2593
+ if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
3030
2594
  const entityName = entityInit.getText();
3031
2595
  const filterSourceFile = filterClass.getSourceFile();
3032
2596
  const resolvedEntity = findType(entityName, filterSourceFile, project);
@@ -3042,17 +2606,17 @@ function extractFilterableEntityFields(filterClass, project) {
3042
2606
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
3043
2607
  if (relationsDecorator) {
3044
2608
  const relArgs = relationsDecorator.getArguments();
3045
- if (relArgs.length > 0 && Node6.isObjectLiteralExpression(relArgs[0])) {
2609
+ if (relArgs.length > 0 && Node5.isObjectLiteralExpression(relArgs[0])) {
3046
2610
  for (const relProp of relArgs[0].getProperties()) {
3047
- if (!Node6.isPropertyAssignment(relProp)) continue;
2611
+ if (!Node5.isPropertyAssignment(relProp)) continue;
3048
2612
  const relInit = relProp.getInitializer();
3049
- if (!relInit || !Node6.isObjectLiteralExpression(relInit)) continue;
2613
+ if (!relInit || !Node5.isObjectLiteralExpression(relInit)) continue;
3050
2614
  const keysProp = relInit.getProperty("keys");
3051
- if (!keysProp || !Node6.isPropertyAssignment(keysProp)) continue;
2615
+ if (!keysProp || !Node5.isPropertyAssignment(keysProp)) continue;
3052
2616
  const keysInit = keysProp.getInitializer();
3053
- if (!keysInit || !Node6.isArrayLiteralExpression(keysInit)) continue;
2617
+ if (!keysInit || !Node5.isArrayLiteralExpression(keysInit)) continue;
3054
2618
  for (const el of keysInit.getElements()) {
3055
- if (Node6.isStringLiteral(el)) {
2619
+ if (Node5.isStringLiteral(el)) {
3056
2620
  fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
3057
2621
  }
3058
2622
  }
@@ -3062,267 +2626,65 @@ function extractFilterableEntityFields(filterClass, project) {
3062
2626
  return fields;
3063
2627
  }
3064
2628
 
3065
- // src/discovery/contracts-fast.ts
3066
- async function discoverContractsFast(opts) {
3067
- const { cwd, glob, tsconfig } = opts;
3068
- const tsconfigPath = tsconfig ? resolve3(tsconfig) : join10(cwd, "tsconfig.json");
3069
- let project;
3070
- try {
3071
- project = new Project3({
3072
- tsConfigFilePath: tsconfigPath,
3073
- skipAddingFilesFromTsConfig: true,
3074
- skipLoadingLibFiles: true,
3075
- skipFileDependencyResolution: true
3076
- });
3077
- } catch {
3078
- project = new Project3({
3079
- skipAddingFilesFromTsConfig: true,
3080
- skipLoadingLibFiles: true,
3081
- skipFileDependencyResolution: true,
3082
- compilerOptions: {
3083
- allowJs: true,
3084
- resolveJsonModule: false,
3085
- strict: false
3086
- }
3087
- });
2629
+ // src/discovery/dto-type-resolver.ts
2630
+ var WRAPPER_TYPES = {
2631
+ // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
2632
+ // wrappers around related entities; the wire shape is just the referenced
2633
+ // entity. Unwrap to the type argument.
2634
+ Ref: "unwrap",
2635
+ Reference: "unwrap",
2636
+ LoadedReference: "unwrap",
2637
+ IdentifiedReference: "unwrap",
2638
+ // MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
2639
+ Opt: "unwrap",
2640
+ Loaded: "unwrap",
2641
+ // Promise<T> — unwrap
2642
+ Promise: "unwrap",
2643
+ // MikroORM Collection<T> serializes as an array of T on the wire.
2644
+ Collection: "arrayOf",
2645
+ // Array<T> generic form
2646
+ Array: "arrayOf"
2647
+ };
2648
+ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2649
+ "Record",
2650
+ "Omit",
2651
+ "Pick",
2652
+ "Partial",
2653
+ "Required",
2654
+ "Readonly",
2655
+ "Map",
2656
+ "Set"
2657
+ ]);
2658
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2659
+ if (depth <= 0) return "unknown";
2660
+ if (Node6.isArrayTypeNode(typeNode)) {
2661
+ const elementType = typeNode.getElementTypeNode();
2662
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3088
2663
  }
3089
- const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3090
- for (const f of files) {
3091
- project.addSourceFileAtPath(f);
2664
+ if (Node6.isUnionTypeNode(typeNode)) {
2665
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3092
2666
  }
3093
- const routes = [];
3094
- const prevCtx = setDiscoveryContext({
3095
- projectRoot: cwd,
3096
- tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3097
- });
3098
- try {
3099
- for (const sourceFile of project.getSourceFiles()) {
3100
- routes.push(...extractFromSourceFile(sourceFile, project));
3101
- }
3102
- } finally {
3103
- restoreDiscoveryContext(prevCtx);
3104
- }
3105
- return routes;
3106
- }
3107
- function zodAstToTs(node) {
3108
- if (!Node7.isCallExpression(node)) return "unknown";
3109
- const expr = node.getExpression();
3110
- if (Node7.isPropertyAccessExpression(expr)) {
3111
- const methodName = expr.getName();
3112
- const receiver = expr.getExpression();
3113
- if (methodName === "optional") {
3114
- return `${zodAstToTs(receiver)} | undefined`;
3115
- }
3116
- if (methodName === "nullable") {
3117
- return `${zodAstToTs(receiver)} | null`;
3118
- }
3119
- const args = node.getArguments();
3120
- switch (methodName) {
3121
- case "string":
3122
- return "string";
3123
- case "number":
3124
- return "number";
3125
- case "boolean":
3126
- return "boolean";
3127
- case "unknown":
3128
- return "unknown";
3129
- case "any":
3130
- return "unknown";
3131
- case "literal": {
3132
- const lit = args[0];
3133
- if (!lit) return "unknown";
3134
- if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
3135
- if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
3136
- if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
3137
- if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
3138
- return "unknown";
3139
- }
3140
- case "enum": {
3141
- const arrArg = args[0];
3142
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
3143
- const members = arrArg.getElements().map(
3144
- (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
3145
- );
3146
- return members.join(" | ");
3147
- }
3148
- case "array": {
3149
- const inner = args[0];
3150
- if (!inner) return "unknown";
3151
- return `Array<${zodAstToTs(inner)}>`;
3152
- }
3153
- case "object": {
3154
- const objArg = args[0];
3155
- if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
3156
- const lines = [];
3157
- for (const prop of objArg.getProperties()) {
3158
- if (!Node7.isPropertyAssignment(prop)) continue;
3159
- const key = prop.getName();
3160
- const valNode = prop.getInitializer();
3161
- if (!valNode) continue;
3162
- const tsType = zodAstToTs(valNode);
3163
- const isOpt = isOptionalChain(valNode);
3164
- lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
3165
- }
3166
- return `{ ${lines.join("; ")} }`;
3167
- }
3168
- case "union": {
3169
- const arrArg = args[0];
3170
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
3171
- return arrArg.getElements().map(zodAstToTs).join(" | ");
3172
- }
3173
- case "record": {
3174
- const valArg = args.length === 1 ? args[0] : args[1];
3175
- if (!valArg) return "unknown";
3176
- return `Record<string, ${zodAstToTs(valArg)}>`;
3177
- }
3178
- case "tuple": {
3179
- const arrArg = args[0];
3180
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
3181
- return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3182
- }
3183
- default:
3184
- return "unknown";
3185
- }
3186
- }
3187
- return "unknown";
3188
- }
3189
- function isOptionalChain(node) {
3190
- if (!Node7.isCallExpression(node)) return false;
3191
- const expr = node.getExpression();
3192
- return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3193
- }
3194
- function decoratorStringArg(decoratorExpr) {
3195
- if (!decoratorExpr) return void 0;
3196
- if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3197
- if (Node7.isArrayLiteralExpression(decoratorExpr)) {
3198
- const first = decoratorExpr.getElements()[0];
3199
- if (first && Node7.isStringLiteral(first)) return first.getLiteralValue();
3200
- }
3201
- return void 0;
3202
- }
3203
- function parseDefineContractCall(callExpr) {
3204
- if (!Node7.isCallExpression(callExpr)) return null;
3205
- const callee = callExpr.getExpression();
3206
- const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
3207
- if (calleeName !== "defineContract") return null;
3208
- const args = callExpr.getArguments();
3209
- const optsArg = args[0];
3210
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3211
- let query = null;
3212
- let body = null;
3213
- let response = "unknown";
3214
- let bodyZodText = null;
3215
- let queryZodText = null;
3216
- for (const prop of optsArg.getProperties()) {
3217
- if (!Node7.isPropertyAssignment(prop)) continue;
3218
- const propName = prop.getName();
3219
- const val = prop.getInitializer();
3220
- if (!val) continue;
3221
- if (propName === "query") {
3222
- query = zodAstToTs(val);
3223
- queryZodText = val.getText();
3224
- } else if (propName === "body") {
3225
- body = zodAstToTs(val);
3226
- bodyZodText = val.getText();
3227
- } else if (propName === "response") {
3228
- response = zodAstToTs(val);
3229
- }
3230
- }
3231
- return { query, body, response, bodyZodText, queryZodText };
3232
- }
3233
- function deriveClassSegment(className) {
3234
- const noSuffix = className.replace(/Controller$/, "");
3235
- if (!noSuffix) {
3236
- throw new Error(
3237
- `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3238
- );
3239
- }
3240
- return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3241
- }
3242
- function resolveRouteName(className, methodName, classAs, methodAs) {
3243
- const classPortion = classAs ?? deriveClassSegment(className);
3244
- const methodPortion = methodAs ?? methodName;
3245
- return `${classPortion}.${methodPortion}`;
3246
- }
3247
- function joinPaths(prefix, suffix) {
3248
- if (!prefix && !suffix) return "/";
3249
- if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3250
- if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3251
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3252
- const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3253
- const combined = p + s;
3254
- return combined === "" ? "/" : combined;
3255
- }
3256
- function extractParams(path) {
3257
- const matches = path.matchAll(/:(\w+)/g);
3258
- return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3259
- }
3260
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3261
- if (depth <= 0) return "unknown";
3262
- if (Node7.isArrayTypeNode(typeNode)) {
3263
- const elementType = typeNode.getElementTypeNode();
3264
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3265
- }
3266
- if (Node7.isUnionTypeNode(typeNode)) {
3267
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3268
- }
3269
- if (Node7.isIntersectionTypeNode(typeNode)) {
2667
+ if (Node6.isIntersectionTypeNode(typeNode)) {
3270
2668
  return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3271
2669
  }
3272
- if (Node7.isParenthesizedTypeNode(typeNode)) {
2670
+ if (Node6.isParenthesizedTypeNode(typeNode)) {
3273
2671
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3274
2672
  }
3275
- if (Node7.isTypeReference(typeNode)) {
2673
+ if (Node6.isTypeReference(typeNode)) {
3276
2674
  const typeName = typeNode.getTypeName();
3277
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2675
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3278
2676
  if (name === "string" || name === "number" || name === "boolean") return name;
3279
2677
  if (name === "Date") return "string";
3280
2678
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
3281
2679
  if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
3282
2680
  return "unknown";
3283
- if (name === "Ref" || name === "Reference" || name === "LoadedReference" || name === "IdentifiedReference") {
3284
- const typeArgs = typeNode.getTypeArguments();
3285
- const firstTypeArg = typeArgs[0];
3286
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3287
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3288
- }
3289
- return "unknown";
3290
- }
3291
- if (name === "Collection") {
3292
- const typeArgs = typeNode.getTypeArguments();
3293
- const firstTypeArg = typeArgs[0];
3294
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3295
- return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
3296
- }
3297
- return "Array<unknown>";
3298
- }
3299
- if (name === "Opt" || name === "Loaded") {
3300
- const typeArgs = typeNode.getTypeArguments();
3301
- const firstTypeArg = typeArgs[0];
3302
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3303
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3304
- }
3305
- return "unknown";
3306
- }
3307
- if (name === "Array") {
3308
- const typeArgs = typeNode.getTypeArguments();
3309
- const firstTypeArg = typeArgs[0];
3310
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3311
- return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
3312
- }
3313
- return "Array<unknown>";
2681
+ const wrapperMode = WRAPPER_TYPES[name];
2682
+ if (wrapperMode) {
2683
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3314
2684
  }
3315
- if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
2685
+ if (PASSTHROUGH_UTILITY.has(name)) {
3316
2686
  return typeNode.getText();
3317
2687
  }
3318
- if (name === "Promise") {
3319
- const typeArgs = typeNode.getTypeArguments();
3320
- const firstTypeArg = typeArgs[0];
3321
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3322
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3323
- }
3324
- return "unknown";
3325
- }
3326
2688
  const resolved = findType(name, sourceFile, project);
3327
2689
  if (resolved) {
3328
2690
  return expandTypeDecl(resolved, project, depth - 1);
@@ -3338,6 +2700,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3338
2700
  if (kind === SyntaxKind3.AnyKeyword) return "unknown";
3339
2701
  return typeNode.getText();
3340
2702
  }
2703
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
2704
+ const typeArgs = typeNode.getTypeArguments();
2705
+ const firstTypeArg = typeArgs[0];
2706
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2707
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
2708
+ return mode === "arrayOf" ? `Array<${inner}>` : inner;
2709
+ }
2710
+ return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2711
+ }
3341
2712
  function expandTypeDecl(result, project, depth) {
3342
2713
  if (depth < 0) return "unknown";
3343
2714
  switch (result.kind) {
@@ -3403,7 +2774,7 @@ function extractParamsType(method, sourceFile, project) {
3403
2774
  const paramArgs = paramDecorator.getArguments();
3404
2775
  if (paramArgs.length === 0) continue;
3405
2776
  const nameArg = paramArgs[0];
3406
- if (!Node7.isStringLiteral(nameArg)) continue;
2777
+ if (!Node6.isStringLiteral(nameArg)) continue;
3407
2778
  const paramName = nameArg.getLiteralValue();
3408
2779
  const typeNode = param.getTypeNode();
3409
2780
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -3416,13 +2787,13 @@ function extractResponseType(method, sourceFile, project) {
3416
2787
  if (apiResponseDecorator) {
3417
2788
  const args = apiResponseDecorator.getArguments();
3418
2789
  const optsArg = args[0];
3419
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
2790
+ if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
3420
2791
  for (const prop of optsArg.getProperties()) {
3421
- if (!Node7.isPropertyAssignment(prop)) continue;
2792
+ if (!Node6.isPropertyAssignment(prop)) continue;
3422
2793
  if (prop.getName() !== "type") continue;
3423
2794
  const val = prop.getInitializer();
3424
2795
  if (!val) continue;
3425
- if (Node7.isArrayLiteralExpression(val)) {
2796
+ if (Node6.isArrayLiteralExpression(val)) {
3426
2797
  const elements = val.getElements();
3427
2798
  const firstEl = elements[0];
3428
2799
  if (elements.length > 0 && firstEl !== void 0) {
@@ -3442,7 +2813,7 @@ function extractResponseType(method, sourceFile, project) {
3442
2813
  return "unknown";
3443
2814
  }
3444
2815
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
3445
- if (!Node7.isIdentifier(node)) return "unknown";
2816
+ if (!Node6.isIdentifier(node)) return "unknown";
3446
2817
  const name = node.getText();
3447
2818
  const resolved = findType(name, sourceFile, project);
3448
2819
  if (resolved) {
@@ -3489,11 +2860,11 @@ function extractDtoContract(method, sourceFile, project) {
3489
2860
  if (apiResp) {
3490
2861
  const args = apiResp.getArguments();
3491
2862
  const optsArg = args[0];
3492
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
2863
+ if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
3493
2864
  for (const prop of optsArg.getProperties()) {
3494
- if (Node7.isPropertyAssignment(prop) && prop.getName() === "type") {
2865
+ if (Node6.isPropertyAssignment(prop) && prop.getName() === "type") {
3495
2866
  const val = prop.getInitializer();
3496
- if (val && Node7.isIdentifier(val)) {
2867
+ if (val && Node6.isIdentifier(val)) {
3497
2868
  const name = val.getText();
3498
2869
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3499
2870
  if (localDecl?.isExported()) {
@@ -3510,27 +2881,18 @@ function extractDtoContract(method, sourceFile, project) {
3510
2881
  }
3511
2882
  }
3512
2883
  }
3513
- let bodyZodText = null;
3514
- let queryZodText = null;
3515
2884
  let bodySchema = null;
3516
2885
  let querySchema = null;
3517
- const formNested = {};
3518
2886
  const formWarnings = [];
3519
2887
  const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
3520
2888
  if (bodyClass) {
3521
- const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
3522
- bodyZodText = result.schemaText;
3523
- for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
3524
- formWarnings.push(...result.warnings);
3525
2889
  bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2890
+ formWarnings.push(...bodySchema.warnings);
3526
2891
  }
3527
2892
  const queryClass = resolveParamClass(method, "Query", sourceFile, project);
3528
2893
  if (queryClass) {
3529
- const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
3530
- queryZodText = result.schemaText;
3531
- for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
3532
- formWarnings.push(...result.warnings);
3533
2894
  querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
2895
+ formWarnings.push(...querySchema.warnings);
3534
2896
  }
3535
2897
  return {
3536
2898
  query,
@@ -3543,9 +2905,6 @@ function extractDtoContract(method, sourceFile, project) {
3543
2905
  filterFields: filterInfo?.fieldNames ?? null,
3544
2906
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
3545
2907
  filterSource: filterInfo?.source ?? null,
3546
- bodyZodText,
3547
- queryZodText,
3548
- formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
3549
2908
  formWarnings,
3550
2909
  bodySchema,
3551
2910
  querySchema
@@ -3565,6 +2924,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
3565
2924
  }
3566
2925
  return null;
3567
2926
  }
2927
+
2928
+ // src/discovery/zod-ast-to-ts.ts
2929
+ import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
2930
+ function zodAstToTs(node) {
2931
+ if (!Node7.isCallExpression(node)) return "unknown";
2932
+ const expr = node.getExpression();
2933
+ if (Node7.isPropertyAccessExpression(expr)) {
2934
+ const methodName = expr.getName();
2935
+ const receiver = expr.getExpression();
2936
+ if (methodName === "optional") {
2937
+ return `${zodAstToTs(receiver)} | undefined`;
2938
+ }
2939
+ if (methodName === "nullable") {
2940
+ return `${zodAstToTs(receiver)} | null`;
2941
+ }
2942
+ const args = node.getArguments();
2943
+ switch (methodName) {
2944
+ case "string":
2945
+ return "string";
2946
+ case "number":
2947
+ return "number";
2948
+ case "boolean":
2949
+ return "boolean";
2950
+ case "unknown":
2951
+ return "unknown";
2952
+ case "any":
2953
+ return "unknown";
2954
+ case "literal": {
2955
+ const lit = args[0];
2956
+ if (!lit) return "unknown";
2957
+ if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2958
+ if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2959
+ if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
2960
+ if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
2961
+ return "unknown";
2962
+ }
2963
+ case "enum": {
2964
+ const arrArg = args[0];
2965
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
2966
+ const members = arrArg.getElements().map(
2967
+ (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2968
+ );
2969
+ return members.join(" | ");
2970
+ }
2971
+ case "array": {
2972
+ const inner = args[0];
2973
+ if (!inner) return "unknown";
2974
+ return `Array<${zodAstToTs(inner)}>`;
2975
+ }
2976
+ case "object": {
2977
+ const objArg = args[0];
2978
+ if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
2979
+ const lines = [];
2980
+ for (const prop of objArg.getProperties()) {
2981
+ if (!Node7.isPropertyAssignment(prop)) continue;
2982
+ const key = prop.getName();
2983
+ const valNode = prop.getInitializer();
2984
+ if (!valNode) continue;
2985
+ const tsType = zodAstToTs(valNode);
2986
+ const isOpt = isOptionalChain(valNode);
2987
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
2988
+ }
2989
+ return `{ ${lines.join("; ")} }`;
2990
+ }
2991
+ case "union": {
2992
+ const arrArg = args[0];
2993
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
2994
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
2995
+ }
2996
+ case "record": {
2997
+ const valArg = args.length === 1 ? args[0] : args[1];
2998
+ if (!valArg) return "unknown";
2999
+ return `Record<string, ${zodAstToTs(valArg)}>`;
3000
+ }
3001
+ case "tuple": {
3002
+ const arrArg = args[0];
3003
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
3004
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3005
+ }
3006
+ default:
3007
+ return "unknown";
3008
+ }
3009
+ }
3010
+ return "unknown";
3011
+ }
3012
+ function isOptionalChain(node) {
3013
+ if (!Node7.isCallExpression(node)) return false;
3014
+ const expr = node.getExpression();
3015
+ return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3016
+ }
3017
+ function parseDefineContractCall(callExpr) {
3018
+ if (!Node7.isCallExpression(callExpr)) return null;
3019
+ const callee = callExpr.getExpression();
3020
+ const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
3021
+ if (calleeName !== "defineContract") return null;
3022
+ const args = callExpr.getArguments();
3023
+ const optsArg = args[0];
3024
+ if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3025
+ let query = null;
3026
+ let body = null;
3027
+ let response = "unknown";
3028
+ let bodyZodText = null;
3029
+ let queryZodText = null;
3030
+ for (const prop of optsArg.getProperties()) {
3031
+ if (!Node7.isPropertyAssignment(prop)) continue;
3032
+ const propName = prop.getName();
3033
+ const val = prop.getInitializer();
3034
+ if (!val) continue;
3035
+ if (propName === "query") {
3036
+ query = zodAstToTs(val);
3037
+ queryZodText = val.getText();
3038
+ } else if (propName === "body") {
3039
+ body = zodAstToTs(val);
3040
+ bodyZodText = val.getText();
3041
+ } else if (propName === "response") {
3042
+ response = zodAstToTs(val);
3043
+ }
3044
+ }
3045
+ return { query, body, response, bodyZodText, queryZodText };
3046
+ }
3047
+
3048
+ // src/discovery/contracts-fast.ts
3049
+ async function discoverContractsFast(opts) {
3050
+ const { cwd, glob, tsconfig } = opts;
3051
+ const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
3052
+ let project;
3053
+ try {
3054
+ project = new Project3({
3055
+ tsConfigFilePath: tsconfigPath,
3056
+ skipAddingFilesFromTsConfig: true,
3057
+ skipLoadingLibFiles: true,
3058
+ skipFileDependencyResolution: true
3059
+ });
3060
+ } catch {
3061
+ project = new Project3({
3062
+ skipAddingFilesFromTsConfig: true,
3063
+ skipLoadingLibFiles: true,
3064
+ skipFileDependencyResolution: true,
3065
+ compilerOptions: {
3066
+ allowJs: true,
3067
+ resolveJsonModule: false,
3068
+ strict: false
3069
+ }
3070
+ });
3071
+ }
3072
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3073
+ for (const f of files) {
3074
+ project.addSourceFileAtPath(f);
3075
+ }
3076
+ const routes = [];
3077
+ setDiscoveryContext(project, {
3078
+ projectRoot: cwd,
3079
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3080
+ });
3081
+ for (const sourceFile of project.getSourceFiles()) {
3082
+ routes.push(...extractFromSourceFile(sourceFile, project));
3083
+ }
3084
+ return routes;
3085
+ }
3086
+ function decoratorStringArg(decoratorExpr) {
3087
+ if (!decoratorExpr) return void 0;
3088
+ if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3089
+ if (Node8.isArrayLiteralExpression(decoratorExpr)) {
3090
+ const first = decoratorExpr.getElements()[0];
3091
+ if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
3092
+ }
3093
+ return void 0;
3094
+ }
3095
+ function deriveClassSegment(className) {
3096
+ const noSuffix = className.replace(/Controller$/, "");
3097
+ if (!noSuffix) {
3098
+ throw new Error(
3099
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3100
+ );
3101
+ }
3102
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3103
+ }
3104
+ function resolveRouteName(className, methodName, classAs, methodAs) {
3105
+ const classPortion = classAs ?? deriveClassSegment(className);
3106
+ const methodPortion = methodAs ?? methodName;
3107
+ return `${classPortion}.${methodPortion}`;
3108
+ }
3109
+ function joinPaths(prefix, suffix) {
3110
+ if (!prefix && !suffix) return "/";
3111
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3112
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3113
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3114
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3115
+ const combined = p + s;
3116
+ return combined === "" ? "/" : combined;
3117
+ }
3118
+ function extractParams(path) {
3119
+ const matches = path.matchAll(/:(\w+)/g);
3120
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3121
+ }
3568
3122
  var HTTP_METHOD_DECORATORS = {
3569
3123
  Get: "GET",
3570
3124
  Post: "POST",
@@ -3575,176 +3129,186 @@ var HTTP_METHOD_DECORATORS = {
3575
3129
  Head: "HEAD",
3576
3130
  All: "ALL"
3577
3131
  };
3132
+ function resolveVerb(method) {
3133
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3134
+ const httpDecorator = method.getDecorator(decoratorName);
3135
+ if (httpDecorator) {
3136
+ const httpArgs = httpDecorator.getArguments();
3137
+ const pathArg = httpArgs[0];
3138
+ return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3139
+ }
3140
+ }
3141
+ return null;
3142
+ }
3143
+ function readAsDecorator(node, label) {
3144
+ const asDecorator = node.getDecorator("As");
3145
+ if (!asDecorator) return void 0;
3146
+ const asName = decoratorStringArg(asDecorator.getArguments()[0]);
3147
+ if (!asName) {
3148
+ throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
3149
+ }
3150
+ return asName;
3151
+ }
3152
+ function buildRoute(args) {
3153
+ const {
3154
+ className,
3155
+ methodName,
3156
+ resolvedMethod,
3157
+ combinedPath,
3158
+ classAs,
3159
+ methodAs,
3160
+ sourceFile,
3161
+ seenNames,
3162
+ contractSource
3163
+ } = args;
3164
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3165
+ const qualifiedRef = `${className}.${methodName}`;
3166
+ const existing = seenNames.get(routeName);
3167
+ if (existing !== void 0) {
3168
+ throw new Error(
3169
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3170
+ );
3171
+ }
3172
+ seenNames.set(routeName, qualifiedRef);
3173
+ return {
3174
+ method: resolvedMethod,
3175
+ path: combinedPath,
3176
+ name: routeName,
3177
+ params: extractParams(combinedPath),
3178
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3179
+ contract: { contractSource }
3180
+ };
3181
+ }
3182
+ function extractContractRoute(args) {
3183
+ const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
3184
+ const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3185
+ if (!firstDecoratorArg) return null;
3186
+ let contractDef = null;
3187
+ let bodyZodRef = null;
3188
+ let queryZodRef = null;
3189
+ if (Node8.isCallExpression(firstDecoratorArg)) {
3190
+ contractDef = parseDefineContractCall(firstDecoratorArg);
3191
+ } else if (Node8.isIdentifier(firstDecoratorArg)) {
3192
+ const identName = firstDecoratorArg.getText();
3193
+ const varDecl = sourceFile.getVariableDeclaration(identName);
3194
+ if (!varDecl) {
3195
+ console.warn(
3196
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3197
+ );
3198
+ return null;
3199
+ }
3200
+ const initializer = varDecl.getInitializer();
3201
+ if (!initializer) return null;
3202
+ contractDef = parseDefineContractCall(initializer);
3203
+ if (contractDef && varDecl.isExported()) {
3204
+ const filePath = sourceFile.getFilePath();
3205
+ if (contractDef.body !== null) {
3206
+ bodyZodRef = { name: `${identName}.body`, filePath };
3207
+ }
3208
+ if (contractDef.query !== null) {
3209
+ queryZodRef = { name: `${identName}.query`, filePath };
3210
+ }
3211
+ }
3212
+ } else {
3213
+ console.warn(
3214
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3215
+ );
3216
+ return null;
3217
+ }
3218
+ if (!contractDef) return null;
3219
+ if (!verb) return null;
3220
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
3221
+ const methodName = method.getName();
3222
+ const classAs = readAsDecorator(cls, `class ${className}`);
3223
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3224
+ return buildRoute({
3225
+ className,
3226
+ methodName,
3227
+ resolvedMethod: verb.httpMethod,
3228
+ combinedPath: resolvedPath,
3229
+ classAs,
3230
+ methodAs,
3231
+ sourceFile,
3232
+ seenNames,
3233
+ contractSource: {
3234
+ query: contractDef.query,
3235
+ body: contractDef.body,
3236
+ response: contractDef.response,
3237
+ // Path A: capture both the importable ref and the raw text. The emitter
3238
+ // prefers inlining the text (client-safe — re-exporting from a controller
3239
+ // would drag server-only deps into the client bundle).
3240
+ bodyZodRef,
3241
+ bodyZodText: contractDef.bodyZodText,
3242
+ queryZodRef,
3243
+ queryZodText: contractDef.queryZodText
3244
+ }
3245
+ });
3246
+ }
3247
+ function extractDtoRoute(args) {
3248
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
3249
+ if (!verb) return null;
3250
+ const combined = joinPaths(prefix, verb.handlerPath);
3251
+ const methodName = method.getName();
3252
+ const classAs = readAsDecorator(cls, `class ${className}`);
3253
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3254
+ const dtoContract = extractDtoContract(method, sourceFile, project);
3255
+ return buildRoute({
3256
+ className,
3257
+ methodName,
3258
+ resolvedMethod: verb.httpMethod,
3259
+ combinedPath: combined,
3260
+ classAs,
3261
+ methodAs,
3262
+ sourceFile,
3263
+ seenNames,
3264
+ contractSource: {
3265
+ query: dtoContract?.query ?? null,
3266
+ body: dtoContract?.body ?? null,
3267
+ response: dtoContract?.response ?? "unknown",
3268
+ queryRef: dtoContract?.queryRef ?? null,
3269
+ bodyRef: dtoContract?.bodyRef ?? null,
3270
+ responseRef: dtoContract?.responseRef ?? null,
3271
+ filterFields: dtoContract?.filterFields ?? null,
3272
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3273
+ filterSource: dtoContract?.filterSource ?? null,
3274
+ formWarnings: dtoContract?.formWarnings ?? [],
3275
+ bodySchema: dtoContract?.bodySchema ?? null,
3276
+ querySchema: dtoContract?.querySchema ?? null
3277
+ }
3278
+ });
3279
+ }
3578
3280
  function extractFromSourceFile(sourceFile, project) {
3579
3281
  const routes = [];
3580
3282
  const seenNames = /* @__PURE__ */ new Map();
3581
- const classes = sourceFile.getClasses();
3582
- for (const cls of classes) {
3283
+ for (const cls of sourceFile.getClasses()) {
3583
3284
  const controllerDecorator = cls.getDecorator("Controller");
3584
3285
  if (!controllerDecorator) continue;
3585
- const controllerArgs = controllerDecorator.getArguments();
3586
- const firstArg3 = controllerArgs[0];
3587
- const prefix = decoratorStringArg(firstArg3) ?? "";
3286
+ const firstArg2 = controllerDecorator.getArguments()[0];
3287
+ const prefix = decoratorStringArg(firstArg2) ?? "";
3588
3288
  const className = cls.getName() ?? "Unknown";
3589
3289
  for (const method of cls.getMethods()) {
3590
- let httpMethod;
3591
- let handlerPath = "";
3592
- for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3593
- const httpDecorator = method.getDecorator(decoratorName);
3594
- if (httpDecorator) {
3595
- httpMethod = verb;
3596
- const httpArgs = httpDecorator.getArguments();
3597
- const pathArg = httpArgs[0];
3598
- handlerPath = decoratorStringArg(pathArg) ?? "";
3599
- break;
3600
- }
3601
- }
3290
+ const verb = resolveVerb(method);
3602
3291
  const applyContractDecorator = method.getDecorator("ApplyContract");
3603
- if (applyContractDecorator) {
3604
- const decoratorArgs = applyContractDecorator.getArguments();
3605
- const firstDecoratorArg = decoratorArgs[0];
3606
- if (!firstDecoratorArg) continue;
3607
- let contractDef = null;
3608
- let bodyZodRef = null;
3609
- let queryZodRef = null;
3610
- if (Node7.isCallExpression(firstDecoratorArg)) {
3611
- contractDef = parseDefineContractCall(firstDecoratorArg);
3612
- } else if (Node7.isIdentifier(firstDecoratorArg)) {
3613
- const identName = firstDecoratorArg.getText();
3614
- const varDecl = sourceFile.getVariableDeclaration(identName);
3615
- if (!varDecl) {
3616
- console.warn(
3617
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3618
- );
3619
- continue;
3620
- }
3621
- const initializer = varDecl.getInitializer();
3622
- if (!initializer) continue;
3623
- contractDef = parseDefineContractCall(initializer);
3624
- if (contractDef && varDecl.isExported()) {
3625
- const filePath = sourceFile.getFilePath();
3626
- if (contractDef.body !== null) {
3627
- bodyZodRef = { name: `${identName}.body`, filePath };
3628
- }
3629
- if (contractDef.query !== null) {
3630
- queryZodRef = { name: `${identName}.query`, filePath };
3631
- }
3632
- }
3633
- } else {
3634
- console.warn(
3635
- `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3636
- );
3637
- continue;
3638
- }
3639
- if (!contractDef) continue;
3640
- if (!httpMethod) continue;
3641
- const resolvedMethod = httpMethod;
3642
- const resolvedPath = joinPaths(prefix, handlerPath);
3643
- const combined = resolvedPath;
3644
- const params = extractParams(combined);
3645
- const methodName = method.getName();
3646
- const classAsDecorator = cls.getDecorator("As");
3647
- let classAs;
3648
- if (classAsDecorator) {
3649
- const classAsArgs = classAsDecorator.getArguments();
3650
- const classAsName = decoratorStringArg(classAsArgs[0]);
3651
- if (!classAsName) {
3652
- throw new Error(
3653
- `@As decorator on class ${className} must have a non-empty string argument.`
3654
- );
3655
- }
3656
- classAs = classAsName;
3657
- }
3658
- const methodAsDecorator = method.getDecorator("As");
3659
- let methodAs;
3660
- if (methodAsDecorator) {
3661
- const methodAsArgs = methodAsDecorator.getArguments();
3662
- const methodAsName = decoratorStringArg(methodAsArgs[0]);
3663
- if (!methodAsName) {
3664
- throw new Error(
3665
- `@As decorator on ${className}.${methodName} must have a non-empty string argument.`
3666
- );
3667
- }
3668
- methodAs = methodAsName;
3669
- }
3670
- const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3671
- const qualifiedRef = `${className}.${methodName}`;
3672
- const existing = seenNames.get(routeName);
3673
- if (existing !== void 0) {
3674
- throw new Error(
3675
- `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3676
- );
3677
- }
3678
- seenNames.set(routeName, qualifiedRef);
3679
- routes.push({
3680
- method: resolvedMethod,
3681
- path: combined,
3682
- name: routeName,
3683
- params,
3684
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3685
- contract: {
3686
- contractSource: {
3687
- query: contractDef.query,
3688
- body: contractDef.body,
3689
- response: contractDef.response,
3690
- // Path A: capture both the importable ref and the raw text. The
3691
- // emitter prefers inlining the text (client-safe — re-exporting from
3692
- // a controller would drag server-only deps into the client bundle).
3693
- bodyZodRef,
3694
- bodyZodText: contractDef.bodyZodText,
3695
- queryZodRef,
3696
- queryZodText: contractDef.queryZodText
3697
- }
3698
- }
3699
- });
3700
- } else {
3701
- if (!httpMethod) continue;
3702
- const combined = joinPaths(prefix, handlerPath);
3703
- const params = extractParams(combined);
3704
- const methodName = method.getName();
3705
- const classAsDecorator = cls.getDecorator("As");
3706
- let classAs;
3707
- if (classAsDecorator) {
3708
- const classAsArgs = classAsDecorator.getArguments();
3709
- const classAsName = decoratorStringArg(classAsArgs[0]);
3710
- if (classAsName) classAs = classAsName;
3711
- }
3712
- const methodAsDecorator = method.getDecorator("As");
3713
- let methodAs;
3714
- if (methodAsDecorator) {
3715
- const methodAsArgs = methodAsDecorator.getArguments();
3716
- const methodAsName = decoratorStringArg(methodAsArgs[0]);
3717
- if (methodAsName) methodAs = methodAsName;
3718
- }
3719
- const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3720
- const dtoContract = extractDtoContract(method, sourceFile, project);
3721
- routes.push({
3722
- method: httpMethod,
3723
- path: combined,
3724
- name: routeName,
3725
- params,
3726
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3727
- contract: {
3728
- contractSource: {
3729
- query: dtoContract?.query ?? null,
3730
- body: dtoContract?.body ?? null,
3731
- response: dtoContract?.response ?? "unknown",
3732
- queryRef: dtoContract?.queryRef ?? null,
3733
- bodyRef: dtoContract?.bodyRef ?? null,
3734
- responseRef: dtoContract?.responseRef ?? null,
3735
- filterFields: dtoContract?.filterFields ?? null,
3736
- filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3737
- filterSource: dtoContract?.filterSource ?? null,
3738
- bodyZodText: dtoContract?.bodyZodText ?? null,
3739
- queryZodText: dtoContract?.queryZodText ?? null,
3740
- formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
3741
- formWarnings: dtoContract?.formWarnings ?? [],
3742
- bodySchema: dtoContract?.bodySchema ?? null,
3743
- querySchema: dtoContract?.querySchema ?? null
3744
- }
3745
- }
3746
- });
3747
- }
3292
+ const route = applyContractDecorator ? extractContractRoute({
3293
+ cls,
3294
+ method,
3295
+ applyContractDecorator,
3296
+ verb,
3297
+ prefix,
3298
+ className,
3299
+ sourceFile,
3300
+ seenNames
3301
+ }) : extractDtoRoute({
3302
+ cls,
3303
+ method,
3304
+ verb,
3305
+ prefix,
3306
+ className,
3307
+ sourceFile,
3308
+ project,
3309
+ seenNames
3310
+ });
3311
+ if (route) routes.push(route);
3748
3312
  }
3749
3313
  }
3750
3314
  return routes;
@@ -3753,7 +3317,7 @@ function extractFromSourceFile(sourceFile, project) {
3753
3317
  // src/watch/lock-file.ts
3754
3318
  import { open } from "fs/promises";
3755
3319
  import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3756
- import { join as join11 } from "path";
3320
+ import { join as join12 } from "path";
3757
3321
  var LOCK_FILE = ".watcher.lock";
3758
3322
  function isProcessAlive(pid) {
3759
3323
  try {
@@ -3765,7 +3329,7 @@ function isProcessAlive(pid) {
3765
3329
  }
3766
3330
  async function acquireLock(outDir) {
3767
3331
  await mkdir8(outDir, { recursive: true });
3768
- const lockPath = join11(outDir, LOCK_FILE);
3332
+ const lockPath = join12(outDir, LOCK_FILE);
3769
3333
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3770
3334
  try {
3771
3335
  const fd = await open(lockPath, "wx");
@@ -3805,7 +3369,7 @@ async function watch(config, onChange) {
3805
3369
  if (lock === null) {
3806
3370
  let holderPid = "unknown";
3807
3371
  try {
3808
- const raw = await readFile3(join12(config.codegen.outDir, ".watcher.lock"), "utf8");
3372
+ const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
3809
3373
  const data = JSON.parse(raw);
3810
3374
  if (data.pid !== void 0) holderPid = String(data.pid);
3811
3375
  } catch {
@@ -3833,7 +3397,7 @@ async function watch(config, onChange) {
3833
3397
  }
3834
3398
  let pagesDebounceTimer;
3835
3399
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3836
- const pagesWatcher = chokidar.watch(join12(config.codegen.cwd, pagesGlob), {
3400
+ const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
3837
3401
  ignoreInitial: true,
3838
3402
  persistent: true,
3839
3403
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3859,7 +3423,7 @@ async function watch(config, onChange) {
3859
3423
  pagesWatcher.on("change", schedulePagesRegenerate);
3860
3424
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3861
3425
  let contractsDebounceTimer;
3862
- const contractsWatcher = chokidar.watch(join12(config.codegen.cwd, config.contracts.glob), {
3426
+ const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
3863
3427
  ignoreInitial: true,
3864
3428
  persistent: true,
3865
3429
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3889,7 +3453,7 @@ async function watch(config, onChange) {
3889
3453
  contractsWatcher.on("add", scheduleContractsRegenerate);
3890
3454
  contractsWatcher.on("change", scheduleContractsRegenerate);
3891
3455
  contractsWatcher.on("unlink", scheduleContractsRegenerate);
3892
- const formsWatcher = chokidar.watch(join12(config.codegen.cwd, config.forms.watch), {
3456
+ const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
3893
3457
  ignoreInitial: true,
3894
3458
  persistent: true,
3895
3459
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3915,8 +3479,57 @@ async function watch(config, onChange) {
3915
3479
  };
3916
3480
  }
3917
3481
 
3482
+ // src/ir/render-ts-type.ts
3483
+ function tsKey(name) {
3484
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
3485
+ }
3486
+ function renderTsType(node, ctx) {
3487
+ switch (node.kind) {
3488
+ case "string":
3489
+ return "string";
3490
+ case "number":
3491
+ return "number";
3492
+ case "boolean":
3493
+ return "boolean";
3494
+ case "date":
3495
+ return "Date";
3496
+ case "unknown":
3497
+ return "unknown";
3498
+ case "instanceof":
3499
+ return node.ctor;
3500
+ case "enum":
3501
+ return node.literals.join(" | ");
3502
+ case "literal":
3503
+ return node.raw;
3504
+ case "union":
3505
+ return node.options.map((o) => renderTsType(o, ctx)).join(" | ");
3506
+ case "array":
3507
+ return `Array<${renderTsType(node.element, ctx)}>`;
3508
+ case "optional":
3509
+ return `${renderTsType(node.inner, ctx)} | undefined`;
3510
+ case "annotated":
3511
+ return renderTsType(node.inner, ctx);
3512
+ case "object": {
3513
+ if (node.fields.length === 0) return node.passthrough ? "Record<string, unknown>" : "{}";
3514
+ const inner = node.fields.map((f) => {
3515
+ if (f.value.kind === "optional") {
3516
+ return `${tsKey(f.key)}?: ${renderTsType(f.value.inner, ctx)}`;
3517
+ }
3518
+ return `${tsKey(f.key)}: ${renderTsType(f.value, ctx)}`;
3519
+ }).join("; ");
3520
+ return `{ ${inner} }`;
3521
+ }
3522
+ case "ref":
3523
+ case "lazyRef": {
3524
+ if (ctx.recursive.has(node.name)) return ctx.typeNameFor(node.name);
3525
+ const target = ctx.named.get(node.name);
3526
+ return target ? renderTsType(target, ctx) : "unknown";
3527
+ }
3528
+ }
3529
+ }
3530
+
3918
3531
  // src/index.ts
3919
- var VERSION = "0.2.1";
3532
+ var VERSION = "0.4.0";
3920
3533
  export {
3921
3534
  CodegenError,
3922
3535
  ConfigError,
@@ -3930,9 +3543,9 @@ export {
3930
3543
  extractSchemaFromDto,
3931
3544
  generate,
3932
3545
  loadConfig,
3546
+ renderTsType,
3933
3547
  resolveAdapter,
3934
3548
  resolveConfig,
3935
- watch,
3936
- zodAdapter
3549
+ watch
3937
3550
  };
3938
3551
  //# sourceMappingURL=index.js.map