@dudousxd/nestjs-codegen 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -44,8 +44,7 @@ __export(src_exports, {
44
44
  loadConfig: () => loadConfig,
45
45
  resolveAdapter: () => resolveAdapter,
46
46
  resolveConfig: () => resolveConfig,
47
- watch: () => watch,
48
- zodAdapter: () => zodAdapter
47
+ watch: () => watch
49
48
  });
50
49
  module.exports = __toCommonJS(src_exports);
51
50
 
@@ -73,112 +72,9 @@ var CodegenError = class extends Error {
73
72
  }
74
73
  };
75
74
 
76
- // src/adapters/zod.ts
77
- function toObjectKey(name) {
78
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
79
- }
80
- function messageSuffix(messageRaw2) {
81
- return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
82
- }
83
- function renderStringChecks(checks) {
84
- let out = "";
85
- for (const c of checks) {
86
- switch (c.check) {
87
- case "email":
88
- out += `.email(${messageSuffix(c.messageRaw)})`;
89
- break;
90
- case "url":
91
- out += `.url(${messageSuffix(c.messageRaw)})`;
92
- break;
93
- case "uuid":
94
- out += `.uuid(${messageSuffix(c.messageRaw)})`;
95
- break;
96
- case "regex":
97
- out += `.regex(${c.pattern})`;
98
- break;
99
- case "min":
100
- out += `.min(${c.value})`;
101
- break;
102
- case "max":
103
- out += `.max(${c.value})`;
104
- break;
105
- }
106
- }
107
- return out;
108
- }
109
- function render(node, ctx) {
110
- switch (node.kind) {
111
- case "string":
112
- return `z.string()${renderStringChecks(node.checks)}`;
113
- case "number": {
114
- let out = "z.number()";
115
- for (const c of node.checks) {
116
- if (c.check === "int") out += ".int()";
117
- else if (c.check === "positive") out += ".positive()";
118
- else if (c.check === "negative") out += ".negative()";
119
- else if (c.check === "min") out += `.min(${c.value})`;
120
- else if (c.check === "max") out += `.max(${c.value})`;
121
- }
122
- return out;
123
- }
124
- case "boolean":
125
- return "z.boolean()";
126
- case "date":
127
- return "z.coerce.date()";
128
- case "unknown":
129
- return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
130
- case "instanceof":
131
- return `z.instanceof(${node.ctor})`;
132
- case "enum":
133
- return `z.enum([${node.literals.join(", ")}])`;
134
- case "literal":
135
- return `z.literal(${node.raw})`;
136
- case "union":
137
- return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
138
- case "object": {
139
- if (node.fields.length === 0) {
140
- return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
141
- }
142
- const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
143
- return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
144
- }
145
- case "array":
146
- return `z.array(${render(node.element, ctx)})`;
147
- case "optional":
148
- return `${render(node.inner, ctx)}.optional()`;
149
- case "ref":
150
- return node.name;
151
- case "lazyRef":
152
- return `z.lazy(() => ${node.name})`;
153
- case "annotated": {
154
- const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
155
- return `${render(node.inner, ctx)} ${comments}`;
156
- }
157
- }
158
- }
159
- var zodAdapter = {
160
- name: "zod",
161
- importStatements(usage) {
162
- return usage.used ? ["import { z } from 'zod';"] : [];
163
- },
164
- render,
165
- inferType(schemaConst) {
166
- return `z.infer<typeof ${schemaConst}>`;
167
- },
168
- renderModule(mod) {
169
- const ctx = { named: mod.named };
170
- const namedNestedSchemas = /* @__PURE__ */ new Map();
171
- for (const [name, node] of mod.named) {
172
- namedNestedSchemas.set(name, render(node, ctx));
173
- }
174
- return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
175
- }
176
- };
177
-
178
75
  // src/adapters/registry.ts
179
76
  function resolveAdapter(option) {
180
77
  if (typeof option !== "string") return option;
181
- if (option === "zod") return zodAdapter;
182
78
  const pkg = `@dudousxd/nestjs-codegen-${option}`;
183
79
  const named = `${option}Adapter`;
184
80
  throw new ConfigError(
@@ -231,8 +127,21 @@ If this is intentional, move the file inside your project directory.`
231
127
  function resolveConfig(userConfig, cwd) {
232
128
  return applyDefaults(userConfig, cwd ?? process.cwd());
233
129
  }
130
+ function validateUserConfig(userConfig) {
131
+ if (userConfig.validation == null) {
132
+ throw new ConfigError(
133
+ "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
134
+ );
135
+ }
136
+ if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
137
+ throw new ConfigError(
138
+ "Config validation failed: `pages.glob` must be a string when `pages` is set"
139
+ );
140
+ }
141
+ }
234
142
  function applyDefaults(userConfig, cwd) {
235
- const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-inertia");
143
+ validateUserConfig(userConfig);
144
+ const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-codegen");
236
145
  const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
237
146
  let app = null;
238
147
  if (userConfig.app) {
@@ -250,7 +159,8 @@ function applyDefaults(userConfig, cwd) {
250
159
  }
251
160
  return {
252
161
  extensions: userConfig.extensions ?? [],
253
- validation: resolveAdapter(userConfig.validation ?? "zod"),
162
+ // Non-null: validateUserConfig() above throws when `validation` is absent.
163
+ validation: resolveAdapter(userConfig.validation),
254
164
  pages: userConfig.pages ? {
255
165
  glob: userConfig.pages.glob,
256
166
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
@@ -304,18 +214,12 @@ Run \`nestjs-codegen init\` to create a starter config.`
304
214
  `Config file must have a default export. Did you forget \`export default defineConfig({...})\`? (${configPath})`
305
215
  );
306
216
  }
307
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
308
- throw new ConfigError(
309
- `Config validation failed: \`pages.glob\` must be a string when \`pages\` is set (${configPath})`
310
- );
311
- }
312
217
  return applyDefaults(userConfig, resolvedCwd);
313
218
  }
314
219
 
315
220
  // src/generate.ts
316
221
  var import_promises9 = require("fs/promises");
317
- var import_node_path9 = require("path");
318
- var import_ts_morph3 = require("ts-morph");
222
+ var import_node_path10 = require("path");
319
223
 
320
224
  // src/discovery/pages.ts
321
225
  var import_promises2 = require("fs/promises");
@@ -370,6 +274,7 @@ function extractPropsSource(source, exportName) {
370
274
  }
371
275
 
372
276
  // src/discovery/shared-props.ts
277
+ var import_node_path3 = require("path");
373
278
  var import_ts_morph = require("ts-morph");
374
279
  function discoverSharedProps(project, moduleEntry) {
375
280
  try {
@@ -390,6 +295,31 @@ function discoverSharedProps(project, moduleEntry) {
390
295
  return null;
391
296
  }
392
297
  }
298
+ function discoverSharedPropsFromConfig(config) {
299
+ if (!config.app?.moduleEntry) return null;
300
+ try {
301
+ const tsconfigPath = config.app.tsconfig ?? (0, import_node_path3.join)(config.codegen.cwd, "tsconfig.json");
302
+ let project;
303
+ try {
304
+ project = new import_ts_morph.Project({
305
+ tsConfigFilePath: tsconfigPath,
306
+ skipAddingFilesFromTsConfig: true,
307
+ skipLoadingLibFiles: true,
308
+ skipFileDependencyResolution: true
309
+ });
310
+ } catch {
311
+ project = new import_ts_morph.Project({
312
+ skipAddingFilesFromTsConfig: true,
313
+ skipLoadingLibFiles: true,
314
+ skipFileDependencyResolution: true,
315
+ compilerOptions: { allowJs: true, strict: false }
316
+ });
317
+ }
318
+ return discoverSharedProps(project, config.app.moduleEntry);
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
393
323
  function findForRootCall(sourceFile) {
394
324
  const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression);
395
325
  for (const call of callExpressions) {
@@ -409,9 +339,9 @@ function findForRootCall(sourceFile) {
409
339
  function findShareInitializer(forRootCall) {
410
340
  if (!import_ts_morph.Node.isCallExpression(forRootCall)) return null;
411
341
  const args = forRootCall.getArguments();
412
- const firstArg3 = args[0];
413
- if (!firstArg3 || !import_ts_morph.Node.isObjectLiteralExpression(firstArg3)) return null;
414
- for (const prop of firstArg3.getProperties()) {
342
+ const firstArg2 = args[0];
343
+ if (!firstArg2 || !import_ts_morph.Node.isObjectLiteralExpression(firstArg2)) return null;
344
+ for (const prop of firstArg2.getProperties()) {
415
345
  if (import_ts_morph.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
416
346
  return prop.getInitializer() ?? null;
417
347
  }
@@ -510,9 +440,9 @@ function extractFromReturnTypeAnnotation(typeNode) {
510
440
  const typeName = typeNode.getTypeName();
511
441
  if (import_ts_morph.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
512
442
  const typeArgs = typeNode.getTypeArguments();
513
- const firstArg3 = typeArgs[0];
514
- if (firstArg3) {
515
- return extractFromReturnTypeAnnotation(firstArg3);
443
+ const firstArg2 = typeArgs[0];
444
+ if (firstArg2) {
445
+ return extractFromReturnTypeAnnotation(firstArg2);
516
446
  }
517
447
  return null;
518
448
  }
@@ -618,25 +548,14 @@ function inferExpressionType(node) {
618
548
 
619
549
  // src/emit/emit-api.ts
620
550
  var import_promises3 = require("fs/promises");
621
- var import_node_path3 = require("path");
551
+ var import_node_path4 = require("path");
622
552
 
623
553
  // src/extension/registry.ts
624
554
  var import_ts_morph2 = require("ts-morph");
625
555
  function resolveApiSlots(extensions) {
626
- let transport;
627
- let transportOwner;
628
556
  let layer;
629
557
  let layerOwner;
630
558
  for (const ext of extensions) {
631
- if (ext.apiTransport) {
632
- if (transport) {
633
- throw new CodegenError(
634
- `api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
635
- );
636
- }
637
- transport = ext.apiTransport;
638
- transportOwner = ext.name;
639
- }
640
559
  if (ext.apiClientLayer) {
641
560
  if (layer) {
642
561
  throw new CodegenError(
@@ -648,11 +567,22 @@ function resolveApiSlots(extensions) {
648
567
  }
649
568
  }
650
569
  return {
651
- ...transport ? { transport } : {},
652
570
  ...layer ? { layer } : {}
653
571
  };
654
572
  }
655
573
  var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
574
+ function mergeExclusive(target, incoming, {
575
+ owner,
576
+ describe
577
+ }) {
578
+ for (const [key, value] of incoming) {
579
+ const prev = target.get(key);
580
+ if (prev !== void 0) {
581
+ throw new CodegenError(describe(key, prev.owner, owner));
582
+ }
583
+ target.set(key, { value, owner });
584
+ }
585
+ }
656
586
  function createExtensionContext(config, getRoutes) {
657
587
  let project;
658
588
  return {
@@ -697,29 +627,36 @@ async function collectEmittedFiles(extensions, ctx) {
697
627
  `Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
698
628
  );
699
629
  }
700
- const prev = owners.get(key);
701
- if (prev !== void 0) {
702
- throw new CodegenError(
703
- `Output file "${file.path}" is emitted by both "${prev}" and "${ext.name}". Two extensions cannot write the same file.`
704
- );
705
- }
706
- owners.set(key, ext.name);
630
+ mergeExclusive(owners, [[key, file]], {
631
+ owner: ext.name,
632
+ describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
633
+ });
707
634
  files.push(file);
708
635
  }
709
636
  }
710
637
  return files;
711
638
  }
712
639
 
640
+ // src/extension/types.ts
641
+ function requestShape(route) {
642
+ const cs = route.contract?.contractSource;
643
+ const isGet = route.method.toUpperCase() === "GET";
644
+ const isQuery = isGet || !!cs?.filterFields?.length;
645
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
646
+ const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
647
+ return { isGet, isQuery, hasBody, hasQuery };
648
+ }
649
+
713
650
  // src/emit/emit-api.ts
714
651
  async function emitApi(routes, outDir, opts = {}) {
715
652
  await (0, import_promises3.mkdir)(outDir, { recursive: true });
716
653
  const content = buildApiFile(routes, outDir, opts);
717
- await (0, import_promises3.writeFile)((0, import_node_path3.join)(outDir, "api.ts"), content, "utf8");
654
+ await (0, import_promises3.writeFile)((0, import_node_path4.join)(outDir, "api.ts"), content, "utf8");
718
655
  }
719
656
  function splitName(name) {
720
657
  return name.split(".");
721
658
  }
722
- function toObjectKey2(segment) {
659
+ function toObjectKey(segment) {
723
660
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
724
661
  return segment;
725
662
  }
@@ -812,7 +749,7 @@ function emitFilterQueryType(c) {
812
749
  }
813
750
  function buildResponseType(c, outDir) {
814
751
  if (c.controllerRef) {
815
- let relPath = (0, import_node_path3.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
752
+ let relPath = (0, import_node_path4.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
816
753
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
817
754
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
818
755
  }
@@ -826,7 +763,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
826
763
  const pad = " ".repeat(indent);
827
764
  const lines = [];
828
765
  for (const [key, node] of tree) {
829
- const objKey = toObjectKey2(key);
766
+ const objKey = toObjectKey(key);
830
767
  if (node.kind === "leaf") {
831
768
  const c = node;
832
769
  const method = c.method.toUpperCase();
@@ -852,15 +789,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
852
789
  return lines;
853
790
  }
854
791
  function buildRequestModel(c) {
855
- const isGet = c.method.toUpperCase() === "GET";
856
792
  const m = c.method.toLowerCase();
857
793
  const flat = JSON.stringify(c.name);
858
794
  const path = JSON.stringify(c.path);
859
795
  const TA = buildRouterTypeAccess(c.name);
860
796
  const withParams = hasPathParams(c.params);
861
- const hasBody = !!c.contractSource.bodyRef || c.contractSource.body != null && c.contractSource.body !== "never";
862
- const isQuery = isGet || !!c.contractSource.filterFields?.length;
863
- const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
797
+ const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
864
798
  const fields = [];
865
799
  if (withParams) fields.push(`params: ${TA}['params']`);
866
800
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -882,7 +816,6 @@ function buildRequestModel(c) {
882
816
  urlExpr,
883
817
  optsExpr,
884
818
  responseType: `${TA}['response']`,
885
- bodyType: `${TA}['body']`,
886
819
  queryKeyExpr: `[${flat}, input] as const`
887
820
  };
888
821
  }
@@ -930,7 +863,7 @@ function emitApiObjectBlock(tree, indent, p) {
930
863
  const pad = " ".repeat(indent);
931
864
  const lines = [];
932
865
  for (const [key, node] of tree) {
933
- const objKey = toObjectKey2(key);
866
+ const objKey = toObjectKey(key);
934
867
  if (node.kind === "branch") {
935
868
  lines.push(`${pad}${objKey}: {`);
936
869
  lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
@@ -938,30 +871,28 @@ function emitApiObjectBlock(tree, indent, p) {
938
871
  continue;
939
872
  }
940
873
  const req = buildRequestModel(node);
941
- const route = {
942
- method: node.method,
943
- path: node.path,
944
- name: node.name,
945
- params: node.params,
946
- contract: { contractSource: node.contractSource },
947
- ...node.controllerRef ? { controllerRef: node.controllerRef } : {}
874
+ const leaf = {
875
+ route: node.route,
876
+ request: req,
877
+ requestExpr: renderFetcherRequest(req)
948
878
  };
949
- const leaf = { route, request: req, requestExpr: "" };
950
- leaf.requestExpr = p.transport ? p.transport.renderRequest(leaf, p.ctx) : renderFetcherRequest(req);
951
- const members = {};
952
- if (p.layer) Object.assign(members, p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx));
879
+ const owned = /* @__PURE__ */ new Map();
880
+ if (p.layer) {
881
+ mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
882
+ owner: p.layer.name,
883
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
884
+ });
885
+ }
953
886
  for (const ext of p.memberExts) {
954
887
  const extra = ext.apiMembers?.(leaf, p.ctx);
955
888
  if (!extra) continue;
956
- for (const [name, value] of Object.entries(extra)) {
957
- if (name in members) {
958
- throw new Error(
959
- `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict at "${ext.name}").`
960
- );
961
- }
962
- members[name] = value;
963
- }
889
+ mergeExclusive(owned, Object.entries(extra), {
890
+ owner: ext.name,
891
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
892
+ });
964
893
  }
894
+ const members = {};
895
+ for (const [name, { value }] of owned) members[name] = value;
965
896
  lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
966
897
  }
967
898
  return lines;
@@ -970,10 +901,82 @@ function buildRouterTypeAccess(name) {
970
901
  const segments = splitName(name);
971
902
  return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
972
903
  }
904
+ var RESOLVER_HELPERS = [
905
+ // --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
906
+ "type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
907
+ " ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
908
+ " : P extends keyof R ? R[P] : never;",
909
+ "",
910
+ // --- ResolveByName: resolve a field from a dot-path name ---
911
+ "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
912
+ "",
913
+ // --- ResolveByPath: scan all leaves for matching method + url ---
914
+ // Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
915
+ "type _LeafValues<T> = T extends { method: string; url: string }",
916
+ " ? T",
917
+ " : T extends object ? _LeafValues<T[keyof T]> : never;",
918
+ "",
919
+ "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
920
+ " ? L extends { method: M; url: U }",
921
+ " ? Field extends keyof L ? L[Field] : never",
922
+ " : never",
923
+ " : never;",
924
+ ""
925
+ ];
926
+ var ROUTE_NAMESPACE = [
927
+ "export namespace Route {",
928
+ ' export type Response<K extends string> = ResolveByName<K, "response">;',
929
+ ' export type Body<K extends string> = ResolveByName<K, "body">;',
930
+ ' export type Query<K extends string> = ResolveByName<K, "query">;',
931
+ ' export type Params<K extends string> = ResolveByName<K, "params">;',
932
+ ' export type Error<K extends string> = ResolveByName<K, "error">;',
933
+ ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
934
+ " export type Request<K extends string> = {",
935
+ " body: Body<K>;",
936
+ " query: Query<K>;",
937
+ " params: Params<K>;",
938
+ " };",
939
+ "}",
940
+ ""
941
+ ];
942
+ var PATH_NAMESPACE = [
943
+ "export namespace Path {",
944
+ ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
945
+ ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
946
+ ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
947
+ ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
948
+ ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
949
+ ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
950
+ "}",
951
+ ""
952
+ ];
953
+ var EMPTY_ROUTE_NAMESPACE = [
954
+ "export namespace Route {",
955
+ " export type Response<K extends string> = never;",
956
+ " export type Body<K extends string> = never;",
957
+ " export type Query<K extends string> = never;",
958
+ " export type Params<K extends string> = never;",
959
+ " export type Error<K extends string> = never;",
960
+ " export type FilterFields<K extends string> = never;",
961
+ " export type Request<K extends string> = { body: never; query: never; params: never };",
962
+ "}",
963
+ ""
964
+ ];
965
+ var EMPTY_PATH_NAMESPACE = [
966
+ "export namespace Path {",
967
+ " export type Response<M extends string, U extends string> = never;",
968
+ " export type Body<M extends string, U extends string> = never;",
969
+ " export type Query<M extends string, U extends string> = never;",
970
+ " export type Params<M extends string, U extends string> = never;",
971
+ " export type Error<M extends string, U extends string> = never;",
972
+ " export type FilterFields<M extends string, U extends string> = never;",
973
+ "}",
974
+ ""
975
+ ];
973
976
  function buildApiFile(routes, outDir, opts = {}) {
974
977
  const fetcherImportPath = opts.fetcherImportPath;
975
978
  const extensions = opts.extensions ?? [];
976
- const { transport, layer } = resolveApiSlots(extensions);
979
+ const { layer } = resolveApiSlots(extensions);
977
980
  const memberExts = extensions.filter((e) => e.apiMembers);
978
981
  const headerExts = extensions.filter((e) => e.apiHeader);
979
982
  const contracted = routes.filter((r) => r.contract);
@@ -1018,7 +1021,6 @@ function buildApiFile(routes, outDir, opts = {}) {
1018
1021
  seenImports.add(imp);
1019
1022
  extImports.push(imp);
1020
1023
  };
1021
- for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
1022
1024
  for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
1023
1025
  for (const ext of headerExts) {
1024
1026
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
@@ -1034,8 +1036,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1034
1036
  const emittedNames = /* @__PURE__ */ new Set();
1035
1037
  for (const [filePath, names] of importsByFile) {
1036
1038
  let relPath;
1037
- if ((0, import_node_path3.isAbsolute)(filePath)) {
1038
- relPath = (0, import_node_path3.relative)(outDir, filePath).replace(/\.ts$/, "");
1039
+ if ((0, import_node_path4.isAbsolute)(filePath)) {
1040
+ relPath = (0, import_node_path4.relative)(outDir, filePath).replace(/\.ts$/, "");
1039
1041
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1040
1042
  } else {
1041
1043
  relPath = filePath;
@@ -1063,27 +1065,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1063
1065
  lines.push("}");
1064
1066
  lines.push("export type Api = ReturnType<typeof createApi>;");
1065
1067
  lines.push("");
1066
- lines.push("export namespace Route {");
1067
- lines.push(" export type Response<K extends string> = never;");
1068
- lines.push(" export type Body<K extends string> = never;");
1069
- lines.push(" export type Query<K extends string> = never;");
1070
- lines.push(" export type Params<K extends string> = never;");
1071
- lines.push(" export type Error<K extends string> = never;");
1072
- lines.push(" export type FilterFields<K extends string> = never;");
1073
- lines.push(
1074
- " export type Request<K extends string> = { body: never; query: never; params: never };"
1075
- );
1076
- lines.push("}");
1077
- lines.push("");
1078
- lines.push("export namespace Path {");
1079
- lines.push(" export type Response<M extends string, U extends string> = never;");
1080
- lines.push(" export type Body<M extends string, U extends string> = never;");
1081
- lines.push(" export type Query<M extends string, U extends string> = never;");
1082
- lines.push(" export type Params<M extends string, U extends string> = never;");
1083
- lines.push(" export type Error<M extends string, U extends string> = never;");
1084
- lines.push(" export type FilterFields<M extends string, U extends string> = never;");
1085
- lines.push("}");
1086
- lines.push("");
1068
+ lines.push(...EMPTY_ROUTE_NAMESPACE);
1069
+ lines.push(...EMPTY_PATH_NAMESPACE);
1087
1070
  return lines.join("\n");
1088
1071
  }
1089
1072
  const tree = /* @__PURE__ */ new Map();
@@ -1101,7 +1084,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1101
1084
  path: r.path,
1102
1085
  params: r.params,
1103
1086
  controllerRef: r.controllerRef,
1104
- contractSource: c.contractSource
1087
+ contractSource: c.contractSource,
1088
+ route: r
1105
1089
  };
1106
1090
  insertIntoTree(tree, segments, leaf, name);
1107
1091
  }
@@ -1114,7 +1098,6 @@ function buildApiFile(routes, outDir, opts = {}) {
1114
1098
  lines.push(" return {");
1115
1099
  lines.push(
1116
1100
  ...emitApiObjectBlock(tree, 4, {
1117
- ...transport ? { transport } : {},
1118
1101
  ...layer ? { layer } : {},
1119
1102
  memberExts,
1120
1103
  ctx
@@ -1125,61 +1108,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1125
1108
  lines.push("");
1126
1109
  lines.push("export type Api = ReturnType<typeof createApi>;");
1127
1110
  lines.push("");
1128
- lines.push("type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`");
1129
- lines.push(" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never");
1130
- lines.push(" : P extends keyof R ? R[P] : never;");
1131
- lines.push("");
1132
- lines.push(
1133
- "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
1134
- );
1135
- lines.push("");
1136
- lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
1137
- lines.push(" ? T");
1138
- lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
1139
- lines.push("");
1140
- lines.push(
1141
- "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
1142
- );
1143
- lines.push(" ? L extends { method: M; url: U }");
1144
- lines.push(" ? Field extends keyof L ? L[Field] : never");
1145
- lines.push(" : never");
1146
- lines.push(" : never;");
1147
- lines.push("");
1148
- lines.push("export namespace Route {");
1149
- lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
1150
- lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
1151
- lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
1152
- lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
1153
- lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
1154
- lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
1155
- lines.push(" export type Request<K extends string> = {");
1156
- lines.push(" body: Body<K>;");
1157
- lines.push(" query: Query<K>;");
1158
- lines.push(" params: Params<K>;");
1159
- lines.push(" };");
1160
- lines.push("}");
1161
- lines.push("");
1162
- lines.push("export namespace Path {");
1163
- lines.push(
1164
- ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
1165
- );
1166
- lines.push(
1167
- ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
1168
- );
1169
- lines.push(
1170
- ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
1171
- );
1172
- lines.push(
1173
- ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
1174
- );
1175
- lines.push(
1176
- ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
1177
- );
1178
- lines.push(
1179
- ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
1180
- );
1181
- lines.push("}");
1182
- lines.push("");
1111
+ lines.push(...RESOLVER_HELPERS);
1112
+ lines.push(...ROUTE_NAMESPACE);
1113
+ lines.push(...PATH_NAMESPACE);
1183
1114
  for (const ext of headerExts) {
1184
1115
  const statements = ext.apiHeader?.(ctx)?.statements;
1185
1116
  if (statements?.length) {
@@ -1191,7 +1122,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1191
1122
 
1192
1123
  // src/emit/emit-cache.ts
1193
1124
  var import_promises4 = require("fs/promises");
1194
- var import_node_path4 = require("path");
1125
+ var import_node_path5 = require("path");
1195
1126
  async function emitCache(pages, outDir) {
1196
1127
  await (0, import_promises4.mkdir)(outDir, { recursive: true });
1197
1128
  const entries = await Promise.all(
@@ -1205,95 +1136,21 @@ async function emitCache(pages, outDir) {
1205
1136
  })
1206
1137
  );
1207
1138
  const cache = { pages: entries };
1208
- await (0, import_promises4.writeFile)((0, import_node_path4.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1139
+ await (0, import_promises4.writeFile)((0, import_node_path5.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1209
1140
  `, "utf8");
1210
1141
  }
1211
1142
 
1212
1143
  // src/emit/emit-forms.ts
1213
1144
  var import_promises5 = require("fs/promises");
1214
- var import_node_path5 = require("path");
1145
+ var import_node_path6 = require("path");
1215
1146
  async function emitForms(routes, outDir, config, adapter) {
1216
1147
  if (config && config.enabled === false) return false;
1217
- if (adapter && adapter.name !== "zod") {
1218
- const content2 = buildFormsFileWithAdapter(routes, adapter);
1219
- if (content2 === null) return false;
1220
- await (0, import_promises5.mkdir)(outDir, { recursive: true });
1221
- await (0, import_promises5.writeFile)((0, import_node_path5.join)(outDir, "forms.ts"), content2, "utf8");
1222
- return true;
1223
- }
1224
- const entries = collectFormEntries(routes);
1225
- if (entries.length === 0) return false;
1148
+ const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
1149
+ if (content === null) return false;
1226
1150
  await (0, import_promises5.mkdir)(outDir, { recursive: true });
1227
- const content = buildFormsFile(entries, outDir, config);
1228
- await (0, import_promises5.writeFile)((0, import_node_path5.join)(outDir, "forms.ts"), content, "utf8");
1151
+ await (0, import_promises5.writeFile)((0, import_node_path6.join)(outDir, "forms.ts"), content, "utf8");
1229
1152
  return true;
1230
1153
  }
1231
- function buildFormsFileWithAdapter(routes, adapter) {
1232
- const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1233
- const methodNameCounts = /* @__PURE__ */ new Map();
1234
- for (const route of sorted) {
1235
- const cs = route.contract.contractSource;
1236
- if (!cs.bodySchema && !cs.querySchema) continue;
1237
- methodNameCounts.set(
1238
- deriveBaseName(route.name).method,
1239
- (methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
1240
- );
1241
- }
1242
- const named = /* @__PURE__ */ new Map();
1243
- const decls = [];
1244
- const mapEntries = [];
1245
- let used = false;
1246
- for (const route of sorted) {
1247
- const cs = route.contract.contractSource;
1248
- const { method, full } = deriveBaseName(route.name);
1249
- const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
1250
- const block = [];
1251
- if (cs.bodyZodText && !cs.bodySchema) {
1252
- block.push(
1253
- `// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1254
- );
1255
- }
1256
- let bodyConst;
1257
- if (cs.bodySchema) {
1258
- used = true;
1259
- const r = adapter.renderModule(cs.bodySchema);
1260
- for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
1261
- bodyConst = `${base}BodySchema`;
1262
- block.push(`export const ${bodyConst} = ${r.schemaText};`);
1263
- block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
1264
- }
1265
- if (cs.querySchema) {
1266
- used = true;
1267
- const r = adapter.renderModule(cs.querySchema);
1268
- for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
1269
- const queryConst = `${base}QuerySchema`;
1270
- block.push(`export const ${queryConst} = ${r.schemaText};`);
1271
- block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
1272
- }
1273
- if (block.length === 0) continue;
1274
- decls.push(`// ${route.name}`, ...block, "");
1275
- if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
1276
- }
1277
- if (!used) return null;
1278
- const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1279
- for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1280
- lines.push("");
1281
- if (named.size > 0) {
1282
- lines.push("// Hoisted nested schemas (shared across endpoints).");
1283
- for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
1284
- lines.push("");
1285
- }
1286
- lines.push(...decls);
1287
- lines.push("/** Route name \u2192 body schema map. */");
1288
- lines.push("export const formSchemas = {");
1289
- lines.push(...mapEntries);
1290
- lines.push("} as const;");
1291
- lines.push("");
1292
- return lines.join("\n");
1293
- }
1294
- function hasSchema(src) {
1295
- return !!src && (src.ref !== null || src.text !== null);
1296
- }
1297
1154
  function pascal(segment) {
1298
1155
  return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1299
1156
  }
@@ -1303,124 +1160,16 @@ function deriveBaseName(routeName) {
1303
1160
  const full = segments.map(pascal).join("");
1304
1161
  return { method, full };
1305
1162
  }
1306
- function collectFormEntries(routes) {
1307
- const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1308
- const methodNameCounts = /* @__PURE__ */ new Map();
1309
- const candidates = [];
1310
- for (const route of sorted) {
1311
- const cs = route.contract.contractSource;
1312
- const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
1313
- const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
1314
- if (!hasSchema(body) && !hasSchema(query)) continue;
1315
- const { method, full } = deriveBaseName(route.name);
1316
- methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1317
- candidates.push({ route, method, full });
1318
- }
1319
- const entries = [];
1320
- for (const { route, method, full } of candidates) {
1321
- const cs = route.contract.contractSource;
1322
- const collision = (methodNameCounts.get(method) ?? 0) > 1;
1323
- const baseName = collision ? full : method;
1324
- const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
1325
- const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
1326
- entries.push({
1327
- routeName: route.name,
1328
- baseName,
1329
- body: hasSchema(body) ? body : void 0,
1330
- query: hasSchema(query) ? query : void 0,
1331
- nestedSchemas: cs.formNestedSchemas ?? null,
1332
- warnings: cs.formWarnings ?? []
1333
- });
1334
- }
1335
- return entries;
1336
- }
1337
1163
  function relImport(outDir, filePath) {
1338
- let relPath = (0, import_node_path5.relative)(outDir, filePath).replace(/\.ts$/, "");
1164
+ let relPath = (0, import_node_path6.relative)(outDir, filePath).replace(/\.ts$/, "");
1339
1165
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1340
1166
  return relPath;
1341
1167
  }
1342
1168
  function refRootIdentifier(refName) {
1343
1169
  return refName.split(".")[0] ?? refName;
1344
1170
  }
1345
- function buildFormsFile(entries, outDir, config) {
1346
- const zodImport = config?.zodImport ?? "zod";
1347
- const lines = [
1348
- "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1349
- `import { z } from '${zodImport}';`
1350
- ];
1351
- const importsByFile = /* @__PURE__ */ new Map();
1352
- const refAlias = /* @__PURE__ */ new Map();
1353
- for (const entry of entries) {
1354
- for (const src of [entry.body, entry.query]) {
1355
- if (src?.ref && !src.text) {
1356
- const root = refRootIdentifier(src.ref.name);
1357
- const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
1358
- set.add(root);
1359
- importsByFile.set(src.ref.filePath, set);
1360
- }
1361
- }
1362
- }
1363
- if (importsByFile.size > 0) {
1364
- const emitted = /* @__PURE__ */ new Set();
1365
- for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1366
- const relPath = relImport(outDir, filePath);
1367
- const specifiers = [];
1368
- for (const root of [...roots].sort()) {
1369
- if (emitted.has(root)) {
1370
- const alias = `${root}_${emitted.size}`;
1371
- specifiers.push(`${root} as ${alias}`);
1372
- emitted.add(alias);
1373
- refAlias.set(`${filePath}\0${root}`, alias);
1374
- } else {
1375
- specifiers.push(root);
1376
- emitted.add(root);
1377
- refAlias.set(`${filePath}\0${root}`, root);
1378
- }
1379
- }
1380
- lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1381
- }
1382
- }
1383
- lines.push("");
1384
- const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1385
- if (globalSchemas.size > 0) {
1386
- lines.push("// Hoisted nested schemas (shared across endpoints).");
1387
- for (const [name, text] of globalSchemas) {
1388
- lines.push(`const ${name} = ${text};`);
1389
- }
1390
- lines.push("");
1391
- }
1392
- const mapEntries = [];
1393
- for (const entry of entries) {
1394
- lines.push(`// ${entry.routeName}`);
1395
- if (entry.warnings && entry.warnings.length > 0) {
1396
- for (const w of entry.warnings) {
1397
- lines.push(`// warning: ${w}`);
1398
- }
1399
- }
1400
- const rename = renamesByEntry.get(entry) ?? null;
1401
- if (entry.body) {
1402
- const schemaName = `${entry.baseName}BodySchema`;
1403
- const typeName = `${entry.baseName}Body`;
1404
- const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
1405
- lines.push(`export const ${schemaName} = ${text};`);
1406
- lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
1407
- mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
1408
- }
1409
- if (entry.query) {
1410
- const schemaName = `${entry.baseName}QuerySchema`;
1411
- const typeName = `${entry.baseName}Query`;
1412
- const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
1413
- lines.push(`export const ${schemaName} = ${text};`);
1414
- lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
1415
- }
1416
- lines.push("");
1417
- }
1418
- lines.push("/** Route name \u2192 body schema map. */");
1419
- lines.push("export const formSchemas = {");
1420
- lines.push(...mapEntries);
1421
- lines.push("} as const;");
1422
- lines.push("");
1423
- return lines.join("\n");
1171
+ function hasSource(src) {
1172
+ return !!(src.schema || src.zodText || src.zodRef);
1424
1173
  }
1425
1174
  function applyRenames(text, renames) {
1426
1175
  if (!renames || renames.size === 0) return text;
@@ -1486,38 +1235,190 @@ function planNestedSchemas(entries) {
1486
1235
  }
1487
1236
  return { globalSchemas, renamesByEntry };
1488
1237
  }
1489
- function renderSchema(src, outDir, refAlias) {
1490
- if (src.text) return src.text;
1491
- if (src.ref) {
1492
- const root = refRootIdentifier(src.ref.name);
1493
- const alias = refAlias.get(`${src.ref.filePath}\0${root}`) ?? root;
1494
- const member = src.ref.name.slice(root.length);
1495
- return `${alias}${member}`;
1238
+ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1239
+ const acceptsRawZod = adapter.acceptsRawZodSource === true;
1240
+ const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1241
+ const methodNameCounts = /* @__PURE__ */ new Map();
1242
+ const candidates = [];
1243
+ for (const route of sorted) {
1244
+ const cs = route.contract.contractSource;
1245
+ const body = {
1246
+ schema: cs.bodySchema ?? null,
1247
+ zodText: cs.bodyZodText ?? null,
1248
+ zodRef: cs.bodyZodRef ?? null
1249
+ };
1250
+ const query = {
1251
+ schema: cs.querySchema ?? null,
1252
+ zodText: cs.queryZodText ?? null,
1253
+ zodRef: cs.queryZodRef ?? null
1254
+ };
1255
+ if (!hasSource(body) && !hasSource(query)) continue;
1256
+ const { method, full } = deriveBaseName(route.name);
1257
+ methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1258
+ candidates.push({
1259
+ routeName: route.name,
1260
+ baseName: full,
1261
+ // resolved below
1262
+ body: hasSource(body) ? body : void 0,
1263
+ query: hasSource(query) ? query : void 0,
1264
+ nestedSchemas: cs.formNestedSchemas ?? null,
1265
+ warnings: cs.formWarnings ?? []
1266
+ });
1496
1267
  }
1497
- return "z.unknown()";
1498
- }
1499
-
1500
- // src/emit/emit-index.ts
1501
- var import_promises6 = require("fs/promises");
1502
- var import_node_path6 = require("path");
1503
- async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1504
- await (0, import_promises6.mkdir)(outDir, { recursive: true });
1505
- const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
1506
- if (hasContracts) {
1507
- exports2.push("export * from './api.js';");
1268
+ const entries = candidates.map((c) => {
1269
+ const { method, full } = deriveBaseName(c.routeName);
1270
+ const collision = (methodNameCounts.get(method) ?? 0) > 1;
1271
+ return { ...c, baseName: collision ? full : method };
1272
+ });
1273
+ if (entries.length === 0) return null;
1274
+ const importsByFile = /* @__PURE__ */ new Map();
1275
+ const refAlias = /* @__PURE__ */ new Map();
1276
+ for (const entry of entries) {
1277
+ for (const src of [entry.body, entry.query]) {
1278
+ if (src?.zodRef && !src.zodText && !src.schema) {
1279
+ const root = refRootIdentifier(src.zodRef.name);
1280
+ const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
1281
+ set.add(root);
1282
+ importsByFile.set(src.zodRef.filePath, set);
1283
+ }
1284
+ }
1508
1285
  }
1509
- if (hasForms) {
1510
- exports2.push("export * from './forms.js';");
1286
+ const importLines = [];
1287
+ if (importsByFile.size > 0) {
1288
+ const emitted = /* @__PURE__ */ new Set();
1289
+ for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1290
+ const relPath = relImport(outDir, filePath);
1291
+ const specifiers = [];
1292
+ for (const root of [...roots].sort()) {
1293
+ if (emitted.has(root)) {
1294
+ const alias = `${root}_${emitted.size}`;
1295
+ specifiers.push(`${root} as ${alias}`);
1296
+ emitted.add(alias);
1297
+ refAlias.set(`${filePath}\0${root}`, alias);
1298
+ } else {
1299
+ specifiers.push(root);
1300
+ emitted.add(root);
1301
+ refAlias.set(`${filePath}\0${root}`, root);
1302
+ }
1303
+ }
1304
+ importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1305
+ }
1511
1306
  }
1512
- const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
1513
- "\n"
1307
+ const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1308
+ const irNamed = /* @__PURE__ */ new Map();
1309
+ const decls = [];
1310
+ const mapEntries = [];
1311
+ let used = false;
1312
+ const renderSource = (src, rename) => {
1313
+ if (src.schema) {
1314
+ const r = adapter.renderModule(src.schema);
1315
+ for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1316
+ return { text: r.schemaText };
1317
+ }
1318
+ if (src.zodText) {
1319
+ if (!acceptsRawZod) {
1320
+ return {
1321
+ text: "",
1322
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1323
+ };
1324
+ }
1325
+ return { text: applyRenames(src.zodText, rename) };
1326
+ }
1327
+ if (src.zodRef) {
1328
+ if (!acceptsRawZod) {
1329
+ return {
1330
+ text: "",
1331
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1332
+ };
1333
+ }
1334
+ const root = refRootIdentifier(src.zodRef.name);
1335
+ const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
1336
+ const member = src.zodRef.name.slice(root.length);
1337
+ return { text: `${alias}${member}` };
1338
+ }
1339
+ return null;
1340
+ };
1341
+ for (const entry of entries) {
1342
+ const block = [];
1343
+ const rename = renamesByEntry.get(entry) ?? null;
1344
+ let bodyConst;
1345
+ if (entry.warnings && entry.warnings.length > 0) {
1346
+ for (const w of entry.warnings) block.push(`// warning: ${w}`);
1347
+ }
1348
+ if (entry.body) {
1349
+ const rendered = renderSource(entry.body, rename);
1350
+ if (rendered?.warn) {
1351
+ block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
1352
+ } else if (rendered) {
1353
+ used = true;
1354
+ bodyConst = `${entry.baseName}BodySchema`;
1355
+ block.push(`export const ${bodyConst} = ${rendered.text};`);
1356
+ block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
1357
+ }
1358
+ }
1359
+ if (entry.query) {
1360
+ const rendered = renderSource(entry.query, rename);
1361
+ if (rendered?.warn) {
1362
+ block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
1363
+ } else if (rendered) {
1364
+ used = true;
1365
+ const queryConst = `${entry.baseName}QuerySchema`;
1366
+ block.push(`export const ${queryConst} = ${rendered.text};`);
1367
+ block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
1368
+ }
1369
+ }
1370
+ if (block.length === 0) continue;
1371
+ decls.push(`// ${entry.routeName}`, ...block, "");
1372
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
1373
+ }
1374
+ if (!used) return null;
1375
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1376
+ if (acceptsRawZod) {
1377
+ const zodImport = config?.zodImport ?? "zod";
1378
+ lines.push(`import { z } from '${zodImport}';`);
1379
+ } else {
1380
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1381
+ }
1382
+ lines.push(...importLines);
1383
+ lines.push("");
1384
+ const allNested = /* @__PURE__ */ new Map();
1385
+ for (const [n, t] of globalSchemas) allNested.set(n, t);
1386
+ for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1387
+ if (allNested.size > 0) {
1388
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
1389
+ for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
1390
+ lines.push("");
1391
+ }
1392
+ lines.push(...decls);
1393
+ lines.push("/** Route name \u2192 body schema map. */");
1394
+ lines.push("export const formSchemas = {");
1395
+ lines.push(...mapEntries);
1396
+ lines.push("} as const;");
1397
+ lines.push("");
1398
+ return lines.join("\n");
1399
+ }
1400
+
1401
+ // src/emit/emit-index.ts
1402
+ var import_promises6 = require("fs/promises");
1403
+ var import_node_path7 = require("path");
1404
+ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1405
+ await (0, import_promises6.mkdir)(outDir, { recursive: true });
1406
+ const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
1407
+ if (hasContracts) {
1408
+ exports2.push("export * from './api.js';");
1409
+ }
1410
+ if (hasForms) {
1411
+ exports2.push("export * from './forms.js';");
1412
+ }
1413
+ const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
1414
+ "\n"
1514
1415
  );
1515
- await (0, import_promises6.writeFile)((0, import_node_path6.join)(outDir, "index.d.ts"), content, "utf8");
1416
+ await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
1516
1417
  }
1517
1418
 
1518
1419
  // src/emit/emit-pages.ts
1519
1420
  var import_promises7 = require("fs/promises");
1520
- var import_node_path7 = require("path");
1421
+ var import_node_path8 = require("path");
1521
1422
  async function emitPages(pages, outDir, _options = {}) {
1522
1423
  await (0, import_promises7.mkdir)(outDir, { recursive: true });
1523
1424
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -1538,7 +1439,7 @@ ${augBody}
1538
1439
  }
1539
1440
  ${sharedPropsBlock}}
1540
1441
  `;
1541
- await (0, import_promises7.writeFile)((0, import_node_path7.join)(outDir, "pages.d.ts"), content, "utf8");
1442
+ await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, "pages.d.ts"), content, "utf8");
1542
1443
  }
1543
1444
  function buildSharedPropsBlock(sharedProps) {
1544
1445
  if (!sharedProps) return "";
@@ -1557,7 +1458,7 @@ ${propsBody}
1557
1458
  `;
1558
1459
  }
1559
1460
  function buildAugmentationType(page, outDir) {
1560
- let importPath = (0, import_node_path7.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1461
+ let importPath = (0, import_node_path8.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1561
1462
  if (!importPath.startsWith(".")) {
1562
1463
  importPath = `./${importPath}`;
1563
1464
  }
@@ -1569,11 +1470,11 @@ function needsQuotes(name) {
1569
1470
 
1570
1471
  // src/emit/emit-routes.ts
1571
1472
  var import_promises8 = require("fs/promises");
1572
- var import_node_path8 = require("path");
1473
+ var import_node_path9 = require("path");
1573
1474
  async function emitRoutes(routes, outDir) {
1574
1475
  await (0, import_promises8.mkdir)(outDir, { recursive: true });
1575
1476
  const content = buildRoutesFile(routes);
1576
- await (0, import_promises8.writeFile)((0, import_node_path8.join)(outDir, "routes.ts"), content, "utf8");
1477
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, "routes.ts"), content, "utf8");
1577
1478
  }
1578
1479
  function buildRoutesFile(routes) {
1579
1480
  if (routes.length === 0) {
@@ -1701,30 +1602,7 @@ async function generate(config, inputRoutes = []) {
1701
1602
  propsExport: pagesConfig.propsExport,
1702
1603
  componentNameStrategy: pagesConfig.componentNameStrategy
1703
1604
  });
1704
- let sharedProps = null;
1705
- if (config.app?.moduleEntry) {
1706
- try {
1707
- const tsconfigPath = config.app.tsconfig ?? (0, import_node_path9.join)(config.codegen.cwd, "tsconfig.json");
1708
- let project;
1709
- try {
1710
- project = new import_ts_morph3.Project({
1711
- tsConfigFilePath: tsconfigPath,
1712
- skipAddingFilesFromTsConfig: true,
1713
- skipLoadingLibFiles: true,
1714
- skipFileDependencyResolution: true
1715
- });
1716
- } catch {
1717
- project = new import_ts_morph3.Project({
1718
- skipAddingFilesFromTsConfig: true,
1719
- skipLoadingLibFiles: true,
1720
- skipFileDependencyResolution: true,
1721
- compilerOptions: { allowJs: true, strict: false }
1722
- });
1723
- }
1724
- sharedProps = discoverSharedProps(project, config.app.moduleEntry);
1725
- } catch {
1726
- }
1727
- }
1605
+ const sharedProps = discoverSharedPropsFromConfig(config);
1728
1606
  await emitPages(pages, config.codegen.outDir, {
1729
1607
  propsExport: pagesConfig.propsExport,
1730
1608
  sharedProps
@@ -1748,8 +1626,8 @@ async function generate(config, inputRoutes = []) {
1748
1626
  if (extensions.length > 0) {
1749
1627
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1750
1628
  for (const file of extraFiles) {
1751
- const dest = (0, import_node_path9.join)(config.codegen.outDir, file.path);
1752
- await (0, import_promises9.mkdir)((0, import_node_path9.dirname)(dest), { recursive: true });
1629
+ const dest = (0, import_node_path10.join)(config.codegen.outDir, file.path);
1630
+ await (0, import_promises9.mkdir)((0, import_node_path10.dirname)(dest), { recursive: true });
1753
1631
  await (0, import_promises9.writeFile)(dest, file.contents, "utf8");
1754
1632
  }
1755
1633
  }
@@ -1757,35 +1635,31 @@ async function generate(config, inputRoutes = []) {
1757
1635
 
1758
1636
  // src/watch/watcher.ts
1759
1637
  var import_promises12 = require("fs/promises");
1760
- var import_node_path13 = require("path");
1638
+ var import_node_path14 = require("path");
1761
1639
  var import_chokidar = __toESM(require("chokidar"), 1);
1762
1640
 
1763
1641
  // src/discovery/contracts-fast.ts
1764
- var import_node_path11 = require("path");
1642
+ var import_node_path12 = require("path");
1765
1643
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
1766
1644
  var import_ts_morph9 = require("ts-morph");
1767
1645
 
1646
+ // src/discovery/dto-type-resolver.ts
1647
+ var import_ts_morph7 = require("ts-morph");
1648
+
1768
1649
  // src/discovery/dto-to-ir.ts
1769
- var import_ts_morph5 = require("ts-morph");
1650
+ var import_ts_morph4 = require("ts-morph");
1770
1651
 
1771
1652
  // src/discovery/type-ref-resolution.ts
1772
1653
  var import_node_fs = require("fs");
1773
- var import_node_path10 = require("path");
1774
- var import_ts_morph4 = require("ts-morph");
1775
- var _ctx = { projectRoot: "", tsconfigPaths: null };
1776
- function setDiscoveryContext(ctx) {
1777
- const prev = _ctx;
1778
- _ctx = ctx;
1779
- return prev;
1780
- }
1781
- function restoreDiscoveryContext(ctx) {
1782
- _ctx = ctx;
1783
- }
1784
- function _projectRoot() {
1785
- return _ctx.projectRoot;
1654
+ var import_node_path11 = require("path");
1655
+ var import_ts_morph3 = require("ts-morph");
1656
+ var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1657
+ var _ctxByProject = /* @__PURE__ */ new WeakMap();
1658
+ function setDiscoveryContext(project, ctx) {
1659
+ _ctxByProject.set(project, ctx);
1786
1660
  }
1787
- function _tsconfigPaths() {
1788
- return _ctx.tsconfigPaths;
1661
+ function _ctxFor(project) {
1662
+ return _ctxByProject.get(project) ?? _EMPTY_CTX;
1789
1663
  }
1790
1664
  var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
1791
1665
  function dbg(...args) {
@@ -1827,18 +1701,19 @@ function findTypeInFile(name, file) {
1827
1701
  }
1828
1702
  return null;
1829
1703
  }
1830
- function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1704
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1831
1705
  if (moduleSpecifier.startsWith(".")) {
1832
- const dir = (0, import_node_path10.dirname)(sourceFile.getFilePath());
1706
+ const dir = (0, import_node_path11.dirname)(sourceFile.getFilePath());
1833
1707
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
1834
1708
  return [
1835
- (0, import_node_path10.resolve)(dir, `${noExt}.ts`),
1836
- (0, import_node_path10.resolve)(dir, `${moduleSpecifier}.ts`),
1837
- (0, import_node_path10.resolve)(dir, moduleSpecifier, "index.ts")
1709
+ (0, import_node_path11.resolve)(dir, `${noExt}.ts`),
1710
+ (0, import_node_path11.resolve)(dir, `${moduleSpecifier}.ts`),
1711
+ (0, import_node_path11.resolve)(dir, moduleSpecifier, "index.ts")
1838
1712
  ];
1839
1713
  }
1840
- const baseUrl = _projectRoot();
1841
- const tsconfigPaths = _tsconfigPaths();
1714
+ const ctx = _ctxFor(project);
1715
+ const baseUrl = ctx.projectRoot;
1716
+ const tsconfigPaths = ctx.tsconfigPaths;
1842
1717
  dbg(
1843
1718
  "resolveModuleSpecifier",
1844
1719
  moduleSpecifier,
@@ -1854,8 +1729,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1854
1729
  const rest = moduleSpecifier.slice(prefix.length);
1855
1730
  const candidates = [];
1856
1731
  for (const mapping of mappings) {
1857
- const resolved = (0, import_node_path10.resolve)(baseUrl, mapping.replace("*", rest));
1858
- candidates.push(`${resolved}.ts`, (0, import_node_path10.resolve)(resolved, "index.ts"));
1732
+ const resolved = (0, import_node_path11.resolve)(baseUrl, mapping.replace("*", rest));
1733
+ candidates.push(`${resolved}.ts`, (0, import_node_path11.resolve)(resolved, "index.ts"));
1859
1734
  }
1860
1735
  dbg(" resolved candidates:", candidates);
1861
1736
  return candidates;
@@ -1955,13 +1830,13 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1955
1830
  name = nodeOrName;
1956
1831
  } else {
1957
1832
  const typeNode = nodeOrName;
1958
- if (opts.unwrapContainers && import_ts_morph4.Node.isArrayTypeNode(typeNode)) {
1833
+ if (opts.unwrapContainers && import_ts_morph3.Node.isArrayTypeNode(typeNode)) {
1959
1834
  const inner = resolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project, opts);
1960
1835
  return inner ? { ...inner, isArray: true } : null;
1961
1836
  }
1962
- if (!import_ts_morph4.Node.isTypeReference(typeNode)) return null;
1837
+ if (!import_ts_morph3.Node.isTypeReference(typeNode)) return null;
1963
1838
  const typeName = typeNode.getTypeName();
1964
- const refName = import_ts_morph4.Node.isIdentifier(typeName) ? typeName.getText() : null;
1839
+ const refName = import_ts_morph3.Node.isIdentifier(typeName) ? typeName.getText() : null;
1965
1840
  if (!refName) return null;
1966
1841
  if (opts.unwrapContainers && refName === "Promise") {
1967
1842
  const first = typeNode.getTypeArguments()[0];
@@ -1984,7 +1859,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1984
1859
  if (!namedImport) continue;
1985
1860
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
1986
1861
  if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
1987
- const tsconfigPaths = _tsconfigPaths();
1862
+ const tsconfigPaths = _ctxFor(project).tsconfigPaths;
1988
1863
  const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
1989
1864
  const prefix = p.replace("*", "");
1990
1865
  return moduleSpecifier.startsWith(prefix);
@@ -2256,22 +2131,22 @@ function firstArgText(decorator) {
2256
2131
  }
2257
2132
  function numericArg(decorator) {
2258
2133
  const arg = firstArg(decorator);
2259
- if (arg && import_ts_morph5.Node.isNumericLiteral(arg)) return arg.getText();
2134
+ if (arg && import_ts_morph4.Node.isNumericLiteral(arg)) return arg.getText();
2260
2135
  return null;
2261
2136
  }
2262
2137
  function numericArgs(decorator) {
2263
2138
  const args = decorator?.getArguments() ?? [];
2264
- const num = (n) => n && import_ts_morph5.Node.isNumericLiteral(n) ? n.getText() : null;
2139
+ const num = (n) => n && import_ts_morph4.Node.isNumericLiteral(n) ? n.getText() : null;
2265
2140
  return [num(args[0]), num(args[1])];
2266
2141
  }
2267
2142
  function messageRaw(decorator) {
2268
2143
  const args = decorator?.getArguments() ?? [];
2269
2144
  for (const arg of args) {
2270
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2145
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
2271
2146
  for (const prop of arg.getProperties()) {
2272
- if (import_ts_morph5.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2147
+ if (import_ts_morph4.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2273
2148
  const init = prop.getInitializer();
2274
- if (init && import_ts_morph5.Node.isStringLiteral(init)) return init.getText();
2149
+ if (init && import_ts_morph4.Node.isStringLiteral(init)) return init.getText();
2275
2150
  }
2276
2151
  }
2277
2152
  }
@@ -2281,9 +2156,9 @@ function messageRaw(decorator) {
2281
2156
  function resolveTypeFactoryName(decorator) {
2282
2157
  const arg = firstArg(decorator);
2283
2158
  if (!arg) return null;
2284
- if (import_ts_morph5.Node.isArrowFunction(arg)) {
2159
+ if (import_ts_morph4.Node.isArrowFunction(arg)) {
2285
2160
  const body = arg.getBody();
2286
- if (import_ts_morph5.Node.isIdentifier(body)) return body.getText();
2161
+ if (import_ts_morph4.Node.isIdentifier(body)) return body.getText();
2287
2162
  }
2288
2163
  return null;
2289
2164
  }
@@ -2294,7 +2169,7 @@ function singularClassName(typeText) {
2294
2169
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2295
2170
  const arg = firstArg(decorator);
2296
2171
  if (!arg) return null;
2297
- if (import_ts_morph5.Node.isIdentifier(arg)) {
2172
+ if (import_ts_morph4.Node.isIdentifier(arg)) {
2298
2173
  const name = arg.getText();
2299
2174
  const resolved = findType(name, classFile, ctx.project);
2300
2175
  if (resolved && resolved.kind === "enum") {
@@ -2308,12 +2183,12 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
2308
2183
  }
2309
2184
  return { kind: "unknown", note: `@IsEnum(${name}): enum not resolvable to literals` };
2310
2185
  }
2311
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2186
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
2312
2187
  const values = [];
2313
2188
  for (const p of arg.getProperties()) {
2314
- if (!import_ts_morph5.Node.isPropertyAssignment(p)) continue;
2189
+ if (!import_ts_morph4.Node.isPropertyAssignment(p)) continue;
2315
2190
  const init = p.getInitializer();
2316
- if (init && import_ts_morph5.Node.isStringLiteral(init)) values.push(init.getText());
2191
+ if (init && import_ts_morph4.Node.isStringLiteral(init)) values.push(init.getText());
2317
2192
  }
2318
2193
  if (values.length > 0) return { kind: "enum", literals: values };
2319
2194
  }
@@ -2321,9 +2196,9 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
2321
2196
  }
2322
2197
  function inSchemaFromDecorator(decorator) {
2323
2198
  const arg = firstArg(decorator);
2324
- if (arg && import_ts_morph5.Node.isArrayLiteralExpression(arg)) {
2199
+ if (arg && import_ts_morph4.Node.isArrayLiteralExpression(arg)) {
2325
2200
  const elements = arg.getElements();
2326
- const allStrings = elements.every((e) => import_ts_morph5.Node.isStringLiteral(e));
2201
+ const allStrings = elements.every((e) => import_ts_morph4.Node.isStringLiteral(e));
2327
2202
  if (allStrings && elements.length > 0) {
2328
2203
  return { kind: "enum", literals: elements.map((e) => e.getText()) };
2329
2204
  }
@@ -2337,458 +2212,130 @@ function inSchemaFromDecorator(decorator) {
2337
2212
  return null;
2338
2213
  }
2339
2214
 
2340
- // src/discovery/dto-to-zod.ts
2215
+ // src/discovery/filter-for.ts
2341
2216
  var import_ts_morph6 = require("ts-morph");
2342
- var KNOWN_DECORATORS2 = /* @__PURE__ */ new Set([
2343
- "IsString",
2344
- "IsNumber",
2345
- "IsInt",
2346
- "IsBoolean",
2347
- "IsDate",
2348
- "IsEmail",
2349
- "IsUrl",
2350
- "IsUUID",
2351
- "MinLength",
2352
- "MaxLength",
2353
- "Length",
2354
- "Min",
2355
- "Max",
2356
- "IsPositive",
2357
- "IsNegative",
2358
- "Matches",
2359
- "IsEnum",
2360
- "IsIn",
2361
- "IsOptional",
2362
- "IsNotEmpty",
2363
- "IsArray",
2364
- "ValidateNested",
2365
- "Type",
2366
- "IsObject",
2367
- "Allow",
2368
- "IsDefined"
2369
- ]);
2370
- function extractZodFromDto(classDecl, sourceFile, project) {
2371
- const ctx = {
2372
- sourceFile,
2373
- project,
2374
- namedNestedSchemas: /* @__PURE__ */ new Map(),
2375
- warnings: [],
2376
- warnedDecorators: /* @__PURE__ */ new Set(),
2377
- emittedClasses: /* @__PURE__ */ new Map(),
2378
- visiting: /* @__PURE__ */ new Set(),
2379
- recursiveSchemas: /* @__PURE__ */ new Set(),
2380
- depth: 0
2381
- };
2382
- const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
2383
- for (const schemaName of ctx.recursiveSchemas) {
2384
- ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
2385
- }
2386
- return {
2387
- schemaText,
2388
- namedNestedSchemas: ctx.namedNestedSchemas,
2389
- warnings: ctx.warnings
2390
- };
2217
+
2218
+ // src/discovery/filter-field-types.ts
2219
+ var import_ts_morph5 = require("ts-morph");
2220
+
2221
+ // src/discovery/enum-resolution.ts
2222
+ function resolveEnumValues(name, sourceFile, project) {
2223
+ const resolved = findType(name, sourceFile, project);
2224
+ if (!resolved || resolved.kind !== "enum") return null;
2225
+ let numeric = true;
2226
+ const values = resolved.members.map((m) => {
2227
+ const parsed = JSON.parse(m);
2228
+ if (typeof parsed === "string") numeric = false;
2229
+ return String(parsed);
2230
+ });
2231
+ if (values.length === 0) return null;
2232
+ return { values, numeric };
2391
2233
  }
2392
- function buildObjectSchema(classDecl, classFile, ctx) {
2393
- const props = classDecl.getProperties();
2394
- if (props.length === 0) {
2395
- return "z.object({}).passthrough()";
2396
- }
2397
- const fields = [];
2398
- for (const prop of props) {
2399
- const name = prop.getName();
2400
- const expr = buildPropertySchema(prop, classFile, ctx);
2401
- fields.push(`${toObjectKey3(name)}: ${expr}`);
2402
- }
2403
- return `z.object({ ${fields.join(", ")} })`;
2234
+
2235
+ // src/discovery/filter-field-types.ts
2236
+ var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2237
+ var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2238
+ var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2239
+ var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2240
+ var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2241
+ function classifyTypeKeyword(raw) {
2242
+ const t = raw.toLowerCase();
2243
+ if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2244
+ if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2245
+ if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2246
+ if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2247
+ if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2248
+ return null;
2404
2249
  }
2405
- function toObjectKey3(name) {
2406
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2250
+ function markNullable(r, nullable) {
2251
+ return nullable ? { ...r, nullable: true } : r;
2407
2252
  }
2408
- function buildPropertySchema(prop, classFile, ctx) {
2409
- const decorators = /* @__PURE__ */ new Map();
2410
- for (const d of prop.getDecorators()) decorators.set(d.getName(), d);
2411
- const has = (n) => decorators.has(n);
2412
- const dec = (n) => decorators.get(n);
2413
- const typeNode = prop.getTypeNode();
2414
- const typeText = typeNode?.getText() ?? "unknown";
2415
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
2416
- const comments = [];
2417
- const typeRefName = resolveTypeFactoryName2(dec("Type"));
2418
- if (has("ValidateNested") || typeRefName) {
2419
- const childName = typeRefName ?? singularClassName2(typeText);
2420
- if (childName) {
2421
- const childExpr = buildNestedReference2(childName, classFile, ctx);
2422
- const wrapArray = has("IsArray") || isArrayType;
2423
- let expr2 = wrapArray ? `z.array(${childExpr})` : childExpr;
2424
- expr2 = applyPresence2(expr2, decorators);
2425
- return expr2;
2426
- }
2427
- }
2428
- let base = baseFromType2(typeText, isArrayType, ctx, classFile);
2429
- const refinements = [];
2430
- if (has("IsString")) base = "z.string()";
2431
- if (has("IsBoolean")) base = "z.boolean()";
2432
- if (has("IsDate")) base = "z.coerce.date()";
2433
- if (has("IsNumber")) base = "z.number()";
2434
- if (has("IsInt")) base = "z.number().int()";
2435
- if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
2436
- if (has("Allow")) base = "z.unknown()";
2437
- if (has("IsEmail")) {
2438
- base = ensureStringBase(base);
2439
- refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
2440
- }
2441
- if (has("IsUrl")) {
2442
- base = ensureStringBase(base);
2443
- refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
2444
- }
2445
- if (has("IsUUID")) {
2446
- base = ensureStringBase(base);
2447
- refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
2448
- }
2449
- if (has("Matches")) {
2450
- const re = firstArgText2(dec("Matches"));
2451
- if (re) {
2452
- base = ensureStringBase(base);
2453
- refinements.push(`.regex(${re})`);
2454
- }
2455
- }
2456
- if (has("MinLength")) {
2457
- const n = numericArg2(dec("MinLength"));
2458
- if (n !== null) refinements.push(`.min(${n})`);
2459
- }
2460
- if (has("MaxLength")) {
2461
- const n = numericArg2(dec("MaxLength"));
2462
- if (n !== null) refinements.push(`.max(${n})`);
2463
- }
2464
- if (has("Length")) {
2465
- const [min, max] = numericArgs2(dec("Length"));
2466
- if (min !== null) refinements.push(`.min(${min})`);
2467
- if (max !== null) refinements.push(`.max(${max})`);
2468
- }
2469
- if (has("Min")) {
2470
- const n = numericArg2(dec("Min"));
2471
- if (n !== null) refinements.push(`.min(${n})`);
2472
- }
2473
- if (has("Max")) {
2474
- const n = numericArg2(dec("Max"));
2475
- if (n !== null) refinements.push(`.max(${n})`);
2476
- }
2477
- if (has("IsPositive")) refinements.push(".positive()");
2478
- if (has("IsNegative")) refinements.push(".negative()");
2479
- if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
2480
- if (has("IsEnum")) {
2481
- const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
2482
- if (enumExpr) base = enumExpr;
2483
- }
2484
- if (has("IsIn")) {
2485
- const inExpr = inSchemaFromDecorator2(dec("IsIn"));
2486
- if (inExpr) base = inExpr;
2487
- }
2488
- for (const name of decorators.keys()) {
2489
- if (!KNOWN_DECORATORS2.has(name)) {
2490
- comments.push(`/* @${name}: not translatable to zod (server-only) */`);
2491
- if (!ctx.warnedDecorators.has(name)) {
2492
- ctx.warnedDecorators.add(name);
2493
- const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
2494
- ctx.warnings.push(msg);
2495
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2253
+ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2254
+ if (import_ts_morph5.Node.isUnionTypeNode(typeNode)) {
2255
+ let nullable = false;
2256
+ const stringLits = [];
2257
+ const numberLits = [];
2258
+ const others = [];
2259
+ for (const member of typeNode.getTypeNodes()) {
2260
+ const kind = member.getKind();
2261
+ if (kind === import_ts_morph5.SyntaxKind.NullKeyword || kind === import_ts_morph5.SyntaxKind.UndefinedKeyword) {
2262
+ nullable = true;
2263
+ continue;
2264
+ }
2265
+ if (import_ts_morph5.Node.isLiteralTypeNode(member)) {
2266
+ const lit = member.getLiteral();
2267
+ if (import_ts_morph5.Node.isStringLiteral(lit)) {
2268
+ stringLits.push(lit.getLiteralValue());
2269
+ continue;
2270
+ }
2271
+ if (import_ts_morph5.Node.isNumericLiteral(lit)) {
2272
+ numberLits.push(lit.getText());
2273
+ continue;
2274
+ }
2275
+ if (lit.getKind() === import_ts_morph5.SyntaxKind.NullKeyword) {
2276
+ nullable = true;
2277
+ continue;
2278
+ }
2496
2279
  }
2280
+ others.push(member);
2281
+ }
2282
+ if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2283
+ return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2284
+ }
2285
+ if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2286
+ return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2497
2287
  }
2288
+ if (others.length === 1) {
2289
+ const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2290
+ return markNullable(inner, nullable || inner.nullable === true);
2291
+ }
2292
+ return markNullable({ kind: "unknown" }, nullable);
2498
2293
  }
2499
- let expr = base + refinements.join("");
2500
- if (isArrayType && !expr.startsWith("z.array(")) {
2501
- expr = `z.array(${expr})`;
2294
+ switch (typeNode.getKind()) {
2295
+ case import_ts_morph5.SyntaxKind.StringKeyword:
2296
+ return { kind: "string" };
2297
+ case import_ts_morph5.SyntaxKind.NumberKeyword:
2298
+ return { kind: "number" };
2299
+ case import_ts_morph5.SyntaxKind.BooleanKeyword:
2300
+ return { kind: "boolean" };
2301
+ case import_ts_morph5.SyntaxKind.AnyKeyword:
2302
+ case import_ts_morph5.SyntaxKind.UnknownKeyword:
2303
+ return { kind: "unknown" };
2304
+ default:
2305
+ break;
2502
2306
  }
2503
- expr = applyPresence2(expr, decorators);
2504
- if (comments.length > 0) {
2505
- expr = `${expr} ${comments.join(" ")}`;
2307
+ if (import_ts_morph5.Node.isTypeReference(typeNode)) {
2308
+ const refName = typeNode.getTypeName().getText();
2309
+ if (refName === "Date") return { kind: "date" };
2310
+ if (refName === "Record" || refName === "Object") return { kind: "json" };
2311
+ const typeRef = opts?.resolveRef?.(refName) ?? null;
2312
+ const en = resolveEnumValues(refName, sourceFile, project);
2313
+ if (en) {
2314
+ const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
2315
+ return typeRef ? { ...base, typeRef } : base;
2316
+ }
2317
+ if (typeRef) return { kind: "unknown", typeRef };
2318
+ return { kind: "unknown" };
2506
2319
  }
2507
- return expr;
2508
- }
2509
- function applyPresence2(expr, decorators) {
2510
- if (decorators.has("IsDefined")) return expr;
2511
- if (decorators.has("IsOptional")) return `${expr}.optional()`;
2512
- return expr;
2513
- }
2514
- function baseFromType2(typeText, isArrayType, ctx, classFile) {
2515
- const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
2516
- switch (inner) {
2517
- case "string":
2518
- return "z.string()";
2519
- case "number":
2520
- return "z.number()";
2521
- case "boolean":
2522
- return "z.boolean()";
2523
- case "Date":
2524
- return "z.coerce.date()";
2525
- case "File":
2526
- case "Express.Multer.File":
2527
- return "z.instanceof(File)";
2528
- default:
2529
- return "z.unknown()";
2530
- }
2531
- }
2532
- function ensureStringBase(base) {
2533
- return isStringBase(base) ? base : "z.string()";
2534
- }
2535
- function isStringBase(base) {
2536
- return base.startsWith("z.string(");
2537
- }
2538
- function buildNestedReference2(className, fromFile, ctx) {
2539
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2540
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
2541
- ctx.emittedClasses.set(className, reserved);
2542
- ctx.recursiveSchemas.add(reserved);
2543
- if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2544
- ctx.warnedDecorators.add(`recursive:${reserved}`);
2545
- const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
2546
- ctx.warnings.push(msg);
2547
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2548
- }
2549
- return `z.lazy(() => ${reserved})`;
2550
- }
2551
- const existing = ctx.emittedClasses.get(className);
2552
- if (existing) return existing;
2553
- const schemaName = aliasFor2(className, ctx);
2554
- const resolved = findType(className, fromFile, ctx.project);
2555
- if (!resolved || resolved.kind !== "class") {
2556
- return "z.object({}).passthrough()";
2557
- }
2558
- ctx.emittedClasses.set(className, schemaName);
2559
- ctx.visiting.add(className);
2560
- ctx.depth += 1;
2561
- const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
2562
- ctx.depth -= 1;
2563
- ctx.visiting.delete(className);
2564
- ctx.namedNestedSchemas.set(schemaName, childText);
2565
- return schemaName;
2566
- }
2567
- function aliasFor2(className, ctx) {
2568
- const baseName = `${className}Schema`;
2569
- let candidate = baseName;
2570
- let i = 1;
2571
- const used = new Set(ctx.namedNestedSchemas.keys());
2572
- for (const v of ctx.emittedClasses.values()) used.add(v);
2573
- while (used.has(candidate)) {
2574
- candidate = `${baseName}_${i}`;
2575
- i += 1;
2576
- }
2577
- return candidate;
2578
- }
2579
- function firstArg2(decorator) {
2580
- return decorator?.getArguments()[0];
2581
- }
2582
- function firstArgText2(decorator) {
2583
- const arg = firstArg2(decorator);
2584
- return arg ? arg.getText() : null;
2585
- }
2586
- function numericArg2(decorator) {
2587
- const arg = firstArg2(decorator);
2588
- if (arg && import_ts_morph6.Node.isNumericLiteral(arg)) return arg.getText();
2589
- return null;
2590
- }
2591
- function numericArgs2(decorator) {
2592
- const args = decorator?.getArguments() ?? [];
2593
- const num = (n) => n && import_ts_morph6.Node.isNumericLiteral(n) ? n.getText() : null;
2594
- return [num(args[0]), num(args[1])];
2595
- }
2596
- function messageArg(decorator) {
2597
- const args = decorator?.getArguments() ?? [];
2598
- for (const arg of args) {
2599
- if (import_ts_morph6.Node.isObjectLiteralExpression(arg)) {
2600
- for (const prop of arg.getProperties()) {
2601
- if (import_ts_morph6.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2602
- const init = prop.getInitializer();
2603
- if (init && import_ts_morph6.Node.isStringLiteral(init)) {
2604
- return `{ message: ${init.getText()} }`;
2605
- }
2606
- }
2607
- }
2608
- }
2609
- }
2610
- return "";
2611
- }
2612
- function resolveTypeFactoryName2(decorator) {
2613
- const arg = firstArg2(decorator);
2614
- if (!arg) return null;
2615
- if (import_ts_morph6.Node.isArrowFunction(arg)) {
2616
- const body = arg.getBody();
2617
- if (import_ts_morph6.Node.isIdentifier(body)) return body.getText();
2618
- }
2619
- return null;
2620
- }
2621
- function singularClassName2(typeText) {
2622
- const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2623
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2624
- }
2625
- function enumSchemaFromDecorator2(decorator, classFile, ctx) {
2626
- const arg = firstArg2(decorator);
2627
- if (!arg) return null;
2628
- if (import_ts_morph6.Node.isIdentifier(arg)) {
2629
- const name = arg.getText();
2630
- const resolved = findType(name, classFile, ctx.project);
2631
- if (resolved && resolved.kind === "enum") {
2632
- return `z.enum([${resolved.members.join(", ")}])`;
2633
- }
2634
- 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().`;
2635
- if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
2636
- ctx.warnedDecorators.add(`IsEnum:${name}`);
2637
- ctx.warnings.push(msg);
2638
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2639
- }
2640
- return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
2641
- }
2642
- if (import_ts_morph6.Node.isObjectLiteralExpression(arg)) {
2643
- const values = [];
2644
- for (const p of arg.getProperties()) {
2645
- if (!import_ts_morph6.Node.isPropertyAssignment(p)) continue;
2646
- const init = p.getInitializer();
2647
- if (init && import_ts_morph6.Node.isStringLiteral(init)) values.push(init.getText());
2648
- }
2649
- if (values.length > 0) return `z.enum([${values.join(", ")}])`;
2650
- }
2651
- return null;
2652
- }
2653
- function inSchemaFromDecorator2(decorator) {
2654
- const arg = firstArg2(decorator);
2655
- if (arg && import_ts_morph6.Node.isArrayLiteralExpression(arg)) {
2656
- const elements = arg.getElements();
2657
- const allStrings = elements.every((e) => import_ts_morph6.Node.isStringLiteral(e));
2658
- if (allStrings && elements.length > 0) {
2659
- return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
2660
- }
2661
- if (elements.length > 0) {
2662
- return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
2663
- }
2664
- }
2665
- return null;
2666
- }
2667
-
2668
- // src/discovery/filter-for.ts
2669
- var import_ts_morph8 = require("ts-morph");
2670
-
2671
- // src/discovery/filter-field-types.ts
2672
- var import_ts_morph7 = require("ts-morph");
2673
-
2674
- // src/discovery/enum-resolution.ts
2675
- function resolveEnumValues(name, sourceFile, project) {
2676
- const resolved = findType(name, sourceFile, project);
2677
- if (!resolved || resolved.kind !== "enum") return null;
2678
- let numeric = true;
2679
- const values = resolved.members.map((m) => {
2680
- const parsed = JSON.parse(m);
2681
- if (typeof parsed === "string") numeric = false;
2682
- return String(parsed);
2683
- });
2684
- if (values.length === 0) return null;
2685
- return { values, numeric };
2686
- }
2687
-
2688
- // src/discovery/filter-field-types.ts
2689
- var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2690
- var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2691
- var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2692
- var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2693
- var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2694
- function classifyTypeKeyword(raw) {
2695
- const t = raw.toLowerCase();
2696
- if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2697
- if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2698
- if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2699
- if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2700
- if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2701
- return null;
2702
- }
2703
- function markNullable(r, nullable) {
2704
- return nullable ? { ...r, nullable: true } : r;
2705
- }
2706
- function classifyTypeNode(typeNode, sourceFile, project, opts) {
2707
- if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
2708
- let nullable = false;
2709
- const stringLits = [];
2710
- const numberLits = [];
2711
- const others = [];
2712
- for (const member of typeNode.getTypeNodes()) {
2713
- const kind = member.getKind();
2714
- if (kind === import_ts_morph7.SyntaxKind.NullKeyword || kind === import_ts_morph7.SyntaxKind.UndefinedKeyword) {
2715
- nullable = true;
2716
- continue;
2717
- }
2718
- if (import_ts_morph7.Node.isLiteralTypeNode(member)) {
2719
- const lit = member.getLiteral();
2720
- if (import_ts_morph7.Node.isStringLiteral(lit)) {
2721
- stringLits.push(lit.getLiteralValue());
2722
- continue;
2723
- }
2724
- if (import_ts_morph7.Node.isNumericLiteral(lit)) {
2725
- numberLits.push(lit.getText());
2726
- continue;
2727
- }
2728
- if (lit.getKind() === import_ts_morph7.SyntaxKind.NullKeyword) {
2729
- nullable = true;
2730
- continue;
2731
- }
2732
- }
2733
- others.push(member);
2734
- }
2735
- if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2736
- return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2737
- }
2738
- if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2739
- return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2740
- }
2741
- if (others.length === 1) {
2742
- const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2743
- return markNullable(inner, nullable || inner.nullable === true);
2744
- }
2745
- return markNullable({ kind: "unknown" }, nullable);
2746
- }
2747
- switch (typeNode.getKind()) {
2748
- case import_ts_morph7.SyntaxKind.StringKeyword:
2749
- return { kind: "string" };
2750
- case import_ts_morph7.SyntaxKind.NumberKeyword:
2751
- return { kind: "number" };
2752
- case import_ts_morph7.SyntaxKind.BooleanKeyword:
2753
- return { kind: "boolean" };
2754
- case import_ts_morph7.SyntaxKind.AnyKeyword:
2755
- case import_ts_morph7.SyntaxKind.UnknownKeyword:
2756
- return { kind: "unknown" };
2757
- default:
2758
- break;
2759
- }
2760
- if (import_ts_morph7.Node.isTypeReference(typeNode)) {
2761
- const refName = typeNode.getTypeName().getText();
2762
- if (refName === "Date") return { kind: "date" };
2763
- if (refName === "Record" || refName === "Object") return { kind: "json" };
2764
- const typeRef = opts?.resolveRef?.(refName) ?? null;
2765
- const en = resolveEnumValues(refName, sourceFile, project);
2766
- if (en) {
2767
- const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
2768
- return typeRef ? { ...base, typeRef } : base;
2769
- }
2770
- if (typeRef) return { kind: "unknown", typeRef };
2771
- return { kind: "unknown" };
2772
- }
2773
- if (import_ts_morph7.Node.isTypeLiteral(typeNode)) return { kind: "json" };
2774
- return { kind: "unknown" };
2320
+ if (import_ts_morph5.Node.isTypeLiteral(typeNode)) return { kind: "json" };
2321
+ return { kind: "unknown" };
2775
2322
  }
2776
2323
  function enumFromDecoratorArgs(args, sourceFile, project) {
2777
2324
  for (const arg of args) {
2778
- if (import_ts_morph7.Node.isArrowFunction(arg)) {
2325
+ if (import_ts_morph5.Node.isArrowFunction(arg)) {
2779
2326
  const body = arg.getBody();
2780
- if (import_ts_morph7.Node.isIdentifier(body)) {
2327
+ if (import_ts_morph5.Node.isIdentifier(body)) {
2781
2328
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2782
2329
  if (en) return en;
2783
2330
  }
2784
2331
  }
2785
- if (import_ts_morph7.Node.isObjectLiteralExpression(arg)) {
2332
+ if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2786
2333
  const itemsProp = arg.getProperty("items");
2787
- if (itemsProp && import_ts_morph7.Node.isPropertyAssignment(itemsProp)) {
2334
+ if (itemsProp && import_ts_morph5.Node.isPropertyAssignment(itemsProp)) {
2788
2335
  const init = itemsProp.getInitializer();
2789
- if (init && import_ts_morph7.Node.isArrowFunction(init)) {
2336
+ if (init && import_ts_morph5.Node.isArrowFunction(init)) {
2790
2337
  const body = init.getBody();
2791
- if (import_ts_morph7.Node.isIdentifier(body)) {
2338
+ if (import_ts_morph5.Node.isIdentifier(body)) {
2792
2339
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2793
2340
  if (en) return en;
2794
2341
  }
@@ -2811,7 +2358,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2811
2358
  return { kind: "string" };
2812
2359
  }
2813
2360
  for (const arg of args) {
2814
- if (import_ts_morph7.Node.isStringLiteral(arg)) {
2361
+ if (import_ts_morph5.Node.isStringLiteral(arg)) {
2815
2362
  const raw = arg.getLiteralValue();
2816
2363
  const kind = classifyTypeKeyword(raw);
2817
2364
  if (kind) {
@@ -2819,11 +2366,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2819
2366
  return { kind };
2820
2367
  }
2821
2368
  }
2822
- if (import_ts_morph7.Node.isObjectLiteralExpression(arg)) {
2369
+ if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2823
2370
  const enumProp = arg.getProperty("enum");
2824
- if (enumProp && import_ts_morph7.Node.isPropertyAssignment(enumProp)) {
2371
+ if (enumProp && import_ts_morph5.Node.isPropertyAssignment(enumProp)) {
2825
2372
  const init = enumProp.getInitializer();
2826
- if (init && import_ts_morph7.Node.isIdentifier(init)) {
2373
+ if (init && import_ts_morph5.Node.isIdentifier(init)) {
2827
2374
  const en = resolveEnumValues(init.getText(), sourceFile, project);
2828
2375
  if (en) {
2829
2376
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -2832,9 +2379,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2832
2379
  }
2833
2380
  }
2834
2381
  const typeProp = arg.getProperty("type");
2835
- if (typeProp && import_ts_morph7.Node.isPropertyAssignment(typeProp)) {
2382
+ if (typeProp && import_ts_morph5.Node.isPropertyAssignment(typeProp)) {
2836
2383
  const init = typeProp.getInitializer();
2837
- if (init && import_ts_morph7.Node.isStringLiteral(init)) {
2384
+ if (init && import_ts_morph5.Node.isStringLiteral(init)) {
2838
2385
  const kind = classifyTypeKeyword(init.getLiteralValue());
2839
2386
  if (kind) return { kind };
2840
2387
  }
@@ -2870,7 +2417,7 @@ function toFilterFieldType(name, r) {
2870
2417
 
2871
2418
  // src/discovery/filter-for.ts
2872
2419
  function classifyFilterForHint(typeInit) {
2873
- if (import_ts_morph8.Node.isStringLiteral(typeInit)) {
2420
+ if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
2874
2421
  switch (typeInit.getLiteralValue()) {
2875
2422
  case "string":
2876
2423
  return { kind: "string" };
@@ -2884,10 +2431,10 @@ function classifyFilterForHint(typeInit) {
2884
2431
  return null;
2885
2432
  }
2886
2433
  }
2887
- if (import_ts_morph8.Node.isArrayLiteralExpression(typeInit)) {
2434
+ if (import_ts_morph6.Node.isArrayLiteralExpression(typeInit)) {
2888
2435
  const values = [];
2889
2436
  for (const el of typeInit.getElements()) {
2890
- if (!import_ts_morph8.Node.isStringLiteral(el)) return null;
2437
+ if (!import_ts_morph6.Node.isStringLiteral(el)) return null;
2891
2438
  values.push(el.getLiteralValue());
2892
2439
  }
2893
2440
  if (values.length === 0) return null;
@@ -2922,11 +2469,11 @@ function extractFilterForHints(classDecl, project) {
2922
2469
  if (!filterForDec) continue;
2923
2470
  const args = filterForDec.getArguments();
2924
2471
  const keyArg = args[0];
2925
- const inputKey = keyArg && import_ts_morph8.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2472
+ const inputKey = keyArg && import_ts_morph6.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2926
2473
  const optsArg = args[1];
2927
- if (optsArg && import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) {
2474
+ if (optsArg && import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) {
2928
2475
  const typeProp = optsArg.getProperty("type");
2929
- if (typeProp && import_ts_morph8.Node.isPropertyAssignment(typeProp)) {
2476
+ if (typeProp && import_ts_morph6.Node.isPropertyAssignment(typeProp)) {
2930
2477
  const typeInit = typeProp.getInitializer();
2931
2478
  if (typeInit) {
2932
2479
  const classified = classifyFilterForHint(typeInit);
@@ -2949,14 +2496,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
2949
2496
  const args = filterDecorator.getArguments();
2950
2497
  if (args.length === 0) continue;
2951
2498
  const filterClassArg = args[0];
2952
- if (!filterClassArg || !import_ts_morph8.Node.isIdentifier(filterClassArg)) continue;
2499
+ if (!filterClassArg || !import_ts_morph6.Node.isIdentifier(filterClassArg)) continue;
2953
2500
  let source = "query";
2954
2501
  const optionsArg = args[1];
2955
- if (optionsArg && import_ts_morph8.Node.isObjectLiteralExpression(optionsArg)) {
2502
+ if (optionsArg && import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) {
2956
2503
  const sourceProp = optionsArg.getProperty("source");
2957
- if (sourceProp && import_ts_morph8.Node.isPropertyAssignment(sourceProp)) {
2504
+ if (sourceProp && import_ts_morph6.Node.isPropertyAssignment(sourceProp)) {
2958
2505
  const init = sourceProp.getInitializer();
2959
- if (init && import_ts_morph8.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
2506
+ if (init && import_ts_morph6.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
2960
2507
  source = "body";
2961
2508
  }
2962
2509
  }
@@ -3019,22 +2566,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
3019
2566
  const args = dec.getArguments();
3020
2567
  if (args.length === 0) continue;
3021
2568
  const arg = args[0];
3022
- if (import_ts_morph8.Node.isObjectLiteralExpression(arg)) {
2569
+ if (import_ts_morph6.Node.isObjectLiteralExpression(arg)) {
3023
2570
  const entityProp = arg.getProperty("entity");
3024
- if (entityProp && import_ts_morph8.Node.isPropertyAssignment(entityProp)) {
2571
+ if (entityProp && import_ts_morph6.Node.isPropertyAssignment(entityProp)) {
3025
2572
  const init = entityProp.getInitializer();
3026
- if (init && import_ts_morph8.Node.isArrowFunction(init)) {
2573
+ if (init && import_ts_morph6.Node.isArrowFunction(init)) {
3027
2574
  const body = init.getBody();
3028
- if (import_ts_morph8.Node.isIdentifier(body)) {
2575
+ if (import_ts_morph6.Node.isIdentifier(body)) {
3029
2576
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3030
2577
  if (resolved?.kind === "class") return resolved.decl;
3031
2578
  }
3032
2579
  }
3033
2580
  }
3034
2581
  }
3035
- if (import_ts_morph8.Node.isArrowFunction(arg)) {
2582
+ if (import_ts_morph6.Node.isArrowFunction(arg)) {
3036
2583
  const body = arg.getBody();
3037
- if (import_ts_morph8.Node.isIdentifier(body)) {
2584
+ if (import_ts_morph6.Node.isIdentifier(body)) {
3038
2585
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3039
2586
  if (resolved?.kind === "class") return resolved.decl;
3040
2587
  }
@@ -3058,11 +2605,11 @@ function extractFilterableEntityFields(filterClass, project) {
3058
2605
  const args = filterableDecorator.getArguments();
3059
2606
  if (args.length === 0) return [];
3060
2607
  const optionsArg = args[0];
3061
- if (!import_ts_morph8.Node.isObjectLiteralExpression(optionsArg)) return [];
2608
+ if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
3062
2609
  const entityProp = optionsArg.getProperty("entity");
3063
- if (!entityProp || !import_ts_morph8.Node.isPropertyAssignment(entityProp)) return [];
2610
+ if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return [];
3064
2611
  const entityInit = entityProp.getInitializer();
3065
- if (!entityInit || !import_ts_morph8.Node.isIdentifier(entityInit)) return [];
2612
+ if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
3066
2613
  const entityName = entityInit.getText();
3067
2614
  const filterSourceFile = filterClass.getSourceFile();
3068
2615
  const resolvedEntity = findType(entityName, filterSourceFile, project);
@@ -3078,17 +2625,17 @@ function extractFilterableEntityFields(filterClass, project) {
3078
2625
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
3079
2626
  if (relationsDecorator) {
3080
2627
  const relArgs = relationsDecorator.getArguments();
3081
- if (relArgs.length > 0 && import_ts_morph8.Node.isObjectLiteralExpression(relArgs[0])) {
2628
+ if (relArgs.length > 0 && import_ts_morph6.Node.isObjectLiteralExpression(relArgs[0])) {
3082
2629
  for (const relProp of relArgs[0].getProperties()) {
3083
- if (!import_ts_morph8.Node.isPropertyAssignment(relProp)) continue;
2630
+ if (!import_ts_morph6.Node.isPropertyAssignment(relProp)) continue;
3084
2631
  const relInit = relProp.getInitializer();
3085
- if (!relInit || !import_ts_morph8.Node.isObjectLiteralExpression(relInit)) continue;
2632
+ if (!relInit || !import_ts_morph6.Node.isObjectLiteralExpression(relInit)) continue;
3086
2633
  const keysProp = relInit.getProperty("keys");
3087
- if (!keysProp || !import_ts_morph8.Node.isPropertyAssignment(keysProp)) continue;
2634
+ if (!keysProp || !import_ts_morph6.Node.isPropertyAssignment(keysProp)) continue;
3088
2635
  const keysInit = keysProp.getInitializer();
3089
- if (!keysInit || !import_ts_morph8.Node.isArrayLiteralExpression(keysInit)) continue;
2636
+ if (!keysInit || !import_ts_morph6.Node.isArrayLiteralExpression(keysInit)) continue;
3090
2637
  for (const el of keysInit.getElements()) {
3091
- if (import_ts_morph8.Node.isStringLiteral(el)) {
2638
+ if (import_ts_morph6.Node.isStringLiteral(el)) {
3092
2639
  fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
3093
2640
  }
3094
2641
  }
@@ -3098,266 +2645,64 @@ function extractFilterableEntityFields(filterClass, project) {
3098
2645
  return fields;
3099
2646
  }
3100
2647
 
3101
- // src/discovery/contracts-fast.ts
3102
- async function discoverContractsFast(opts) {
3103
- const { cwd, glob, tsconfig } = opts;
3104
- const tsconfigPath = tsconfig ? (0, import_node_path11.resolve)(tsconfig) : (0, import_node_path11.join)(cwd, "tsconfig.json");
3105
- let project;
3106
- try {
3107
- project = new import_ts_morph9.Project({
3108
- tsConfigFilePath: tsconfigPath,
3109
- skipAddingFilesFromTsConfig: true,
3110
- skipLoadingLibFiles: true,
3111
- skipFileDependencyResolution: true
3112
- });
3113
- } catch {
3114
- project = new import_ts_morph9.Project({
3115
- skipAddingFilesFromTsConfig: true,
3116
- skipLoadingLibFiles: true,
3117
- skipFileDependencyResolution: true,
3118
- compilerOptions: {
3119
- allowJs: true,
3120
- resolveJsonModule: false,
3121
- strict: false
3122
- }
3123
- });
2648
+ // src/discovery/dto-type-resolver.ts
2649
+ var WRAPPER_TYPES = {
2650
+ // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
2651
+ // wrappers around related entities; the wire shape is just the referenced
2652
+ // entity. Unwrap to the type argument.
2653
+ Ref: "unwrap",
2654
+ Reference: "unwrap",
2655
+ LoadedReference: "unwrap",
2656
+ IdentifiedReference: "unwrap",
2657
+ // MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
2658
+ Opt: "unwrap",
2659
+ Loaded: "unwrap",
2660
+ // Promise<T> — unwrap
2661
+ Promise: "unwrap",
2662
+ // MikroORM Collection<T> serializes as an array of T on the wire.
2663
+ Collection: "arrayOf",
2664
+ // Array<T> generic form
2665
+ Array: "arrayOf"
2666
+ };
2667
+ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2668
+ "Record",
2669
+ "Omit",
2670
+ "Pick",
2671
+ "Partial",
2672
+ "Required",
2673
+ "Readonly",
2674
+ "Map",
2675
+ "Set"
2676
+ ]);
2677
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2678
+ if (depth <= 0) return "unknown";
2679
+ if (import_ts_morph7.Node.isArrayTypeNode(typeNode)) {
2680
+ const elementType = typeNode.getElementTypeNode();
2681
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3124
2682
  }
3125
- const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3126
- for (const f of files) {
3127
- project.addSourceFileAtPath(f);
2683
+ if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
2684
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3128
2685
  }
3129
- const routes = [];
3130
- const prevCtx = setDiscoveryContext({
3131
- projectRoot: cwd,
3132
- tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3133
- });
3134
- try {
3135
- for (const sourceFile of project.getSourceFiles()) {
3136
- routes.push(...extractFromSourceFile(sourceFile, project));
3137
- }
3138
- } finally {
3139
- restoreDiscoveryContext(prevCtx);
2686
+ if (import_ts_morph7.Node.isIntersectionTypeNode(typeNode)) {
2687
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3140
2688
  }
3141
- return routes;
3142
- }
3143
- function zodAstToTs(node) {
3144
- if (!import_ts_morph9.Node.isCallExpression(node)) return "unknown";
3145
- const expr = node.getExpression();
3146
- if (import_ts_morph9.Node.isPropertyAccessExpression(expr)) {
3147
- const methodName = expr.getName();
3148
- const receiver = expr.getExpression();
3149
- if (methodName === "optional") {
3150
- return `${zodAstToTs(receiver)} | undefined`;
2689
+ if (import_ts_morph7.Node.isParenthesizedTypeNode(typeNode)) {
2690
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
2691
+ }
2692
+ if (import_ts_morph7.Node.isTypeReference(typeNode)) {
2693
+ const typeName = typeNode.getTypeName();
2694
+ const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2695
+ if (name === "string" || name === "number" || name === "boolean") return name;
2696
+ if (name === "Date") return "string";
2697
+ if (name === "unknown" || name === "any" || name === "void") return "unknown";
2698
+ if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
2699
+ return "unknown";
2700
+ const wrapperMode = WRAPPER_TYPES[name];
2701
+ if (wrapperMode) {
2702
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3151
2703
  }
3152
- if (methodName === "nullable") {
3153
- return `${zodAstToTs(receiver)} | null`;
3154
- }
3155
- const args = node.getArguments();
3156
- switch (methodName) {
3157
- case "string":
3158
- return "string";
3159
- case "number":
3160
- return "number";
3161
- case "boolean":
3162
- return "boolean";
3163
- case "unknown":
3164
- return "unknown";
3165
- case "any":
3166
- return "unknown";
3167
- case "literal": {
3168
- const lit = args[0];
3169
- if (!lit) return "unknown";
3170
- if (import_ts_morph9.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
3171
- if (import_ts_morph9.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
3172
- if (lit.getKind() === import_ts_morph9.SyntaxKind.TrueKeyword) return "true";
3173
- if (lit.getKind() === import_ts_morph9.SyntaxKind.FalseKeyword) return "false";
3174
- return "unknown";
3175
- }
3176
- case "enum": {
3177
- const arrArg = args[0];
3178
- if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3179
- const members = arrArg.getElements().map(
3180
- (el) => import_ts_morph9.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
3181
- );
3182
- return members.join(" | ");
3183
- }
3184
- case "array": {
3185
- const inner = args[0];
3186
- if (!inner) return "unknown";
3187
- return `Array<${zodAstToTs(inner)}>`;
3188
- }
3189
- case "object": {
3190
- const objArg = args[0];
3191
- if (!objArg || !import_ts_morph9.Node.isObjectLiteralExpression(objArg)) return "unknown";
3192
- const lines = [];
3193
- for (const prop of objArg.getProperties()) {
3194
- if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
3195
- const key = prop.getName();
3196
- const valNode = prop.getInitializer();
3197
- if (!valNode) continue;
3198
- const tsType = zodAstToTs(valNode);
3199
- const isOpt = isOptionalChain(valNode);
3200
- lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
3201
- }
3202
- return `{ ${lines.join("; ")} }`;
3203
- }
3204
- case "union": {
3205
- const arrArg = args[0];
3206
- if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3207
- return arrArg.getElements().map(zodAstToTs).join(" | ");
3208
- }
3209
- case "record": {
3210
- const valArg = args.length === 1 ? args[0] : args[1];
3211
- if (!valArg) return "unknown";
3212
- return `Record<string, ${zodAstToTs(valArg)}>`;
3213
- }
3214
- case "tuple": {
3215
- const arrArg = args[0];
3216
- if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3217
- return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3218
- }
3219
- default:
3220
- return "unknown";
3221
- }
3222
- }
3223
- return "unknown";
3224
- }
3225
- function isOptionalChain(node) {
3226
- if (!import_ts_morph9.Node.isCallExpression(node)) return false;
3227
- const expr = node.getExpression();
3228
- return import_ts_morph9.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3229
- }
3230
- function decoratorStringArg(decoratorExpr) {
3231
- if (!decoratorExpr) return void 0;
3232
- if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3233
- if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
3234
- const first = decoratorExpr.getElements()[0];
3235
- if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
3236
- }
3237
- return void 0;
3238
- }
3239
- function parseDefineContractCall(callExpr) {
3240
- if (!import_ts_morph9.Node.isCallExpression(callExpr)) return null;
3241
- const callee = callExpr.getExpression();
3242
- const calleeName = import_ts_morph9.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph9.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
3243
- if (calleeName !== "defineContract") return null;
3244
- const args = callExpr.getArguments();
3245
- const optsArg = args[0];
3246
- if (!optsArg || !import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) return null;
3247
- let query = null;
3248
- let body = null;
3249
- let response = "unknown";
3250
- let bodyZodText = null;
3251
- let queryZodText = null;
3252
- for (const prop of optsArg.getProperties()) {
3253
- if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
3254
- const propName = prop.getName();
3255
- const val = prop.getInitializer();
3256
- if (!val) continue;
3257
- if (propName === "query") {
3258
- query = zodAstToTs(val);
3259
- queryZodText = val.getText();
3260
- } else if (propName === "body") {
3261
- body = zodAstToTs(val);
3262
- bodyZodText = val.getText();
3263
- } else if (propName === "response") {
3264
- response = zodAstToTs(val);
3265
- }
3266
- }
3267
- return { query, body, response, bodyZodText, queryZodText };
3268
- }
3269
- function deriveClassSegment(className) {
3270
- const noSuffix = className.replace(/Controller$/, "");
3271
- if (!noSuffix) {
3272
- throw new Error(
3273
- `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3274
- );
3275
- }
3276
- return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3277
- }
3278
- function resolveRouteName(className, methodName, classAs, methodAs) {
3279
- const classPortion = classAs ?? deriveClassSegment(className);
3280
- const methodPortion = methodAs ?? methodName;
3281
- return `${classPortion}.${methodPortion}`;
3282
- }
3283
- function joinPaths(prefix, suffix) {
3284
- if (!prefix && !suffix) return "/";
3285
- if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3286
- if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3287
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3288
- const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3289
- const combined = p + s;
3290
- return combined === "" ? "/" : combined;
3291
- }
3292
- function extractParams(path) {
3293
- const matches = path.matchAll(/:(\w+)/g);
3294
- return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3295
- }
3296
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3297
- if (depth <= 0) return "unknown";
3298
- if (import_ts_morph9.Node.isArrayTypeNode(typeNode)) {
3299
- const elementType = typeNode.getElementTypeNode();
3300
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3301
- }
3302
- if (import_ts_morph9.Node.isUnionTypeNode(typeNode)) {
3303
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3304
- }
3305
- if (import_ts_morph9.Node.isIntersectionTypeNode(typeNode)) {
3306
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3307
- }
3308
- if (import_ts_morph9.Node.isParenthesizedTypeNode(typeNode)) {
3309
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3310
- }
3311
- if (import_ts_morph9.Node.isTypeReference(typeNode)) {
3312
- const typeName = typeNode.getTypeName();
3313
- const name = import_ts_morph9.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3314
- if (name === "string" || name === "number" || name === "boolean") return name;
3315
- if (name === "Date") return "string";
3316
- if (name === "unknown" || name === "any" || name === "void") return "unknown";
3317
- if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
3318
- return "unknown";
3319
- if (name === "Ref" || name === "Reference" || name === "LoadedReference" || name === "IdentifiedReference") {
3320
- const typeArgs = typeNode.getTypeArguments();
3321
- const firstTypeArg = typeArgs[0];
3322
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3323
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3324
- }
3325
- return "unknown";
3326
- }
3327
- if (name === "Collection") {
3328
- const typeArgs = typeNode.getTypeArguments();
3329
- const firstTypeArg = typeArgs[0];
3330
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3331
- return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
3332
- }
3333
- return "Array<unknown>";
3334
- }
3335
- if (name === "Opt" || name === "Loaded") {
3336
- const typeArgs = typeNode.getTypeArguments();
3337
- const firstTypeArg = typeArgs[0];
3338
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3339
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3340
- }
3341
- return "unknown";
3342
- }
3343
- if (name === "Array") {
3344
- const typeArgs = typeNode.getTypeArguments();
3345
- const firstTypeArg = typeArgs[0];
3346
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3347
- return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
3348
- }
3349
- return "Array<unknown>";
3350
- }
3351
- if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
3352
- return typeNode.getText();
3353
- }
3354
- if (name === "Promise") {
3355
- const typeArgs = typeNode.getTypeArguments();
3356
- const firstTypeArg = typeArgs[0];
3357
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3358
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3359
- }
3360
- return "unknown";
2704
+ if (PASSTHROUGH_UTILITY.has(name)) {
2705
+ return typeNode.getText();
3361
2706
  }
3362
2707
  const resolved = findType(name, sourceFile, project);
3363
2708
  if (resolved) {
@@ -3367,13 +2712,22 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3367
2712
  return "unknown";
3368
2713
  }
3369
2714
  const kind = typeNode.getKind();
3370
- if (kind === import_ts_morph9.SyntaxKind.StringKeyword) return "string";
3371
- if (kind === import_ts_morph9.SyntaxKind.NumberKeyword) return "number";
3372
- if (kind === import_ts_morph9.SyntaxKind.BooleanKeyword) return "boolean";
3373
- if (kind === import_ts_morph9.SyntaxKind.UnknownKeyword) return "unknown";
3374
- if (kind === import_ts_morph9.SyntaxKind.AnyKeyword) return "unknown";
2715
+ if (kind === import_ts_morph7.SyntaxKind.StringKeyword) return "string";
2716
+ if (kind === import_ts_morph7.SyntaxKind.NumberKeyword) return "number";
2717
+ if (kind === import_ts_morph7.SyntaxKind.BooleanKeyword) return "boolean";
2718
+ if (kind === import_ts_morph7.SyntaxKind.UnknownKeyword) return "unknown";
2719
+ if (kind === import_ts_morph7.SyntaxKind.AnyKeyword) return "unknown";
3375
2720
  return typeNode.getText();
3376
2721
  }
2722
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
2723
+ const typeArgs = typeNode.getTypeArguments();
2724
+ const firstTypeArg = typeArgs[0];
2725
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2726
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
2727
+ return mode === "arrayOf" ? `Array<${inner}>` : inner;
2728
+ }
2729
+ return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2730
+ }
3377
2731
  function expandTypeDecl(result, project, depth) {
3378
2732
  if (depth < 0) return "unknown";
3379
2733
  switch (result.kind) {
@@ -3439,7 +2793,7 @@ function extractParamsType(method, sourceFile, project) {
3439
2793
  const paramArgs = paramDecorator.getArguments();
3440
2794
  if (paramArgs.length === 0) continue;
3441
2795
  const nameArg = paramArgs[0];
3442
- if (!import_ts_morph9.Node.isStringLiteral(nameArg)) continue;
2796
+ if (!import_ts_morph7.Node.isStringLiteral(nameArg)) continue;
3443
2797
  const paramName = nameArg.getLiteralValue();
3444
2798
  const typeNode = param.getTypeNode();
3445
2799
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -3452,13 +2806,13 @@ function extractResponseType(method, sourceFile, project) {
3452
2806
  if (apiResponseDecorator) {
3453
2807
  const args = apiResponseDecorator.getArguments();
3454
2808
  const optsArg = args[0];
3455
- if (optsArg && import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) {
2809
+ if (optsArg && import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) {
3456
2810
  for (const prop of optsArg.getProperties()) {
3457
- if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
2811
+ if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
3458
2812
  if (prop.getName() !== "type") continue;
3459
2813
  const val = prop.getInitializer();
3460
2814
  if (!val) continue;
3461
- if (import_ts_morph9.Node.isArrayLiteralExpression(val)) {
2815
+ if (import_ts_morph7.Node.isArrayLiteralExpression(val)) {
3462
2816
  const elements = val.getElements();
3463
2817
  const firstEl = elements[0];
3464
2818
  if (elements.length > 0 && firstEl !== void 0) {
@@ -3478,7 +2832,7 @@ function extractResponseType(method, sourceFile, project) {
3478
2832
  return "unknown";
3479
2833
  }
3480
2834
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
3481
- if (!import_ts_morph9.Node.isIdentifier(node)) return "unknown";
2835
+ if (!import_ts_morph7.Node.isIdentifier(node)) return "unknown";
3482
2836
  const name = node.getText();
3483
2837
  const resolved = findType(name, sourceFile, project);
3484
2838
  if (resolved) {
@@ -3525,11 +2879,11 @@ function extractDtoContract(method, sourceFile, project) {
3525
2879
  if (apiResp) {
3526
2880
  const args = apiResp.getArguments();
3527
2881
  const optsArg = args[0];
3528
- if (optsArg && import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) {
2882
+ if (optsArg && import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) {
3529
2883
  for (const prop of optsArg.getProperties()) {
3530
- if (import_ts_morph9.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
2884
+ if (import_ts_morph7.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
3531
2885
  const val = prop.getInitializer();
3532
- if (val && import_ts_morph9.Node.isIdentifier(val)) {
2886
+ if (val && import_ts_morph7.Node.isIdentifier(val)) {
3533
2887
  const name = val.getText();
3534
2888
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3535
2889
  if (localDecl?.isExported()) {
@@ -3546,27 +2900,18 @@ function extractDtoContract(method, sourceFile, project) {
3546
2900
  }
3547
2901
  }
3548
2902
  }
3549
- let bodyZodText = null;
3550
- let queryZodText = null;
3551
2903
  let bodySchema = null;
3552
2904
  let querySchema = null;
3553
- const formNested = {};
3554
2905
  const formWarnings = [];
3555
2906
  const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
3556
2907
  if (bodyClass) {
3557
- const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
3558
- bodyZodText = result.schemaText;
3559
- for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
3560
- formWarnings.push(...result.warnings);
3561
2908
  bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2909
+ formWarnings.push(...bodySchema.warnings);
3562
2910
  }
3563
2911
  const queryClass = resolveParamClass(method, "Query", sourceFile, project);
3564
2912
  if (queryClass) {
3565
- const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
3566
- queryZodText = result.schemaText;
3567
- for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
3568
- formWarnings.push(...result.warnings);
3569
2913
  querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
2914
+ formWarnings.push(...querySchema.warnings);
3570
2915
  }
3571
2916
  return {
3572
2917
  query,
@@ -3579,9 +2924,6 @@ function extractDtoContract(method, sourceFile, project) {
3579
2924
  filterFields: filterInfo?.fieldNames ?? null,
3580
2925
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
3581
2926
  filterSource: filterInfo?.source ?? null,
3582
- bodyZodText,
3583
- queryZodText,
3584
- formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
3585
2927
  formWarnings,
3586
2928
  bodySchema,
3587
2929
  querySchema
@@ -3601,6 +2943,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
3601
2943
  }
3602
2944
  return null;
3603
2945
  }
2946
+
2947
+ // src/discovery/zod-ast-to-ts.ts
2948
+ var import_ts_morph8 = require("ts-morph");
2949
+ function zodAstToTs(node) {
2950
+ if (!import_ts_morph8.Node.isCallExpression(node)) return "unknown";
2951
+ const expr = node.getExpression();
2952
+ if (import_ts_morph8.Node.isPropertyAccessExpression(expr)) {
2953
+ const methodName = expr.getName();
2954
+ const receiver = expr.getExpression();
2955
+ if (methodName === "optional") {
2956
+ return `${zodAstToTs(receiver)} | undefined`;
2957
+ }
2958
+ if (methodName === "nullable") {
2959
+ return `${zodAstToTs(receiver)} | null`;
2960
+ }
2961
+ const args = node.getArguments();
2962
+ switch (methodName) {
2963
+ case "string":
2964
+ return "string";
2965
+ case "number":
2966
+ return "number";
2967
+ case "boolean":
2968
+ return "boolean";
2969
+ case "unknown":
2970
+ return "unknown";
2971
+ case "any":
2972
+ return "unknown";
2973
+ case "literal": {
2974
+ const lit = args[0];
2975
+ if (!lit) return "unknown";
2976
+ if (import_ts_morph8.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2977
+ if (import_ts_morph8.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2978
+ if (lit.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword) return "true";
2979
+ if (lit.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) return "false";
2980
+ return "unknown";
2981
+ }
2982
+ case "enum": {
2983
+ const arrArg = args[0];
2984
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
2985
+ const members = arrArg.getElements().map(
2986
+ (el) => import_ts_morph8.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2987
+ );
2988
+ return members.join(" | ");
2989
+ }
2990
+ case "array": {
2991
+ const inner = args[0];
2992
+ if (!inner) return "unknown";
2993
+ return `Array<${zodAstToTs(inner)}>`;
2994
+ }
2995
+ case "object": {
2996
+ const objArg = args[0];
2997
+ if (!objArg || !import_ts_morph8.Node.isObjectLiteralExpression(objArg)) return "unknown";
2998
+ const lines = [];
2999
+ for (const prop of objArg.getProperties()) {
3000
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
3001
+ const key = prop.getName();
3002
+ const valNode = prop.getInitializer();
3003
+ if (!valNode) continue;
3004
+ const tsType = zodAstToTs(valNode);
3005
+ const isOpt = isOptionalChain(valNode);
3006
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
3007
+ }
3008
+ return `{ ${lines.join("; ")} }`;
3009
+ }
3010
+ case "union": {
3011
+ const arrArg = args[0];
3012
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3013
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
3014
+ }
3015
+ case "record": {
3016
+ const valArg = args.length === 1 ? args[0] : args[1];
3017
+ if (!valArg) return "unknown";
3018
+ return `Record<string, ${zodAstToTs(valArg)}>`;
3019
+ }
3020
+ case "tuple": {
3021
+ const arrArg = args[0];
3022
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3023
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3024
+ }
3025
+ default:
3026
+ return "unknown";
3027
+ }
3028
+ }
3029
+ return "unknown";
3030
+ }
3031
+ function isOptionalChain(node) {
3032
+ if (!import_ts_morph8.Node.isCallExpression(node)) return false;
3033
+ const expr = node.getExpression();
3034
+ return import_ts_morph8.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3035
+ }
3036
+ function parseDefineContractCall(callExpr) {
3037
+ if (!import_ts_morph8.Node.isCallExpression(callExpr)) return null;
3038
+ const callee = callExpr.getExpression();
3039
+ const calleeName = import_ts_morph8.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph8.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
3040
+ if (calleeName !== "defineContract") return null;
3041
+ const args = callExpr.getArguments();
3042
+ const optsArg = args[0];
3043
+ if (!optsArg || !import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) return null;
3044
+ let query = null;
3045
+ let body = null;
3046
+ let response = "unknown";
3047
+ let bodyZodText = null;
3048
+ let queryZodText = null;
3049
+ for (const prop of optsArg.getProperties()) {
3050
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
3051
+ const propName = prop.getName();
3052
+ const val = prop.getInitializer();
3053
+ if (!val) continue;
3054
+ if (propName === "query") {
3055
+ query = zodAstToTs(val);
3056
+ queryZodText = val.getText();
3057
+ } else if (propName === "body") {
3058
+ body = zodAstToTs(val);
3059
+ bodyZodText = val.getText();
3060
+ } else if (propName === "response") {
3061
+ response = zodAstToTs(val);
3062
+ }
3063
+ }
3064
+ return { query, body, response, bodyZodText, queryZodText };
3065
+ }
3066
+
3067
+ // src/discovery/contracts-fast.ts
3068
+ async function discoverContractsFast(opts) {
3069
+ const { cwd, glob, tsconfig } = opts;
3070
+ const tsconfigPath = tsconfig ? (0, import_node_path12.resolve)(tsconfig) : (0, import_node_path12.join)(cwd, "tsconfig.json");
3071
+ let project;
3072
+ try {
3073
+ project = new import_ts_morph9.Project({
3074
+ tsConfigFilePath: tsconfigPath,
3075
+ skipAddingFilesFromTsConfig: true,
3076
+ skipLoadingLibFiles: true,
3077
+ skipFileDependencyResolution: true
3078
+ });
3079
+ } catch {
3080
+ project = new import_ts_morph9.Project({
3081
+ skipAddingFilesFromTsConfig: true,
3082
+ skipLoadingLibFiles: true,
3083
+ skipFileDependencyResolution: true,
3084
+ compilerOptions: {
3085
+ allowJs: true,
3086
+ resolveJsonModule: false,
3087
+ strict: false
3088
+ }
3089
+ });
3090
+ }
3091
+ const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3092
+ for (const f of files) {
3093
+ project.addSourceFileAtPath(f);
3094
+ }
3095
+ const routes = [];
3096
+ setDiscoveryContext(project, {
3097
+ projectRoot: cwd,
3098
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3099
+ });
3100
+ for (const sourceFile of project.getSourceFiles()) {
3101
+ routes.push(...extractFromSourceFile(sourceFile, project));
3102
+ }
3103
+ return routes;
3104
+ }
3105
+ function decoratorStringArg(decoratorExpr) {
3106
+ if (!decoratorExpr) return void 0;
3107
+ if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3108
+ if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
3109
+ const first = decoratorExpr.getElements()[0];
3110
+ if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
3111
+ }
3112
+ return void 0;
3113
+ }
3114
+ function deriveClassSegment(className) {
3115
+ const noSuffix = className.replace(/Controller$/, "");
3116
+ if (!noSuffix) {
3117
+ throw new Error(
3118
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3119
+ );
3120
+ }
3121
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3122
+ }
3123
+ function resolveRouteName(className, methodName, classAs, methodAs) {
3124
+ const classPortion = classAs ?? deriveClassSegment(className);
3125
+ const methodPortion = methodAs ?? methodName;
3126
+ return `${classPortion}.${methodPortion}`;
3127
+ }
3128
+ function joinPaths(prefix, suffix) {
3129
+ if (!prefix && !suffix) return "/";
3130
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3131
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3132
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3133
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3134
+ const combined = p + s;
3135
+ return combined === "" ? "/" : combined;
3136
+ }
3137
+ function extractParams(path) {
3138
+ const matches = path.matchAll(/:(\w+)/g);
3139
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3140
+ }
3604
3141
  var HTTP_METHOD_DECORATORS = {
3605
3142
  Get: "GET",
3606
3143
  Post: "POST",
@@ -3611,176 +3148,186 @@ var HTTP_METHOD_DECORATORS = {
3611
3148
  Head: "HEAD",
3612
3149
  All: "ALL"
3613
3150
  };
3151
+ function resolveVerb(method) {
3152
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3153
+ const httpDecorator = method.getDecorator(decoratorName);
3154
+ if (httpDecorator) {
3155
+ const httpArgs = httpDecorator.getArguments();
3156
+ const pathArg = httpArgs[0];
3157
+ return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3158
+ }
3159
+ }
3160
+ return null;
3161
+ }
3162
+ function readAsDecorator(node, label) {
3163
+ const asDecorator = node.getDecorator("As");
3164
+ if (!asDecorator) return void 0;
3165
+ const asName = decoratorStringArg(asDecorator.getArguments()[0]);
3166
+ if (!asName) {
3167
+ throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
3168
+ }
3169
+ return asName;
3170
+ }
3171
+ function buildRoute(args) {
3172
+ const {
3173
+ className,
3174
+ methodName,
3175
+ resolvedMethod,
3176
+ combinedPath,
3177
+ classAs,
3178
+ methodAs,
3179
+ sourceFile,
3180
+ seenNames,
3181
+ contractSource
3182
+ } = args;
3183
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3184
+ const qualifiedRef = `${className}.${methodName}`;
3185
+ const existing = seenNames.get(routeName);
3186
+ if (existing !== void 0) {
3187
+ throw new Error(
3188
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3189
+ );
3190
+ }
3191
+ seenNames.set(routeName, qualifiedRef);
3192
+ return {
3193
+ method: resolvedMethod,
3194
+ path: combinedPath,
3195
+ name: routeName,
3196
+ params: extractParams(combinedPath),
3197
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3198
+ contract: { contractSource }
3199
+ };
3200
+ }
3201
+ function extractContractRoute(args) {
3202
+ const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
3203
+ const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3204
+ if (!firstDecoratorArg) return null;
3205
+ let contractDef = null;
3206
+ let bodyZodRef = null;
3207
+ let queryZodRef = null;
3208
+ if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
3209
+ contractDef = parseDefineContractCall(firstDecoratorArg);
3210
+ } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
3211
+ const identName = firstDecoratorArg.getText();
3212
+ const varDecl = sourceFile.getVariableDeclaration(identName);
3213
+ if (!varDecl) {
3214
+ console.warn(
3215
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3216
+ );
3217
+ return null;
3218
+ }
3219
+ const initializer = varDecl.getInitializer();
3220
+ if (!initializer) return null;
3221
+ contractDef = parseDefineContractCall(initializer);
3222
+ if (contractDef && varDecl.isExported()) {
3223
+ const filePath = sourceFile.getFilePath();
3224
+ if (contractDef.body !== null) {
3225
+ bodyZodRef = { name: `${identName}.body`, filePath };
3226
+ }
3227
+ if (contractDef.query !== null) {
3228
+ queryZodRef = { name: `${identName}.query`, filePath };
3229
+ }
3230
+ }
3231
+ } else {
3232
+ console.warn(
3233
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3234
+ );
3235
+ return null;
3236
+ }
3237
+ if (!contractDef) return null;
3238
+ if (!verb) return null;
3239
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
3240
+ const methodName = method.getName();
3241
+ const classAs = readAsDecorator(cls, `class ${className}`);
3242
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3243
+ return buildRoute({
3244
+ className,
3245
+ methodName,
3246
+ resolvedMethod: verb.httpMethod,
3247
+ combinedPath: resolvedPath,
3248
+ classAs,
3249
+ methodAs,
3250
+ sourceFile,
3251
+ seenNames,
3252
+ contractSource: {
3253
+ query: contractDef.query,
3254
+ body: contractDef.body,
3255
+ response: contractDef.response,
3256
+ // Path A: capture both the importable ref and the raw text. The emitter
3257
+ // prefers inlining the text (client-safe — re-exporting from a controller
3258
+ // would drag server-only deps into the client bundle).
3259
+ bodyZodRef,
3260
+ bodyZodText: contractDef.bodyZodText,
3261
+ queryZodRef,
3262
+ queryZodText: contractDef.queryZodText
3263
+ }
3264
+ });
3265
+ }
3266
+ function extractDtoRoute(args) {
3267
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
3268
+ if (!verb) return null;
3269
+ const combined = joinPaths(prefix, verb.handlerPath);
3270
+ const methodName = method.getName();
3271
+ const classAs = readAsDecorator(cls, `class ${className}`);
3272
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3273
+ const dtoContract = extractDtoContract(method, sourceFile, project);
3274
+ return buildRoute({
3275
+ className,
3276
+ methodName,
3277
+ resolvedMethod: verb.httpMethod,
3278
+ combinedPath: combined,
3279
+ classAs,
3280
+ methodAs,
3281
+ sourceFile,
3282
+ seenNames,
3283
+ contractSource: {
3284
+ query: dtoContract?.query ?? null,
3285
+ body: dtoContract?.body ?? null,
3286
+ response: dtoContract?.response ?? "unknown",
3287
+ queryRef: dtoContract?.queryRef ?? null,
3288
+ bodyRef: dtoContract?.bodyRef ?? null,
3289
+ responseRef: dtoContract?.responseRef ?? null,
3290
+ filterFields: dtoContract?.filterFields ?? null,
3291
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3292
+ filterSource: dtoContract?.filterSource ?? null,
3293
+ formWarnings: dtoContract?.formWarnings ?? [],
3294
+ bodySchema: dtoContract?.bodySchema ?? null,
3295
+ querySchema: dtoContract?.querySchema ?? null
3296
+ }
3297
+ });
3298
+ }
3614
3299
  function extractFromSourceFile(sourceFile, project) {
3615
3300
  const routes = [];
3616
3301
  const seenNames = /* @__PURE__ */ new Map();
3617
- const classes = sourceFile.getClasses();
3618
- for (const cls of classes) {
3302
+ for (const cls of sourceFile.getClasses()) {
3619
3303
  const controllerDecorator = cls.getDecorator("Controller");
3620
3304
  if (!controllerDecorator) continue;
3621
- const controllerArgs = controllerDecorator.getArguments();
3622
- const firstArg3 = controllerArgs[0];
3623
- const prefix = decoratorStringArg(firstArg3) ?? "";
3305
+ const firstArg2 = controllerDecorator.getArguments()[0];
3306
+ const prefix = decoratorStringArg(firstArg2) ?? "";
3624
3307
  const className = cls.getName() ?? "Unknown";
3625
3308
  for (const method of cls.getMethods()) {
3626
- let httpMethod;
3627
- let handlerPath = "";
3628
- for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3629
- const httpDecorator = method.getDecorator(decoratorName);
3630
- if (httpDecorator) {
3631
- httpMethod = verb;
3632
- const httpArgs = httpDecorator.getArguments();
3633
- const pathArg = httpArgs[0];
3634
- handlerPath = decoratorStringArg(pathArg) ?? "";
3635
- break;
3636
- }
3637
- }
3309
+ const verb = resolveVerb(method);
3638
3310
  const applyContractDecorator = method.getDecorator("ApplyContract");
3639
- if (applyContractDecorator) {
3640
- const decoratorArgs = applyContractDecorator.getArguments();
3641
- const firstDecoratorArg = decoratorArgs[0];
3642
- if (!firstDecoratorArg) continue;
3643
- let contractDef = null;
3644
- let bodyZodRef = null;
3645
- let queryZodRef = null;
3646
- if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
3647
- contractDef = parseDefineContractCall(firstDecoratorArg);
3648
- } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
3649
- const identName = firstDecoratorArg.getText();
3650
- const varDecl = sourceFile.getVariableDeclaration(identName);
3651
- if (!varDecl) {
3652
- console.warn(
3653
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3654
- );
3655
- continue;
3656
- }
3657
- const initializer = varDecl.getInitializer();
3658
- if (!initializer) continue;
3659
- contractDef = parseDefineContractCall(initializer);
3660
- if (contractDef && varDecl.isExported()) {
3661
- const filePath = sourceFile.getFilePath();
3662
- if (contractDef.body !== null) {
3663
- bodyZodRef = { name: `${identName}.body`, filePath };
3664
- }
3665
- if (contractDef.query !== null) {
3666
- queryZodRef = { name: `${identName}.query`, filePath };
3667
- }
3668
- }
3669
- } else {
3670
- console.warn(
3671
- `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3672
- );
3673
- continue;
3674
- }
3675
- if (!contractDef) continue;
3676
- if (!httpMethod) continue;
3677
- const resolvedMethod = httpMethod;
3678
- const resolvedPath = joinPaths(prefix, handlerPath);
3679
- const combined = resolvedPath;
3680
- const params = extractParams(combined);
3681
- const methodName = method.getName();
3682
- const classAsDecorator = cls.getDecorator("As");
3683
- let classAs;
3684
- if (classAsDecorator) {
3685
- const classAsArgs = classAsDecorator.getArguments();
3686
- const classAsName = decoratorStringArg(classAsArgs[0]);
3687
- if (!classAsName) {
3688
- throw new Error(
3689
- `@As decorator on class ${className} must have a non-empty string argument.`
3690
- );
3691
- }
3692
- classAs = classAsName;
3693
- }
3694
- const methodAsDecorator = method.getDecorator("As");
3695
- let methodAs;
3696
- if (methodAsDecorator) {
3697
- const methodAsArgs = methodAsDecorator.getArguments();
3698
- const methodAsName = decoratorStringArg(methodAsArgs[0]);
3699
- if (!methodAsName) {
3700
- throw new Error(
3701
- `@As decorator on ${className}.${methodName} must have a non-empty string argument.`
3702
- );
3703
- }
3704
- methodAs = methodAsName;
3705
- }
3706
- const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3707
- const qualifiedRef = `${className}.${methodName}`;
3708
- const existing = seenNames.get(routeName);
3709
- if (existing !== void 0) {
3710
- throw new Error(
3711
- `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3712
- );
3713
- }
3714
- seenNames.set(routeName, qualifiedRef);
3715
- routes.push({
3716
- method: resolvedMethod,
3717
- path: combined,
3718
- name: routeName,
3719
- params,
3720
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3721
- contract: {
3722
- contractSource: {
3723
- query: contractDef.query,
3724
- body: contractDef.body,
3725
- response: contractDef.response,
3726
- // Path A: capture both the importable ref and the raw text. The
3727
- // emitter prefers inlining the text (client-safe — re-exporting from
3728
- // a controller would drag server-only deps into the client bundle).
3729
- bodyZodRef,
3730
- bodyZodText: contractDef.bodyZodText,
3731
- queryZodRef,
3732
- queryZodText: contractDef.queryZodText
3733
- }
3734
- }
3735
- });
3736
- } else {
3737
- if (!httpMethod) continue;
3738
- const combined = joinPaths(prefix, handlerPath);
3739
- const params = extractParams(combined);
3740
- const methodName = method.getName();
3741
- const classAsDecorator = cls.getDecorator("As");
3742
- let classAs;
3743
- if (classAsDecorator) {
3744
- const classAsArgs = classAsDecorator.getArguments();
3745
- const classAsName = decoratorStringArg(classAsArgs[0]);
3746
- if (classAsName) classAs = classAsName;
3747
- }
3748
- const methodAsDecorator = method.getDecorator("As");
3749
- let methodAs;
3750
- if (methodAsDecorator) {
3751
- const methodAsArgs = methodAsDecorator.getArguments();
3752
- const methodAsName = decoratorStringArg(methodAsArgs[0]);
3753
- if (methodAsName) methodAs = methodAsName;
3754
- }
3755
- const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3756
- const dtoContract = extractDtoContract(method, sourceFile, project);
3757
- routes.push({
3758
- method: httpMethod,
3759
- path: combined,
3760
- name: routeName,
3761
- params,
3762
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3763
- contract: {
3764
- contractSource: {
3765
- query: dtoContract?.query ?? null,
3766
- body: dtoContract?.body ?? null,
3767
- response: dtoContract?.response ?? "unknown",
3768
- queryRef: dtoContract?.queryRef ?? null,
3769
- bodyRef: dtoContract?.bodyRef ?? null,
3770
- responseRef: dtoContract?.responseRef ?? null,
3771
- filterFields: dtoContract?.filterFields ?? null,
3772
- filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3773
- filterSource: dtoContract?.filterSource ?? null,
3774
- bodyZodText: dtoContract?.bodyZodText ?? null,
3775
- queryZodText: dtoContract?.queryZodText ?? null,
3776
- formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
3777
- formWarnings: dtoContract?.formWarnings ?? [],
3778
- bodySchema: dtoContract?.bodySchema ?? null,
3779
- querySchema: dtoContract?.querySchema ?? null
3780
- }
3781
- }
3782
- });
3783
- }
3311
+ const route = applyContractDecorator ? extractContractRoute({
3312
+ cls,
3313
+ method,
3314
+ applyContractDecorator,
3315
+ verb,
3316
+ prefix,
3317
+ className,
3318
+ sourceFile,
3319
+ seenNames
3320
+ }) : extractDtoRoute({
3321
+ cls,
3322
+ method,
3323
+ verb,
3324
+ prefix,
3325
+ className,
3326
+ sourceFile,
3327
+ project,
3328
+ seenNames
3329
+ });
3330
+ if (route) routes.push(route);
3784
3331
  }
3785
3332
  }
3786
3333
  return routes;
@@ -3789,7 +3336,7 @@ function extractFromSourceFile(sourceFile, project) {
3789
3336
  // src/watch/lock-file.ts
3790
3337
  var import_promises10 = require("fs/promises");
3791
3338
  var import_promises11 = require("fs/promises");
3792
- var import_node_path12 = require("path");
3339
+ var import_node_path13 = require("path");
3793
3340
  var LOCK_FILE = ".watcher.lock";
3794
3341
  function isProcessAlive(pid) {
3795
3342
  try {
@@ -3801,7 +3348,7 @@ function isProcessAlive(pid) {
3801
3348
  }
3802
3349
  async function acquireLock(outDir) {
3803
3350
  await (0, import_promises11.mkdir)(outDir, { recursive: true });
3804
- const lockPath = (0, import_node_path12.join)(outDir, LOCK_FILE);
3351
+ const lockPath = (0, import_node_path13.join)(outDir, LOCK_FILE);
3805
3352
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3806
3353
  try {
3807
3354
  const fd = await (0, import_promises10.open)(lockPath, "wx");
@@ -3841,7 +3388,7 @@ async function watch(config, onChange) {
3841
3388
  if (lock === null) {
3842
3389
  let holderPid = "unknown";
3843
3390
  try {
3844
- const raw = await (0, import_promises12.readFile)((0, import_node_path13.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3391
+ const raw = await (0, import_promises12.readFile)((0, import_node_path14.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3845
3392
  const data = JSON.parse(raw);
3846
3393
  if (data.pid !== void 0) holderPid = String(data.pid);
3847
3394
  } catch {
@@ -3869,7 +3416,7 @@ async function watch(config, onChange) {
3869
3416
  }
3870
3417
  let pagesDebounceTimer;
3871
3418
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3872
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, pagesGlob), {
3419
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, pagesGlob), {
3873
3420
  ignoreInitial: true,
3874
3421
  persistent: true,
3875
3422
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3895,7 +3442,7 @@ async function watch(config, onChange) {
3895
3442
  pagesWatcher.on("change", schedulePagesRegenerate);
3896
3443
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3897
3444
  let contractsDebounceTimer;
3898
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.contracts.glob), {
3445
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.contracts.glob), {
3899
3446
  ignoreInitial: true,
3900
3447
  persistent: true,
3901
3448
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3925,7 +3472,7 @@ async function watch(config, onChange) {
3925
3472
  contractsWatcher.on("add", scheduleContractsRegenerate);
3926
3473
  contractsWatcher.on("change", scheduleContractsRegenerate);
3927
3474
  contractsWatcher.on("unlink", scheduleContractsRegenerate);
3928
- const formsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.forms.watch), {
3475
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.forms.watch), {
3929
3476
  ignoreInitial: true,
3930
3477
  persistent: true,
3931
3478
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3952,7 +3499,7 @@ async function watch(config, onChange) {
3952
3499
  }
3953
3500
 
3954
3501
  // src/index.ts
3955
- var VERSION = "0.2.0";
3502
+ var VERSION = "0.3.0";
3956
3503
  // Annotate the CommonJS export names for ESM import in node:
3957
3504
  0 && (module.exports = {
3958
3505
  CodegenError,
@@ -3969,7 +3516,6 @@ var VERSION = "0.2.0";
3969
3516
  loadConfig,
3970
3517
  resolveAdapter,
3971
3518
  resolveConfig,
3972
- watch,
3973
- zodAdapter
3519
+ watch
3974
3520
  });
3975
3521
  //# sourceMappingURL=index.cjs.map