@dudousxd/nestjs-codegen 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -42,10 +42,10 @@ __export(src_exports, {
42
42
  extractSchemaFromDto: () => extractSchemaFromDto,
43
43
  generate: () => generate,
44
44
  loadConfig: () => loadConfig,
45
+ renderTsType: () => renderTsType,
45
46
  resolveAdapter: () => resolveAdapter,
46
47
  resolveConfig: () => resolveConfig,
47
- watch: () => watch,
48
- zodAdapter: () => zodAdapter
48
+ watch: () => watch
49
49
  });
50
50
  module.exports = __toCommonJS(src_exports);
51
51
 
@@ -73,112 +73,9 @@ var CodegenError = class extends Error {
73
73
  }
74
74
  };
75
75
 
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
76
  // src/adapters/registry.ts
179
77
  function resolveAdapter(option) {
180
78
  if (typeof option !== "string") return option;
181
- if (option === "zod") return zodAdapter;
182
79
  const pkg = `@dudousxd/nestjs-codegen-${option}`;
183
80
  const named = `${option}Adapter`;
184
81
  throw new ConfigError(
@@ -231,8 +128,21 @@ If this is intentional, move the file inside your project directory.`
231
128
  function resolveConfig(userConfig, cwd) {
232
129
  return applyDefaults(userConfig, cwd ?? process.cwd());
233
130
  }
131
+ function validateUserConfig(userConfig) {
132
+ if (userConfig.validation == null) {
133
+ throw new ConfigError(
134
+ "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
135
+ );
136
+ }
137
+ if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
138
+ throw new ConfigError(
139
+ "Config validation failed: `pages.glob` must be a string when `pages` is set"
140
+ );
141
+ }
142
+ }
234
143
  function applyDefaults(userConfig, cwd) {
235
- const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-inertia");
144
+ validateUserConfig(userConfig);
145
+ const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-codegen");
236
146
  const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
237
147
  let app = null;
238
148
  if (userConfig.app) {
@@ -250,7 +160,8 @@ function applyDefaults(userConfig, cwd) {
250
160
  }
251
161
  return {
252
162
  extensions: userConfig.extensions ?? [],
253
- validation: resolveAdapter(userConfig.validation ?? "zod"),
163
+ // Non-null: validateUserConfig() above throws when `validation` is absent.
164
+ validation: resolveAdapter(userConfig.validation),
254
165
  pages: userConfig.pages ? {
255
166
  glob: userConfig.pages.glob,
256
167
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
@@ -304,18 +215,12 @@ Run \`nestjs-codegen init\` to create a starter config.`
304
215
  `Config file must have a default export. Did you forget \`export default defineConfig({...})\`? (${configPath})`
305
216
  );
306
217
  }
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
218
  return applyDefaults(userConfig, resolvedCwd);
313
219
  }
314
220
 
315
221
  // src/generate.ts
316
222
  var import_promises9 = require("fs/promises");
317
- var import_node_path9 = require("path");
318
- var import_ts_morph3 = require("ts-morph");
223
+ var import_node_path10 = require("path");
319
224
 
320
225
  // src/discovery/pages.ts
321
226
  var import_promises2 = require("fs/promises");
@@ -370,6 +275,7 @@ function extractPropsSource(source, exportName) {
370
275
  }
371
276
 
372
277
  // src/discovery/shared-props.ts
278
+ var import_node_path3 = require("path");
373
279
  var import_ts_morph = require("ts-morph");
374
280
  function discoverSharedProps(project, moduleEntry) {
375
281
  try {
@@ -390,6 +296,31 @@ function discoverSharedProps(project, moduleEntry) {
390
296
  return null;
391
297
  }
392
298
  }
299
+ function discoverSharedPropsFromConfig(config) {
300
+ if (!config.app?.moduleEntry) return null;
301
+ try {
302
+ const tsconfigPath = config.app.tsconfig ?? (0, import_node_path3.join)(config.codegen.cwd, "tsconfig.json");
303
+ let project;
304
+ try {
305
+ project = new import_ts_morph.Project({
306
+ tsConfigFilePath: tsconfigPath,
307
+ skipAddingFilesFromTsConfig: true,
308
+ skipLoadingLibFiles: true,
309
+ skipFileDependencyResolution: true
310
+ });
311
+ } catch {
312
+ project = new import_ts_morph.Project({
313
+ skipAddingFilesFromTsConfig: true,
314
+ skipLoadingLibFiles: true,
315
+ skipFileDependencyResolution: true,
316
+ compilerOptions: { allowJs: true, strict: false }
317
+ });
318
+ }
319
+ return discoverSharedProps(project, config.app.moduleEntry);
320
+ } catch {
321
+ return null;
322
+ }
323
+ }
393
324
  function findForRootCall(sourceFile) {
394
325
  const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression);
395
326
  for (const call of callExpressions) {
@@ -409,9 +340,9 @@ function findForRootCall(sourceFile) {
409
340
  function findShareInitializer(forRootCall) {
410
341
  if (!import_ts_morph.Node.isCallExpression(forRootCall)) return null;
411
342
  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()) {
343
+ const firstArg2 = args[0];
344
+ if (!firstArg2 || !import_ts_morph.Node.isObjectLiteralExpression(firstArg2)) return null;
345
+ for (const prop of firstArg2.getProperties()) {
415
346
  if (import_ts_morph.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
416
347
  return prop.getInitializer() ?? null;
417
348
  }
@@ -510,9 +441,9 @@ function extractFromReturnTypeAnnotation(typeNode) {
510
441
  const typeName = typeNode.getTypeName();
511
442
  if (import_ts_morph.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
512
443
  const typeArgs = typeNode.getTypeArguments();
513
- const firstArg3 = typeArgs[0];
514
- if (firstArg3) {
515
- return extractFromReturnTypeAnnotation(firstArg3);
444
+ const firstArg2 = typeArgs[0];
445
+ if (firstArg2) {
446
+ return extractFromReturnTypeAnnotation(firstArg2);
516
447
  }
517
448
  return null;
518
449
  }
@@ -618,25 +549,14 @@ function inferExpressionType(node) {
618
549
 
619
550
  // src/emit/emit-api.ts
620
551
  var import_promises3 = require("fs/promises");
621
- var import_node_path3 = require("path");
552
+ var import_node_path4 = require("path");
622
553
 
623
554
  // src/extension/registry.ts
624
555
  var import_ts_morph2 = require("ts-morph");
625
556
  function resolveApiSlots(extensions) {
626
- let transport;
627
- let transportOwner;
628
557
  let layer;
629
558
  let layerOwner;
630
559
  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
560
  if (ext.apiClientLayer) {
641
561
  if (layer) {
642
562
  throw new CodegenError(
@@ -648,11 +568,22 @@ function resolveApiSlots(extensions) {
648
568
  }
649
569
  }
650
570
  return {
651
- ...transport ? { transport } : {},
652
571
  ...layer ? { layer } : {}
653
572
  };
654
573
  }
655
574
  var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
575
+ function mergeExclusive(target, incoming, {
576
+ owner,
577
+ describe
578
+ }) {
579
+ for (const [key, value] of incoming) {
580
+ const prev = target.get(key);
581
+ if (prev !== void 0) {
582
+ throw new CodegenError(describe(key, prev.owner, owner));
583
+ }
584
+ target.set(key, { value, owner });
585
+ }
586
+ }
656
587
  function createExtensionContext(config, getRoutes) {
657
588
  let project;
658
589
  return {
@@ -697,29 +628,36 @@ async function collectEmittedFiles(extensions, ctx) {
697
628
  `Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
698
629
  );
699
630
  }
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);
631
+ mergeExclusive(owners, [[key, file]], {
632
+ owner: ext.name,
633
+ describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
634
+ });
707
635
  files.push(file);
708
636
  }
709
637
  }
710
638
  return files;
711
639
  }
712
640
 
641
+ // src/extension/types.ts
642
+ function requestShape(route) {
643
+ const cs = route.contract?.contractSource;
644
+ const isGet = route.method.toUpperCase() === "GET";
645
+ const isQuery = isGet || !!cs?.filterFields?.length;
646
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
647
+ const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
648
+ return { isGet, isQuery, hasBody, hasQuery };
649
+ }
650
+
713
651
  // src/emit/emit-api.ts
714
652
  async function emitApi(routes, outDir, opts = {}) {
715
653
  await (0, import_promises3.mkdir)(outDir, { recursive: true });
716
654
  const content = buildApiFile(routes, outDir, opts);
717
- await (0, import_promises3.writeFile)((0, import_node_path3.join)(outDir, "api.ts"), content, "utf8");
655
+ await (0, import_promises3.writeFile)((0, import_node_path4.join)(outDir, "api.ts"), content, "utf8");
718
656
  }
719
657
  function splitName(name) {
720
658
  return name.split(".");
721
659
  }
722
- function toObjectKey2(segment) {
660
+ function toObjectKey(segment) {
723
661
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
724
662
  return segment;
725
663
  }
@@ -812,7 +750,7 @@ function emitFilterQueryType(c) {
812
750
  }
813
751
  function buildResponseType(c, outDir) {
814
752
  if (c.controllerRef) {
815
- let relPath = (0, import_node_path3.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
753
+ let relPath = (0, import_node_path4.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
816
754
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
817
755
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
818
756
  }
@@ -826,7 +764,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
826
764
  const pad = " ".repeat(indent);
827
765
  const lines = [];
828
766
  for (const [key, node] of tree) {
829
- const objKey = toObjectKey2(key);
767
+ const objKey = toObjectKey(key);
830
768
  if (node.kind === "leaf") {
831
769
  const c = node;
832
770
  const method = c.method.toUpperCase();
@@ -852,15 +790,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
852
790
  return lines;
853
791
  }
854
792
  function buildRequestModel(c) {
855
- const isGet = c.method.toUpperCase() === "GET";
856
793
  const m = c.method.toLowerCase();
857
794
  const flat = JSON.stringify(c.name);
858
795
  const path = JSON.stringify(c.path);
859
796
  const TA = buildRouterTypeAccess(c.name);
860
797
  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";
798
+ const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
864
799
  const fields = [];
865
800
  if (withParams) fields.push(`params: ${TA}['params']`);
866
801
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -882,7 +817,6 @@ function buildRequestModel(c) {
882
817
  urlExpr,
883
818
  optsExpr,
884
819
  responseType: `${TA}['response']`,
885
- bodyType: `${TA}['body']`,
886
820
  queryKeyExpr: `[${flat}, input] as const`
887
821
  };
888
822
  }
@@ -930,7 +864,7 @@ function emitApiObjectBlock(tree, indent, p) {
930
864
  const pad = " ".repeat(indent);
931
865
  const lines = [];
932
866
  for (const [key, node] of tree) {
933
- const objKey = toObjectKey2(key);
867
+ const objKey = toObjectKey(key);
934
868
  if (node.kind === "branch") {
935
869
  lines.push(`${pad}${objKey}: {`);
936
870
  lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
@@ -938,30 +872,28 @@ function emitApiObjectBlock(tree, indent, p) {
938
872
  continue;
939
873
  }
940
874
  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 } : {}
875
+ const leaf = {
876
+ route: node.route,
877
+ request: req,
878
+ requestExpr: renderFetcherRequest(req)
948
879
  };
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));
880
+ const owned = /* @__PURE__ */ new Map();
881
+ if (p.layer) {
882
+ mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
883
+ owner: p.layer.name,
884
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
885
+ });
886
+ }
953
887
  for (const ext of p.memberExts) {
954
888
  const extra = ext.apiMembers?.(leaf, p.ctx);
955
889
  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
- }
890
+ mergeExclusive(owned, Object.entries(extra), {
891
+ owner: ext.name,
892
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
893
+ });
964
894
  }
895
+ const members = {};
896
+ for (const [name, { value }] of owned) members[name] = value;
965
897
  lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
966
898
  }
967
899
  return lines;
@@ -970,10 +902,82 @@ function buildRouterTypeAccess(name) {
970
902
  const segments = splitName(name);
971
903
  return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
972
904
  }
905
+ var RESOLVER_HELPERS = [
906
+ // --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
907
+ "type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
908
+ " ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
909
+ " : P extends keyof R ? R[P] : never;",
910
+ "",
911
+ // --- ResolveByName: resolve a field from a dot-path name ---
912
+ "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
913
+ "",
914
+ // --- ResolveByPath: scan all leaves for matching method + url ---
915
+ // Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
916
+ "type _LeafValues<T> = T extends { method: string; url: string }",
917
+ " ? T",
918
+ " : T extends object ? _LeafValues<T[keyof T]> : never;",
919
+ "",
920
+ "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
921
+ " ? L extends { method: M; url: U }",
922
+ " ? Field extends keyof L ? L[Field] : never",
923
+ " : never",
924
+ " : never;",
925
+ ""
926
+ ];
927
+ var ROUTE_NAMESPACE = [
928
+ "export namespace Route {",
929
+ ' export type Response<K extends string> = ResolveByName<K, "response">;',
930
+ ' export type Body<K extends string> = ResolveByName<K, "body">;',
931
+ ' export type Query<K extends string> = ResolveByName<K, "query">;',
932
+ ' export type Params<K extends string> = ResolveByName<K, "params">;',
933
+ ' export type Error<K extends string> = ResolveByName<K, "error">;',
934
+ ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
935
+ " export type Request<K extends string> = {",
936
+ " body: Body<K>;",
937
+ " query: Query<K>;",
938
+ " params: Params<K>;",
939
+ " };",
940
+ "}",
941
+ ""
942
+ ];
943
+ var PATH_NAMESPACE = [
944
+ "export namespace Path {",
945
+ ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
946
+ ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
947
+ ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
948
+ ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
949
+ ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
950
+ ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
951
+ "}",
952
+ ""
953
+ ];
954
+ var EMPTY_ROUTE_NAMESPACE = [
955
+ "export namespace Route {",
956
+ " export type Response<K extends string> = never;",
957
+ " export type Body<K extends string> = never;",
958
+ " export type Query<K extends string> = never;",
959
+ " export type Params<K extends string> = never;",
960
+ " export type Error<K extends string> = never;",
961
+ " export type FilterFields<K extends string> = never;",
962
+ " export type Request<K extends string> = { body: never; query: never; params: never };",
963
+ "}",
964
+ ""
965
+ ];
966
+ var EMPTY_PATH_NAMESPACE = [
967
+ "export namespace Path {",
968
+ " export type Response<M extends string, U extends string> = never;",
969
+ " export type Body<M extends string, U extends string> = never;",
970
+ " export type Query<M extends string, U extends string> = never;",
971
+ " export type Params<M extends string, U extends string> = never;",
972
+ " export type Error<M extends string, U extends string> = never;",
973
+ " export type FilterFields<M extends string, U extends string> = never;",
974
+ "}",
975
+ ""
976
+ ];
973
977
  function buildApiFile(routes, outDir, opts = {}) {
974
978
  const fetcherImportPath = opts.fetcherImportPath;
975
979
  const extensions = opts.extensions ?? [];
976
- const { transport, layer } = resolveApiSlots(extensions);
980
+ const { layer } = resolveApiSlots(extensions);
977
981
  const memberExts = extensions.filter((e) => e.apiMembers);
978
982
  const headerExts = extensions.filter((e) => e.apiHeader);
979
983
  const contracted = routes.filter((r) => r.contract);
@@ -1018,7 +1022,6 @@ function buildApiFile(routes, outDir, opts = {}) {
1018
1022
  seenImports.add(imp);
1019
1023
  extImports.push(imp);
1020
1024
  };
1021
- for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
1022
1025
  for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
1023
1026
  for (const ext of headerExts) {
1024
1027
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
@@ -1034,8 +1037,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1034
1037
  const emittedNames = /* @__PURE__ */ new Set();
1035
1038
  for (const [filePath, names] of importsByFile) {
1036
1039
  let relPath;
1037
- if ((0, import_node_path3.isAbsolute)(filePath)) {
1038
- relPath = (0, import_node_path3.relative)(outDir, filePath).replace(/\.ts$/, "");
1040
+ if ((0, import_node_path4.isAbsolute)(filePath)) {
1041
+ relPath = (0, import_node_path4.relative)(outDir, filePath).replace(/\.ts$/, "");
1039
1042
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1040
1043
  } else {
1041
1044
  relPath = filePath;
@@ -1063,27 +1066,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1063
1066
  lines.push("}");
1064
1067
  lines.push("export type Api = ReturnType<typeof createApi>;");
1065
1068
  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("");
1069
+ lines.push(...EMPTY_ROUTE_NAMESPACE);
1070
+ lines.push(...EMPTY_PATH_NAMESPACE);
1087
1071
  return lines.join("\n");
1088
1072
  }
1089
1073
  const tree = /* @__PURE__ */ new Map();
@@ -1101,7 +1085,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1101
1085
  path: r.path,
1102
1086
  params: r.params,
1103
1087
  controllerRef: r.controllerRef,
1104
- contractSource: c.contractSource
1088
+ contractSource: c.contractSource,
1089
+ route: r
1105
1090
  };
1106
1091
  insertIntoTree(tree, segments, leaf, name);
1107
1092
  }
@@ -1114,7 +1099,6 @@ function buildApiFile(routes, outDir, opts = {}) {
1114
1099
  lines.push(" return {");
1115
1100
  lines.push(
1116
1101
  ...emitApiObjectBlock(tree, 4, {
1117
- ...transport ? { transport } : {},
1118
1102
  ...layer ? { layer } : {},
1119
1103
  memberExts,
1120
1104
  ctx
@@ -1125,61 +1109,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1125
1109
  lines.push("");
1126
1110
  lines.push("export type Api = ReturnType<typeof createApi>;");
1127
1111
  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("");
1112
+ lines.push(...RESOLVER_HELPERS);
1113
+ lines.push(...ROUTE_NAMESPACE);
1114
+ lines.push(...PATH_NAMESPACE);
1183
1115
  for (const ext of headerExts) {
1184
1116
  const statements = ext.apiHeader?.(ctx)?.statements;
1185
1117
  if (statements?.length) {
@@ -1191,7 +1123,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1191
1123
 
1192
1124
  // src/emit/emit-cache.ts
1193
1125
  var import_promises4 = require("fs/promises");
1194
- var import_node_path4 = require("path");
1126
+ var import_node_path5 = require("path");
1195
1127
  async function emitCache(pages, outDir) {
1196
1128
  await (0, import_promises4.mkdir)(outDir, { recursive: true });
1197
1129
  const entries = await Promise.all(
@@ -1205,95 +1137,21 @@ async function emitCache(pages, outDir) {
1205
1137
  })
1206
1138
  );
1207
1139
  const cache = { pages: entries };
1208
- await (0, import_promises4.writeFile)((0, import_node_path4.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1140
+ await (0, import_promises4.writeFile)((0, import_node_path5.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1209
1141
  `, "utf8");
1210
1142
  }
1211
1143
 
1212
1144
  // src/emit/emit-forms.ts
1213
1145
  var import_promises5 = require("fs/promises");
1214
- var import_node_path5 = require("path");
1146
+ var import_node_path6 = require("path");
1215
1147
  async function emitForms(routes, outDir, config, adapter) {
1216
1148
  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;
1149
+ const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
1150
+ if (content === null) return false;
1226
1151
  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");
1152
+ await (0, import_promises5.writeFile)((0, import_node_path6.join)(outDir, "forms.ts"), content, "utf8");
1229
1153
  return true;
1230
1154
  }
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
1155
  function pascal(segment) {
1298
1156
  return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1299
1157
  }
@@ -1303,124 +1161,16 @@ function deriveBaseName(routeName) {
1303
1161
  const full = segments.map(pascal).join("");
1304
1162
  return { method, full };
1305
1163
  }
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
1164
  function relImport(outDir, filePath) {
1338
- let relPath = (0, import_node_path5.relative)(outDir, filePath).replace(/\.ts$/, "");
1165
+ let relPath = (0, import_node_path6.relative)(outDir, filePath).replace(/\.ts$/, "");
1339
1166
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1340
1167
  return relPath;
1341
1168
  }
1342
1169
  function refRootIdentifier(refName) {
1343
1170
  return refName.split(".")[0] ?? refName;
1344
1171
  }
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");
1172
+ function hasSource(src) {
1173
+ return !!(src.schema || src.zodText || src.zodRef);
1424
1174
  }
1425
1175
  function applyRenames(text, renames) {
1426
1176
  if (!renames || renames.size === 0) return text;
@@ -1486,38 +1236,200 @@ function planNestedSchemas(entries) {
1486
1236
  }
1487
1237
  return { globalSchemas, renamesByEntry };
1488
1238
  }
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}`;
1496
- }
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';");
1239
+ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1240
+ const acceptsRawZod = adapter.acceptsRawZodSource === true;
1241
+ const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1242
+ const methodNameCounts = /* @__PURE__ */ new Map();
1243
+ const candidates = [];
1244
+ for (const route of sorted) {
1245
+ const cs = route.contract.contractSource;
1246
+ const body = {
1247
+ schema: cs.bodySchema ?? null,
1248
+ zodText: cs.bodyZodText ?? null,
1249
+ zodRef: cs.bodyZodRef ?? null
1250
+ };
1251
+ const query = {
1252
+ schema: cs.querySchema ?? null,
1253
+ zodText: cs.queryZodText ?? null,
1254
+ zodRef: cs.queryZodRef ?? null
1255
+ };
1256
+ if (!hasSource(body) && !hasSource(query)) continue;
1257
+ const { method, full } = deriveBaseName(route.name);
1258
+ methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1259
+ candidates.push({
1260
+ routeName: route.name,
1261
+ baseName: full,
1262
+ // resolved below
1263
+ body: hasSource(body) ? body : void 0,
1264
+ query: hasSource(query) ? query : void 0,
1265
+ nestedSchemas: cs.formNestedSchemas ?? null,
1266
+ warnings: cs.formWarnings ?? []
1267
+ });
1508
1268
  }
1509
- if (hasForms) {
1510
- exports2.push("export * from './forms.js';");
1269
+ const entries = candidates.map((c) => {
1270
+ const { method, full } = deriveBaseName(c.routeName);
1271
+ const collision = (methodNameCounts.get(method) ?? 0) > 1;
1272
+ return { ...c, baseName: collision ? full : method };
1273
+ });
1274
+ if (entries.length === 0) return null;
1275
+ const importsByFile = /* @__PURE__ */ new Map();
1276
+ const refAlias = /* @__PURE__ */ new Map();
1277
+ for (const entry of entries) {
1278
+ for (const src of [entry.body, entry.query]) {
1279
+ if (src?.zodRef && !src.zodText && !src.schema) {
1280
+ const root = refRootIdentifier(src.zodRef.name);
1281
+ const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
1282
+ set.add(root);
1283
+ importsByFile.set(src.zodRef.filePath, set);
1284
+ }
1285
+ }
1511
1286
  }
1512
- const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
1513
- "\n"
1514
- );
1515
- await (0, import_promises6.writeFile)((0, import_node_path6.join)(outDir, "index.d.ts"), content, "utf8");
1516
- }
1517
-
1518
- // src/emit/emit-pages.ts
1519
- var import_promises7 = require("fs/promises");
1287
+ const importLines = [];
1288
+ if (importsByFile.size > 0) {
1289
+ const emitted = /* @__PURE__ */ new Set();
1290
+ for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1291
+ const relPath = relImport(outDir, filePath);
1292
+ const specifiers = [];
1293
+ for (const root of [...roots].sort()) {
1294
+ if (emitted.has(root)) {
1295
+ const alias = `${root}_${emitted.size}`;
1296
+ specifiers.push(`${root} as ${alias}`);
1297
+ emitted.add(alias);
1298
+ refAlias.set(`${filePath}\0${root}`, alias);
1299
+ } else {
1300
+ specifiers.push(root);
1301
+ emitted.add(root);
1302
+ refAlias.set(`${filePath}\0${root}`, root);
1303
+ }
1304
+ }
1305
+ importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1306
+ }
1307
+ }
1308
+ const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1309
+ const irNamed = /* @__PURE__ */ new Map();
1310
+ const irTypeAliases = /* @__PURE__ */ new Map();
1311
+ const irAnnotations = /* @__PURE__ */ new Map();
1312
+ const decls = [];
1313
+ const mapEntries = [];
1314
+ let used = false;
1315
+ const renderSource = (src, rename) => {
1316
+ if (src.schema) {
1317
+ const r = adapter.renderModule(src.schema);
1318
+ for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1319
+ if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
1320
+ if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
1321
+ return { text: r.schemaText };
1322
+ }
1323
+ if (src.zodText) {
1324
+ if (!acceptsRawZod) {
1325
+ return {
1326
+ text: "",
1327
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1328
+ };
1329
+ }
1330
+ return { text: applyRenames(src.zodText, rename) };
1331
+ }
1332
+ if (src.zodRef) {
1333
+ if (!acceptsRawZod) {
1334
+ return {
1335
+ text: "",
1336
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1337
+ };
1338
+ }
1339
+ const root = refRootIdentifier(src.zodRef.name);
1340
+ const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
1341
+ const member = src.zodRef.name.slice(root.length);
1342
+ return { text: `${alias}${member}` };
1343
+ }
1344
+ return null;
1345
+ };
1346
+ for (const entry of entries) {
1347
+ const block = [];
1348
+ const rename = renamesByEntry.get(entry) ?? null;
1349
+ let bodyConst;
1350
+ if (entry.warnings && entry.warnings.length > 0) {
1351
+ for (const w of entry.warnings) block.push(`// warning: ${w}`);
1352
+ }
1353
+ if (entry.body) {
1354
+ const rendered = renderSource(entry.body, rename);
1355
+ if (rendered?.warn) {
1356
+ block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
1357
+ } else if (rendered) {
1358
+ used = true;
1359
+ bodyConst = `${entry.baseName}BodySchema`;
1360
+ block.push(`export const ${bodyConst} = ${rendered.text};`);
1361
+ block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
1362
+ }
1363
+ }
1364
+ if (entry.query) {
1365
+ const rendered = renderSource(entry.query, rename);
1366
+ if (rendered?.warn) {
1367
+ block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
1368
+ } else if (rendered) {
1369
+ used = true;
1370
+ const queryConst = `${entry.baseName}QuerySchema`;
1371
+ block.push(`export const ${queryConst} = ${rendered.text};`);
1372
+ block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
1373
+ }
1374
+ }
1375
+ if (block.length === 0) continue;
1376
+ decls.push(`// ${entry.routeName}`, ...block, "");
1377
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
1378
+ }
1379
+ if (!used) return null;
1380
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1381
+ if (acceptsRawZod) {
1382
+ const zodImport = config?.zodImport ?? "zod";
1383
+ lines.push(`import { z } from '${zodImport}';`);
1384
+ } else {
1385
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1386
+ }
1387
+ lines.push(...importLines);
1388
+ lines.push("");
1389
+ const allNested = /* @__PURE__ */ new Map();
1390
+ for (const [n, t] of globalSchemas) allNested.set(n, t);
1391
+ for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1392
+ if (allNested.size > 0) {
1393
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
1394
+ for (const [n, alias] of irTypeAliases) {
1395
+ if (allNested.has(n)) lines.push(`${alias};`);
1396
+ }
1397
+ for (const [n, t] of allNested) {
1398
+ const annotation = irAnnotations.get(n);
1399
+ lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
1400
+ }
1401
+ lines.push("");
1402
+ }
1403
+ lines.push(...decls);
1404
+ lines.push("/** Route name \u2192 body schema map. */");
1405
+ lines.push("export const formSchemas = {");
1406
+ lines.push(...mapEntries);
1407
+ lines.push("} as const;");
1408
+ lines.push("");
1409
+ return lines.join("\n");
1410
+ }
1411
+
1412
+ // src/emit/emit-index.ts
1413
+ var import_promises6 = require("fs/promises");
1520
1414
  var import_node_path7 = require("path");
1415
+ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1416
+ await (0, import_promises6.mkdir)(outDir, { recursive: true });
1417
+ const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
1418
+ if (hasContracts) {
1419
+ exports2.push("export * from './api.js';");
1420
+ }
1421
+ if (hasForms) {
1422
+ exports2.push("export * from './forms.js';");
1423
+ }
1424
+ const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
1425
+ "\n"
1426
+ );
1427
+ await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
1428
+ }
1429
+
1430
+ // src/emit/emit-pages.ts
1431
+ var import_promises7 = require("fs/promises");
1432
+ var import_node_path8 = require("path");
1521
1433
  async function emitPages(pages, outDir, _options = {}) {
1522
1434
  await (0, import_promises7.mkdir)(outDir, { recursive: true });
1523
1435
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -1538,7 +1450,7 @@ ${augBody}
1538
1450
  }
1539
1451
  ${sharedPropsBlock}}
1540
1452
  `;
1541
- await (0, import_promises7.writeFile)((0, import_node_path7.join)(outDir, "pages.d.ts"), content, "utf8");
1453
+ await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, "pages.d.ts"), content, "utf8");
1542
1454
  }
1543
1455
  function buildSharedPropsBlock(sharedProps) {
1544
1456
  if (!sharedProps) return "";
@@ -1557,7 +1469,7 @@ ${propsBody}
1557
1469
  `;
1558
1470
  }
1559
1471
  function buildAugmentationType(page, outDir) {
1560
- let importPath = (0, import_node_path7.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1472
+ let importPath = (0, import_node_path8.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1561
1473
  if (!importPath.startsWith(".")) {
1562
1474
  importPath = `./${importPath}`;
1563
1475
  }
@@ -1569,11 +1481,11 @@ function needsQuotes(name) {
1569
1481
 
1570
1482
  // src/emit/emit-routes.ts
1571
1483
  var import_promises8 = require("fs/promises");
1572
- var import_node_path8 = require("path");
1484
+ var import_node_path9 = require("path");
1573
1485
  async function emitRoutes(routes, outDir) {
1574
1486
  await (0, import_promises8.mkdir)(outDir, { recursive: true });
1575
1487
  const content = buildRoutesFile(routes);
1576
- await (0, import_promises8.writeFile)((0, import_node_path8.join)(outDir, "routes.ts"), content, "utf8");
1488
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, "routes.ts"), content, "utf8");
1577
1489
  }
1578
1490
  function buildRoutesFile(routes) {
1579
1491
  if (routes.length === 0) {
@@ -1701,30 +1613,7 @@ async function generate(config, inputRoutes = []) {
1701
1613
  propsExport: pagesConfig.propsExport,
1702
1614
  componentNameStrategy: pagesConfig.componentNameStrategy
1703
1615
  });
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
- }
1616
+ const sharedProps = discoverSharedPropsFromConfig(config);
1728
1617
  await emitPages(pages, config.codegen.outDir, {
1729
1618
  propsExport: pagesConfig.propsExport,
1730
1619
  sharedProps
@@ -1748,8 +1637,8 @@ async function generate(config, inputRoutes = []) {
1748
1637
  if (extensions.length > 0) {
1749
1638
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1750
1639
  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 });
1640
+ const dest = (0, import_node_path10.join)(config.codegen.outDir, file.path);
1641
+ await (0, import_promises9.mkdir)((0, import_node_path10.dirname)(dest), { recursive: true });
1753
1642
  await (0, import_promises9.writeFile)(dest, file.contents, "utf8");
1754
1643
  }
1755
1644
  }
@@ -1757,35 +1646,31 @@ async function generate(config, inputRoutes = []) {
1757
1646
 
1758
1647
  // src/watch/watcher.ts
1759
1648
  var import_promises12 = require("fs/promises");
1760
- var import_node_path13 = require("path");
1649
+ var import_node_path14 = require("path");
1761
1650
  var import_chokidar = __toESM(require("chokidar"), 1);
1762
1651
 
1763
1652
  // src/discovery/contracts-fast.ts
1764
- var import_node_path11 = require("path");
1653
+ var import_node_path12 = require("path");
1765
1654
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
1766
1655
  var import_ts_morph9 = require("ts-morph");
1767
1656
 
1657
+ // src/discovery/dto-type-resolver.ts
1658
+ var import_ts_morph7 = require("ts-morph");
1659
+
1768
1660
  // src/discovery/dto-to-ir.ts
1769
- var import_ts_morph5 = require("ts-morph");
1661
+ var import_ts_morph4 = require("ts-morph");
1770
1662
 
1771
1663
  // src/discovery/type-ref-resolution.ts
1772
1664
  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;
1665
+ var import_node_path11 = require("path");
1666
+ var import_ts_morph3 = require("ts-morph");
1667
+ var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1668
+ var _ctxByProject = /* @__PURE__ */ new WeakMap();
1669
+ function setDiscoveryContext(project, ctx) {
1670
+ _ctxByProject.set(project, ctx);
1786
1671
  }
1787
- function _tsconfigPaths() {
1788
- return _ctx.tsconfigPaths;
1672
+ function _ctxFor(project) {
1673
+ return _ctxByProject.get(project) ?? _EMPTY_CTX;
1789
1674
  }
1790
1675
  var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
1791
1676
  function dbg(...args) {
@@ -1827,18 +1712,19 @@ function findTypeInFile(name, file) {
1827
1712
  }
1828
1713
  return null;
1829
1714
  }
1830
- function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1715
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1831
1716
  if (moduleSpecifier.startsWith(".")) {
1832
- const dir = (0, import_node_path10.dirname)(sourceFile.getFilePath());
1717
+ const dir = (0, import_node_path11.dirname)(sourceFile.getFilePath());
1833
1718
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
1834
1719
  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")
1720
+ (0, import_node_path11.resolve)(dir, `${noExt}.ts`),
1721
+ (0, import_node_path11.resolve)(dir, `${moduleSpecifier}.ts`),
1722
+ (0, import_node_path11.resolve)(dir, moduleSpecifier, "index.ts")
1838
1723
  ];
1839
1724
  }
1840
- const baseUrl = _projectRoot();
1841
- const tsconfigPaths = _tsconfigPaths();
1725
+ const ctx = _ctxFor(project);
1726
+ const baseUrl = ctx.projectRoot;
1727
+ const tsconfigPaths = ctx.tsconfigPaths;
1842
1728
  dbg(
1843
1729
  "resolveModuleSpecifier",
1844
1730
  moduleSpecifier,
@@ -1854,8 +1740,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1854
1740
  const rest = moduleSpecifier.slice(prefix.length);
1855
1741
  const candidates = [];
1856
1742
  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"));
1743
+ const resolved = (0, import_node_path11.resolve)(baseUrl, mapping.replace("*", rest));
1744
+ candidates.push(`${resolved}.ts`, (0, import_node_path11.resolve)(resolved, "index.ts"));
1859
1745
  }
1860
1746
  dbg(" resolved candidates:", candidates);
1861
1747
  return candidates;
@@ -1955,13 +1841,13 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1955
1841
  name = nodeOrName;
1956
1842
  } else {
1957
1843
  const typeNode = nodeOrName;
1958
- if (opts.unwrapContainers && import_ts_morph4.Node.isArrayTypeNode(typeNode)) {
1844
+ if (opts.unwrapContainers && import_ts_morph3.Node.isArrayTypeNode(typeNode)) {
1959
1845
  const inner = resolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project, opts);
1960
1846
  return inner ? { ...inner, isArray: true } : null;
1961
1847
  }
1962
- if (!import_ts_morph4.Node.isTypeReference(typeNode)) return null;
1848
+ if (!import_ts_morph3.Node.isTypeReference(typeNode)) return null;
1963
1849
  const typeName = typeNode.getTypeName();
1964
- const refName = import_ts_morph4.Node.isIdentifier(typeName) ? typeName.getText() : null;
1850
+ const refName = import_ts_morph3.Node.isIdentifier(typeName) ? typeName.getText() : null;
1965
1851
  if (!refName) return null;
1966
1852
  if (opts.unwrapContainers && refName === "Promise") {
1967
1853
  const first = typeNode.getTypeArguments()[0];
@@ -1984,7 +1870,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1984
1870
  if (!namedImport) continue;
1985
1871
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
1986
1872
  if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
1987
- const tsconfigPaths = _tsconfigPaths();
1873
+ const tsconfigPaths = _ctxFor(project).tsconfigPaths;
1988
1874
  const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
1989
1875
  const prefix = p.replace("*", "");
1990
1876
  return moduleSpecifier.startsWith(prefix);
@@ -2053,10 +1939,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
2053
1939
  depth: 0
2054
1940
  };
2055
1941
  const root = buildObject(classDecl, sourceFile, ctx);
2056
- for (const schemaName of ctx.recursiveSchemas) {
2057
- ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
2058
- }
2059
- return { root, named: ctx.named, warnings: ctx.warnings };
1942
+ return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
2060
1943
  }
2061
1944
  function buildObject(classDecl, classFile, ctx) {
2062
1945
  const props = classDecl.getProperties();
@@ -2076,7 +1959,7 @@ function buildProperty(prop, classFile, ctx) {
2076
1959
  const dec = (n) => decorators.get(n);
2077
1960
  const typeNode = prop.getTypeNode();
2078
1961
  const typeText = typeNode?.getText() ?? "unknown";
2079
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
1962
+ const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
2080
1963
  const typeRefName = resolveTypeFactoryName(dec("Type"));
2081
1964
  if (has("ValidateNested") || typeRefName) {
2082
1965
  const childName = typeRefName ?? singularClassName(typeText);
@@ -2207,18 +2090,27 @@ function baseFromType(typeText, isArrayType) {
2207
2090
  }
2208
2091
  }
2209
2092
  function buildNestedReference(className, fromFile, ctx) {
2210
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2093
+ if (ctx.visiting.has(className)) {
2211
2094
  const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2212
2095
  ctx.emittedClasses.set(className, reserved);
2213
2096
  ctx.recursiveSchemas.add(reserved);
2214
2097
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2215
2098
  ctx.warnedDecorators.add(`recursive:${reserved}`);
2216
- const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
2099
+ const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
2217
2100
  ctx.warnings.push(msg);
2218
2101
  console.warn(`[nestjs-codegen] ${msg}`);
2219
2102
  }
2220
2103
  return { kind: "lazyRef", name: reserved };
2221
2104
  }
2105
+ if (ctx.depth >= 8) {
2106
+ if (!ctx.warnedDecorators.has(`deep:${className}`)) {
2107
+ ctx.warnedDecorators.add(`deep:${className}`);
2108
+ const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
2109
+ ctx.warnings.push(msg);
2110
+ console.warn(`[nestjs-codegen] ${msg}`);
2111
+ }
2112
+ return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2113
+ }
2222
2114
  const existing = ctx.emittedClasses.get(className);
2223
2115
  if (existing) return { kind: "ref", name: existing };
2224
2116
  const schemaName = aliasFor(className, ctx);
@@ -2256,22 +2148,22 @@ function firstArgText(decorator) {
2256
2148
  }
2257
2149
  function numericArg(decorator) {
2258
2150
  const arg = firstArg(decorator);
2259
- if (arg && import_ts_morph5.Node.isNumericLiteral(arg)) return arg.getText();
2151
+ if (arg && import_ts_morph4.Node.isNumericLiteral(arg)) return arg.getText();
2260
2152
  return null;
2261
2153
  }
2262
2154
  function numericArgs(decorator) {
2263
2155
  const args = decorator?.getArguments() ?? [];
2264
- const num = (n) => n && import_ts_morph5.Node.isNumericLiteral(n) ? n.getText() : null;
2156
+ const num = (n) => n && import_ts_morph4.Node.isNumericLiteral(n) ? n.getText() : null;
2265
2157
  return [num(args[0]), num(args[1])];
2266
2158
  }
2267
2159
  function messageRaw(decorator) {
2268
2160
  const args = decorator?.getArguments() ?? [];
2269
2161
  for (const arg of args) {
2270
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2162
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
2271
2163
  for (const prop of arg.getProperties()) {
2272
- if (import_ts_morph5.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2164
+ if (import_ts_morph4.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2273
2165
  const init = prop.getInitializer();
2274
- if (init && import_ts_morph5.Node.isStringLiteral(init)) return init.getText();
2166
+ if (init && import_ts_morph4.Node.isStringLiteral(init)) return init.getText();
2275
2167
  }
2276
2168
  }
2277
2169
  }
@@ -2281,9 +2173,9 @@ function messageRaw(decorator) {
2281
2173
  function resolveTypeFactoryName(decorator) {
2282
2174
  const arg = firstArg(decorator);
2283
2175
  if (!arg) return null;
2284
- if (import_ts_morph5.Node.isArrowFunction(arg)) {
2176
+ if (import_ts_morph4.Node.isArrowFunction(arg)) {
2285
2177
  const body = arg.getBody();
2286
- if (import_ts_morph5.Node.isIdentifier(body)) return body.getText();
2178
+ if (import_ts_morph4.Node.isIdentifier(body)) return body.getText();
2287
2179
  }
2288
2180
  return null;
2289
2181
  }
@@ -2294,7 +2186,7 @@ function singularClassName(typeText) {
2294
2186
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2295
2187
  const arg = firstArg(decorator);
2296
2188
  if (!arg) return null;
2297
- if (import_ts_morph5.Node.isIdentifier(arg)) {
2189
+ if (import_ts_morph4.Node.isIdentifier(arg)) {
2298
2190
  const name = arg.getText();
2299
2191
  const resolved = findType(name, classFile, ctx.project);
2300
2192
  if (resolved && resolved.kind === "enum") {
@@ -2308,12 +2200,12 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
2308
2200
  }
2309
2201
  return { kind: "unknown", note: `@IsEnum(${name}): enum not resolvable to literals` };
2310
2202
  }
2311
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2203
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
2312
2204
  const values = [];
2313
2205
  for (const p of arg.getProperties()) {
2314
- if (!import_ts_morph5.Node.isPropertyAssignment(p)) continue;
2206
+ if (!import_ts_morph4.Node.isPropertyAssignment(p)) continue;
2315
2207
  const init = p.getInitializer();
2316
- if (init && import_ts_morph5.Node.isStringLiteral(init)) values.push(init.getText());
2208
+ if (init && import_ts_morph4.Node.isStringLiteral(init)) values.push(init.getText());
2317
2209
  }
2318
2210
  if (values.length > 0) return { kind: "enum", literals: values };
2319
2211
  }
@@ -2321,9 +2213,9 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
2321
2213
  }
2322
2214
  function inSchemaFromDecorator(decorator) {
2323
2215
  const arg = firstArg(decorator);
2324
- if (arg && import_ts_morph5.Node.isArrayLiteralExpression(arg)) {
2216
+ if (arg && import_ts_morph4.Node.isArrayLiteralExpression(arg)) {
2325
2217
  const elements = arg.getElements();
2326
- const allStrings = elements.every((e) => import_ts_morph5.Node.isStringLiteral(e));
2218
+ const allStrings = elements.every((e) => import_ts_morph4.Node.isStringLiteral(e));
2327
2219
  if (allStrings && elements.length > 0) {
2328
2220
  return { kind: "enum", literals: elements.map((e) => e.getText()) };
2329
2221
  }
@@ -2337,458 +2229,130 @@ function inSchemaFromDecorator(decorator) {
2337
2229
  return null;
2338
2230
  }
2339
2231
 
2340
- // src/discovery/dto-to-zod.ts
2232
+ // src/discovery/filter-for.ts
2341
2233
  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
- };
2234
+
2235
+ // src/discovery/filter-field-types.ts
2236
+ var import_ts_morph5 = require("ts-morph");
2237
+
2238
+ // src/discovery/enum-resolution.ts
2239
+ function resolveEnumValues(name, sourceFile, project) {
2240
+ const resolved = findType(name, sourceFile, project);
2241
+ if (!resolved || resolved.kind !== "enum") return null;
2242
+ let numeric = true;
2243
+ const values = resolved.members.map((m) => {
2244
+ const parsed = JSON.parse(m);
2245
+ if (typeof parsed === "string") numeric = false;
2246
+ return String(parsed);
2247
+ });
2248
+ if (values.length === 0) return null;
2249
+ return { values, numeric };
2391
2250
  }
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(", ")} })`;
2251
+
2252
+ // src/discovery/filter-field-types.ts
2253
+ var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2254
+ var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2255
+ var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2256
+ var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2257
+ var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2258
+ function classifyTypeKeyword(raw) {
2259
+ const t = raw.toLowerCase();
2260
+ if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2261
+ if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2262
+ if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2263
+ if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2264
+ if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2265
+ return null;
2404
2266
  }
2405
- function toObjectKey3(name) {
2406
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2267
+ function markNullable(r, nullable) {
2268
+ return nullable ? { ...r, nullable: true } : r;
2407
2269
  }
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"))})`);
2270
+ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2271
+ if (import_ts_morph5.Node.isUnionTypeNode(typeNode)) {
2272
+ let nullable = false;
2273
+ const stringLits = [];
2274
+ const numberLits = [];
2275
+ const others = [];
2276
+ for (const member of typeNode.getTypeNodes()) {
2277
+ const kind = member.getKind();
2278
+ if (kind === import_ts_morph5.SyntaxKind.NullKeyword || kind === import_ts_morph5.SyntaxKind.UndefinedKeyword) {
2279
+ nullable = true;
2280
+ continue;
2281
+ }
2282
+ if (import_ts_morph5.Node.isLiteralTypeNode(member)) {
2283
+ const lit = member.getLiteral();
2284
+ if (import_ts_morph5.Node.isStringLiteral(lit)) {
2285
+ stringLits.push(lit.getLiteralValue());
2286
+ continue;
2287
+ }
2288
+ if (import_ts_morph5.Node.isNumericLiteral(lit)) {
2289
+ numberLits.push(lit.getText());
2290
+ continue;
2291
+ }
2292
+ if (lit.getKind() === import_ts_morph5.SyntaxKind.NullKeyword) {
2293
+ nullable = true;
2294
+ continue;
2295
+ }
2296
+ }
2297
+ others.push(member);
2298
+ }
2299
+ if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2300
+ return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2301
+ }
2302
+ if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2303
+ return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2304
+ }
2305
+ if (others.length === 1) {
2306
+ const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2307
+ return markNullable(inner, nullable || inner.nullable === true);
2308
+ }
2309
+ return markNullable({ kind: "unknown" }, nullable);
2444
2310
  }
2445
- if (has("IsUUID")) {
2446
- base = ensureStringBase(base);
2447
- refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
2311
+ switch (typeNode.getKind()) {
2312
+ case import_ts_morph5.SyntaxKind.StringKeyword:
2313
+ return { kind: "string" };
2314
+ case import_ts_morph5.SyntaxKind.NumberKeyword:
2315
+ return { kind: "number" };
2316
+ case import_ts_morph5.SyntaxKind.BooleanKeyword:
2317
+ return { kind: "boolean" };
2318
+ case import_ts_morph5.SyntaxKind.AnyKeyword:
2319
+ case import_ts_morph5.SyntaxKind.UnknownKeyword:
2320
+ return { kind: "unknown" };
2321
+ default:
2322
+ break;
2448
2323
  }
2449
- if (has("Matches")) {
2450
- const re = firstArgText2(dec("Matches"));
2451
- if (re) {
2452
- base = ensureStringBase(base);
2453
- refinements.push(`.regex(${re})`);
2324
+ if (import_ts_morph5.Node.isTypeReference(typeNode)) {
2325
+ const refName = typeNode.getTypeName().getText();
2326
+ if (refName === "Date") return { kind: "date" };
2327
+ if (refName === "Record" || refName === "Object") return { kind: "json" };
2328
+ const typeRef = opts?.resolveRef?.(refName) ?? null;
2329
+ const en = resolveEnumValues(refName, sourceFile, project);
2330
+ if (en) {
2331
+ const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
2332
+ return typeRef ? { ...base, typeRef } : base;
2454
2333
  }
2334
+ if (typeRef) return { kind: "unknown", typeRef };
2335
+ return { kind: "unknown" };
2455
2336
  }
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}`);
2496
- }
2497
- }
2498
- }
2499
- let expr = base + refinements.join("");
2500
- if (isArrayType && !expr.startsWith("z.array(")) {
2501
- expr = `z.array(${expr})`;
2502
- }
2503
- expr = applyPresence2(expr, decorators);
2504
- if (comments.length > 0) {
2505
- expr = `${expr} ${comments.join(" ")}`;
2506
- }
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" };
2337
+ if (import_ts_morph5.Node.isTypeLiteral(typeNode)) return { kind: "json" };
2774
2338
  return { kind: "unknown" };
2775
2339
  }
2776
2340
  function enumFromDecoratorArgs(args, sourceFile, project) {
2777
2341
  for (const arg of args) {
2778
- if (import_ts_morph7.Node.isArrowFunction(arg)) {
2342
+ if (import_ts_morph5.Node.isArrowFunction(arg)) {
2779
2343
  const body = arg.getBody();
2780
- if (import_ts_morph7.Node.isIdentifier(body)) {
2344
+ if (import_ts_morph5.Node.isIdentifier(body)) {
2781
2345
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2782
2346
  if (en) return en;
2783
2347
  }
2784
2348
  }
2785
- if (import_ts_morph7.Node.isObjectLiteralExpression(arg)) {
2349
+ if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2786
2350
  const itemsProp = arg.getProperty("items");
2787
- if (itemsProp && import_ts_morph7.Node.isPropertyAssignment(itemsProp)) {
2351
+ if (itemsProp && import_ts_morph5.Node.isPropertyAssignment(itemsProp)) {
2788
2352
  const init = itemsProp.getInitializer();
2789
- if (init && import_ts_morph7.Node.isArrowFunction(init)) {
2353
+ if (init && import_ts_morph5.Node.isArrowFunction(init)) {
2790
2354
  const body = init.getBody();
2791
- if (import_ts_morph7.Node.isIdentifier(body)) {
2355
+ if (import_ts_morph5.Node.isIdentifier(body)) {
2792
2356
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2793
2357
  if (en) return en;
2794
2358
  }
@@ -2811,7 +2375,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2811
2375
  return { kind: "string" };
2812
2376
  }
2813
2377
  for (const arg of args) {
2814
- if (import_ts_morph7.Node.isStringLiteral(arg)) {
2378
+ if (import_ts_morph5.Node.isStringLiteral(arg)) {
2815
2379
  const raw = arg.getLiteralValue();
2816
2380
  const kind = classifyTypeKeyword(raw);
2817
2381
  if (kind) {
@@ -2819,11 +2383,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2819
2383
  return { kind };
2820
2384
  }
2821
2385
  }
2822
- if (import_ts_morph7.Node.isObjectLiteralExpression(arg)) {
2386
+ if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2823
2387
  const enumProp = arg.getProperty("enum");
2824
- if (enumProp && import_ts_morph7.Node.isPropertyAssignment(enumProp)) {
2388
+ if (enumProp && import_ts_morph5.Node.isPropertyAssignment(enumProp)) {
2825
2389
  const init = enumProp.getInitializer();
2826
- if (init && import_ts_morph7.Node.isIdentifier(init)) {
2390
+ if (init && import_ts_morph5.Node.isIdentifier(init)) {
2827
2391
  const en = resolveEnumValues(init.getText(), sourceFile, project);
2828
2392
  if (en) {
2829
2393
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -2832,9 +2396,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2832
2396
  }
2833
2397
  }
2834
2398
  const typeProp = arg.getProperty("type");
2835
- if (typeProp && import_ts_morph7.Node.isPropertyAssignment(typeProp)) {
2399
+ if (typeProp && import_ts_morph5.Node.isPropertyAssignment(typeProp)) {
2836
2400
  const init = typeProp.getInitializer();
2837
- if (init && import_ts_morph7.Node.isStringLiteral(init)) {
2401
+ if (init && import_ts_morph5.Node.isStringLiteral(init)) {
2838
2402
  const kind = classifyTypeKeyword(init.getLiteralValue());
2839
2403
  if (kind) return { kind };
2840
2404
  }
@@ -2870,7 +2434,7 @@ function toFilterFieldType(name, r) {
2870
2434
 
2871
2435
  // src/discovery/filter-for.ts
2872
2436
  function classifyFilterForHint(typeInit) {
2873
- if (import_ts_morph8.Node.isStringLiteral(typeInit)) {
2437
+ if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
2874
2438
  switch (typeInit.getLiteralValue()) {
2875
2439
  case "string":
2876
2440
  return { kind: "string" };
@@ -2884,10 +2448,10 @@ function classifyFilterForHint(typeInit) {
2884
2448
  return null;
2885
2449
  }
2886
2450
  }
2887
- if (import_ts_morph8.Node.isArrayLiteralExpression(typeInit)) {
2451
+ if (import_ts_morph6.Node.isArrayLiteralExpression(typeInit)) {
2888
2452
  const values = [];
2889
2453
  for (const el of typeInit.getElements()) {
2890
- if (!import_ts_morph8.Node.isStringLiteral(el)) return null;
2454
+ if (!import_ts_morph6.Node.isStringLiteral(el)) return null;
2891
2455
  values.push(el.getLiteralValue());
2892
2456
  }
2893
2457
  if (values.length === 0) return null;
@@ -2922,11 +2486,11 @@ function extractFilterForHints(classDecl, project) {
2922
2486
  if (!filterForDec) continue;
2923
2487
  const args = filterForDec.getArguments();
2924
2488
  const keyArg = args[0];
2925
- const inputKey = keyArg && import_ts_morph8.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2489
+ const inputKey = keyArg && import_ts_morph6.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2926
2490
  const optsArg = args[1];
2927
- if (optsArg && import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) {
2491
+ if (optsArg && import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) {
2928
2492
  const typeProp = optsArg.getProperty("type");
2929
- if (typeProp && import_ts_morph8.Node.isPropertyAssignment(typeProp)) {
2493
+ if (typeProp && import_ts_morph6.Node.isPropertyAssignment(typeProp)) {
2930
2494
  const typeInit = typeProp.getInitializer();
2931
2495
  if (typeInit) {
2932
2496
  const classified = classifyFilterForHint(typeInit);
@@ -2949,14 +2513,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
2949
2513
  const args = filterDecorator.getArguments();
2950
2514
  if (args.length === 0) continue;
2951
2515
  const filterClassArg = args[0];
2952
- if (!filterClassArg || !import_ts_morph8.Node.isIdentifier(filterClassArg)) continue;
2516
+ if (!filterClassArg || !import_ts_morph6.Node.isIdentifier(filterClassArg)) continue;
2953
2517
  let source = "query";
2954
2518
  const optionsArg = args[1];
2955
- if (optionsArg && import_ts_morph8.Node.isObjectLiteralExpression(optionsArg)) {
2519
+ if (optionsArg && import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) {
2956
2520
  const sourceProp = optionsArg.getProperty("source");
2957
- if (sourceProp && import_ts_morph8.Node.isPropertyAssignment(sourceProp)) {
2521
+ if (sourceProp && import_ts_morph6.Node.isPropertyAssignment(sourceProp)) {
2958
2522
  const init = sourceProp.getInitializer();
2959
- if (init && import_ts_morph8.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
2523
+ if (init && import_ts_morph6.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
2960
2524
  source = "body";
2961
2525
  }
2962
2526
  }
@@ -3019,22 +2583,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
3019
2583
  const args = dec.getArguments();
3020
2584
  if (args.length === 0) continue;
3021
2585
  const arg = args[0];
3022
- if (import_ts_morph8.Node.isObjectLiteralExpression(arg)) {
2586
+ if (import_ts_morph6.Node.isObjectLiteralExpression(arg)) {
3023
2587
  const entityProp = arg.getProperty("entity");
3024
- if (entityProp && import_ts_morph8.Node.isPropertyAssignment(entityProp)) {
2588
+ if (entityProp && import_ts_morph6.Node.isPropertyAssignment(entityProp)) {
3025
2589
  const init = entityProp.getInitializer();
3026
- if (init && import_ts_morph8.Node.isArrowFunction(init)) {
2590
+ if (init && import_ts_morph6.Node.isArrowFunction(init)) {
3027
2591
  const body = init.getBody();
3028
- if (import_ts_morph8.Node.isIdentifier(body)) {
2592
+ if (import_ts_morph6.Node.isIdentifier(body)) {
3029
2593
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3030
2594
  if (resolved?.kind === "class") return resolved.decl;
3031
2595
  }
3032
2596
  }
3033
2597
  }
3034
2598
  }
3035
- if (import_ts_morph8.Node.isArrowFunction(arg)) {
2599
+ if (import_ts_morph6.Node.isArrowFunction(arg)) {
3036
2600
  const body = arg.getBody();
3037
- if (import_ts_morph8.Node.isIdentifier(body)) {
2601
+ if (import_ts_morph6.Node.isIdentifier(body)) {
3038
2602
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3039
2603
  if (resolved?.kind === "class") return resolved.decl;
3040
2604
  }
@@ -3058,11 +2622,11 @@ function extractFilterableEntityFields(filterClass, project) {
3058
2622
  const args = filterableDecorator.getArguments();
3059
2623
  if (args.length === 0) return [];
3060
2624
  const optionsArg = args[0];
3061
- if (!import_ts_morph8.Node.isObjectLiteralExpression(optionsArg)) return [];
2625
+ if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
3062
2626
  const entityProp = optionsArg.getProperty("entity");
3063
- if (!entityProp || !import_ts_morph8.Node.isPropertyAssignment(entityProp)) return [];
2627
+ if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return [];
3064
2628
  const entityInit = entityProp.getInitializer();
3065
- if (!entityInit || !import_ts_morph8.Node.isIdentifier(entityInit)) return [];
2629
+ if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
3066
2630
  const entityName = entityInit.getText();
3067
2631
  const filterSourceFile = filterClass.getSourceFile();
3068
2632
  const resolvedEntity = findType(entityName, filterSourceFile, project);
@@ -3078,17 +2642,17 @@ function extractFilterableEntityFields(filterClass, project) {
3078
2642
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
3079
2643
  if (relationsDecorator) {
3080
2644
  const relArgs = relationsDecorator.getArguments();
3081
- if (relArgs.length > 0 && import_ts_morph8.Node.isObjectLiteralExpression(relArgs[0])) {
2645
+ if (relArgs.length > 0 && import_ts_morph6.Node.isObjectLiteralExpression(relArgs[0])) {
3082
2646
  for (const relProp of relArgs[0].getProperties()) {
3083
- if (!import_ts_morph8.Node.isPropertyAssignment(relProp)) continue;
2647
+ if (!import_ts_morph6.Node.isPropertyAssignment(relProp)) continue;
3084
2648
  const relInit = relProp.getInitializer();
3085
- if (!relInit || !import_ts_morph8.Node.isObjectLiteralExpression(relInit)) continue;
2649
+ if (!relInit || !import_ts_morph6.Node.isObjectLiteralExpression(relInit)) continue;
3086
2650
  const keysProp = relInit.getProperty("keys");
3087
- if (!keysProp || !import_ts_morph8.Node.isPropertyAssignment(keysProp)) continue;
2651
+ if (!keysProp || !import_ts_morph6.Node.isPropertyAssignment(keysProp)) continue;
3088
2652
  const keysInit = keysProp.getInitializer();
3089
- if (!keysInit || !import_ts_morph8.Node.isArrayLiteralExpression(keysInit)) continue;
2653
+ if (!keysInit || !import_ts_morph6.Node.isArrayLiteralExpression(keysInit)) continue;
3090
2654
  for (const el of keysInit.getElements()) {
3091
- if (import_ts_morph8.Node.isStringLiteral(el)) {
2655
+ if (import_ts_morph6.Node.isStringLiteral(el)) {
3092
2656
  fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
3093
2657
  }
3094
2658
  }
@@ -3098,267 +2662,65 @@ function extractFilterableEntityFields(filterClass, project) {
3098
2662
  return fields;
3099
2663
  }
3100
2664
 
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
- });
2665
+ // src/discovery/dto-type-resolver.ts
2666
+ var WRAPPER_TYPES = {
2667
+ // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
2668
+ // wrappers around related entities; the wire shape is just the referenced
2669
+ // entity. Unwrap to the type argument.
2670
+ Ref: "unwrap",
2671
+ Reference: "unwrap",
2672
+ LoadedReference: "unwrap",
2673
+ IdentifiedReference: "unwrap",
2674
+ // MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
2675
+ Opt: "unwrap",
2676
+ Loaded: "unwrap",
2677
+ // Promise<T> — unwrap
2678
+ Promise: "unwrap",
2679
+ // MikroORM Collection<T> serializes as an array of T on the wire.
2680
+ Collection: "arrayOf",
2681
+ // Array<T> generic form
2682
+ Array: "arrayOf"
2683
+ };
2684
+ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2685
+ "Record",
2686
+ "Omit",
2687
+ "Pick",
2688
+ "Partial",
2689
+ "Required",
2690
+ "Readonly",
2691
+ "Map",
2692
+ "Set"
2693
+ ]);
2694
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2695
+ if (depth <= 0) return "unknown";
2696
+ if (import_ts_morph7.Node.isArrayTypeNode(typeNode)) {
2697
+ const elementType = typeNode.getElementTypeNode();
2698
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3124
2699
  }
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);
2700
+ if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
2701
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3128
2702
  }
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);
2703
+ if (import_ts_morph7.Node.isIntersectionTypeNode(typeNode)) {
2704
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3140
2705
  }
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`;
3151
- }
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)) {
2706
+ if (import_ts_morph7.Node.isParenthesizedTypeNode(typeNode)) {
3309
2707
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3310
2708
  }
3311
- if (import_ts_morph9.Node.isTypeReference(typeNode)) {
2709
+ if (import_ts_morph7.Node.isTypeReference(typeNode)) {
3312
2710
  const typeName = typeNode.getTypeName();
3313
- const name = import_ts_morph9.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2711
+ const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3314
2712
  if (name === "string" || name === "number" || name === "boolean") return name;
3315
2713
  if (name === "Date") return "string";
3316
2714
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
3317
2715
  if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
3318
2716
  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";
2717
+ const wrapperMode = WRAPPER_TYPES[name];
2718
+ if (wrapperMode) {
2719
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3326
2720
  }
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)) {
2721
+ if (PASSTHROUGH_UTILITY.has(name)) {
3352
2722
  return typeNode.getText();
3353
2723
  }
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";
3361
- }
3362
2724
  const resolved = findType(name, sourceFile, project);
3363
2725
  if (resolved) {
3364
2726
  return expandTypeDecl(resolved, project, depth - 1);
@@ -3367,13 +2729,22 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3367
2729
  return "unknown";
3368
2730
  }
3369
2731
  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";
2732
+ if (kind === import_ts_morph7.SyntaxKind.StringKeyword) return "string";
2733
+ if (kind === import_ts_morph7.SyntaxKind.NumberKeyword) return "number";
2734
+ if (kind === import_ts_morph7.SyntaxKind.BooleanKeyword) return "boolean";
2735
+ if (kind === import_ts_morph7.SyntaxKind.UnknownKeyword) return "unknown";
2736
+ if (kind === import_ts_morph7.SyntaxKind.AnyKeyword) return "unknown";
3375
2737
  return typeNode.getText();
3376
2738
  }
2739
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
2740
+ const typeArgs = typeNode.getTypeArguments();
2741
+ const firstTypeArg = typeArgs[0];
2742
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2743
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
2744
+ return mode === "arrayOf" ? `Array<${inner}>` : inner;
2745
+ }
2746
+ return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2747
+ }
3377
2748
  function expandTypeDecl(result, project, depth) {
3378
2749
  if (depth < 0) return "unknown";
3379
2750
  switch (result.kind) {
@@ -3439,7 +2810,7 @@ function extractParamsType(method, sourceFile, project) {
3439
2810
  const paramArgs = paramDecorator.getArguments();
3440
2811
  if (paramArgs.length === 0) continue;
3441
2812
  const nameArg = paramArgs[0];
3442
- if (!import_ts_morph9.Node.isStringLiteral(nameArg)) continue;
2813
+ if (!import_ts_morph7.Node.isStringLiteral(nameArg)) continue;
3443
2814
  const paramName = nameArg.getLiteralValue();
3444
2815
  const typeNode = param.getTypeNode();
3445
2816
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -3452,13 +2823,13 @@ function extractResponseType(method, sourceFile, project) {
3452
2823
  if (apiResponseDecorator) {
3453
2824
  const args = apiResponseDecorator.getArguments();
3454
2825
  const optsArg = args[0];
3455
- if (optsArg && import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) {
2826
+ if (optsArg && import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) {
3456
2827
  for (const prop of optsArg.getProperties()) {
3457
- if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
2828
+ if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
3458
2829
  if (prop.getName() !== "type") continue;
3459
2830
  const val = prop.getInitializer();
3460
2831
  if (!val) continue;
3461
- if (import_ts_morph9.Node.isArrayLiteralExpression(val)) {
2832
+ if (import_ts_morph7.Node.isArrayLiteralExpression(val)) {
3462
2833
  const elements = val.getElements();
3463
2834
  const firstEl = elements[0];
3464
2835
  if (elements.length > 0 && firstEl !== void 0) {
@@ -3478,7 +2849,7 @@ function extractResponseType(method, sourceFile, project) {
3478
2849
  return "unknown";
3479
2850
  }
3480
2851
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
3481
- if (!import_ts_morph9.Node.isIdentifier(node)) return "unknown";
2852
+ if (!import_ts_morph7.Node.isIdentifier(node)) return "unknown";
3482
2853
  const name = node.getText();
3483
2854
  const resolved = findType(name, sourceFile, project);
3484
2855
  if (resolved) {
@@ -3525,11 +2896,11 @@ function extractDtoContract(method, sourceFile, project) {
3525
2896
  if (apiResp) {
3526
2897
  const args = apiResp.getArguments();
3527
2898
  const optsArg = args[0];
3528
- if (optsArg && import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) {
2899
+ if (optsArg && import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) {
3529
2900
  for (const prop of optsArg.getProperties()) {
3530
- if (import_ts_morph9.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
2901
+ if (import_ts_morph7.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
3531
2902
  const val = prop.getInitializer();
3532
- if (val && import_ts_morph9.Node.isIdentifier(val)) {
2903
+ if (val && import_ts_morph7.Node.isIdentifier(val)) {
3533
2904
  const name = val.getText();
3534
2905
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3535
2906
  if (localDecl?.isExported()) {
@@ -3546,27 +2917,18 @@ function extractDtoContract(method, sourceFile, project) {
3546
2917
  }
3547
2918
  }
3548
2919
  }
3549
- let bodyZodText = null;
3550
- let queryZodText = null;
3551
2920
  let bodySchema = null;
3552
2921
  let querySchema = null;
3553
- const formNested = {};
3554
2922
  const formWarnings = [];
3555
2923
  const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
3556
2924
  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
2925
  bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2926
+ formWarnings.push(...bodySchema.warnings);
3562
2927
  }
3563
2928
  const queryClass = resolveParamClass(method, "Query", sourceFile, project);
3564
2929
  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
2930
  querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
2931
+ formWarnings.push(...querySchema.warnings);
3570
2932
  }
3571
2933
  return {
3572
2934
  query,
@@ -3579,9 +2941,6 @@ function extractDtoContract(method, sourceFile, project) {
3579
2941
  filterFields: filterInfo?.fieldNames ?? null,
3580
2942
  filterFieldTypes: filterInfo?.fieldTypes ?? null,
3581
2943
  filterSource: filterInfo?.source ?? null,
3582
- bodyZodText,
3583
- queryZodText,
3584
- formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
3585
2944
  formWarnings,
3586
2945
  bodySchema,
3587
2946
  querySchema
@@ -3601,6 +2960,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
3601
2960
  }
3602
2961
  return null;
3603
2962
  }
2963
+
2964
+ // src/discovery/zod-ast-to-ts.ts
2965
+ var import_ts_morph8 = require("ts-morph");
2966
+ function zodAstToTs(node) {
2967
+ if (!import_ts_morph8.Node.isCallExpression(node)) return "unknown";
2968
+ const expr = node.getExpression();
2969
+ if (import_ts_morph8.Node.isPropertyAccessExpression(expr)) {
2970
+ const methodName = expr.getName();
2971
+ const receiver = expr.getExpression();
2972
+ if (methodName === "optional") {
2973
+ return `${zodAstToTs(receiver)} | undefined`;
2974
+ }
2975
+ if (methodName === "nullable") {
2976
+ return `${zodAstToTs(receiver)} | null`;
2977
+ }
2978
+ const args = node.getArguments();
2979
+ switch (methodName) {
2980
+ case "string":
2981
+ return "string";
2982
+ case "number":
2983
+ return "number";
2984
+ case "boolean":
2985
+ return "boolean";
2986
+ case "unknown":
2987
+ return "unknown";
2988
+ case "any":
2989
+ return "unknown";
2990
+ case "literal": {
2991
+ const lit = args[0];
2992
+ if (!lit) return "unknown";
2993
+ if (import_ts_morph8.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2994
+ if (import_ts_morph8.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2995
+ if (lit.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword) return "true";
2996
+ if (lit.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) return "false";
2997
+ return "unknown";
2998
+ }
2999
+ case "enum": {
3000
+ const arrArg = args[0];
3001
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3002
+ const members = arrArg.getElements().map(
3003
+ (el) => import_ts_morph8.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
3004
+ );
3005
+ return members.join(" | ");
3006
+ }
3007
+ case "array": {
3008
+ const inner = args[0];
3009
+ if (!inner) return "unknown";
3010
+ return `Array<${zodAstToTs(inner)}>`;
3011
+ }
3012
+ case "object": {
3013
+ const objArg = args[0];
3014
+ if (!objArg || !import_ts_morph8.Node.isObjectLiteralExpression(objArg)) return "unknown";
3015
+ const lines = [];
3016
+ for (const prop of objArg.getProperties()) {
3017
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
3018
+ const key = prop.getName();
3019
+ const valNode = prop.getInitializer();
3020
+ if (!valNode) continue;
3021
+ const tsType = zodAstToTs(valNode);
3022
+ const isOpt = isOptionalChain(valNode);
3023
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
3024
+ }
3025
+ return `{ ${lines.join("; ")} }`;
3026
+ }
3027
+ case "union": {
3028
+ const arrArg = args[0];
3029
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3030
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
3031
+ }
3032
+ case "record": {
3033
+ const valArg = args.length === 1 ? args[0] : args[1];
3034
+ if (!valArg) return "unknown";
3035
+ return `Record<string, ${zodAstToTs(valArg)}>`;
3036
+ }
3037
+ case "tuple": {
3038
+ const arrArg = args[0];
3039
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3040
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3041
+ }
3042
+ default:
3043
+ return "unknown";
3044
+ }
3045
+ }
3046
+ return "unknown";
3047
+ }
3048
+ function isOptionalChain(node) {
3049
+ if (!import_ts_morph8.Node.isCallExpression(node)) return false;
3050
+ const expr = node.getExpression();
3051
+ return import_ts_morph8.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3052
+ }
3053
+ function parseDefineContractCall(callExpr) {
3054
+ if (!import_ts_morph8.Node.isCallExpression(callExpr)) return null;
3055
+ const callee = callExpr.getExpression();
3056
+ const calleeName = import_ts_morph8.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph8.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
3057
+ if (calleeName !== "defineContract") return null;
3058
+ const args = callExpr.getArguments();
3059
+ const optsArg = args[0];
3060
+ if (!optsArg || !import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) return null;
3061
+ let query = null;
3062
+ let body = null;
3063
+ let response = "unknown";
3064
+ let bodyZodText = null;
3065
+ let queryZodText = null;
3066
+ for (const prop of optsArg.getProperties()) {
3067
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
3068
+ const propName = prop.getName();
3069
+ const val = prop.getInitializer();
3070
+ if (!val) continue;
3071
+ if (propName === "query") {
3072
+ query = zodAstToTs(val);
3073
+ queryZodText = val.getText();
3074
+ } else if (propName === "body") {
3075
+ body = zodAstToTs(val);
3076
+ bodyZodText = val.getText();
3077
+ } else if (propName === "response") {
3078
+ response = zodAstToTs(val);
3079
+ }
3080
+ }
3081
+ return { query, body, response, bodyZodText, queryZodText };
3082
+ }
3083
+
3084
+ // src/discovery/contracts-fast.ts
3085
+ async function discoverContractsFast(opts) {
3086
+ const { cwd, glob, tsconfig } = opts;
3087
+ const tsconfigPath = tsconfig ? (0, import_node_path12.resolve)(tsconfig) : (0, import_node_path12.join)(cwd, "tsconfig.json");
3088
+ let project;
3089
+ try {
3090
+ project = new import_ts_morph9.Project({
3091
+ tsConfigFilePath: tsconfigPath,
3092
+ skipAddingFilesFromTsConfig: true,
3093
+ skipLoadingLibFiles: true,
3094
+ skipFileDependencyResolution: true
3095
+ });
3096
+ } catch {
3097
+ project = new import_ts_morph9.Project({
3098
+ skipAddingFilesFromTsConfig: true,
3099
+ skipLoadingLibFiles: true,
3100
+ skipFileDependencyResolution: true,
3101
+ compilerOptions: {
3102
+ allowJs: true,
3103
+ resolveJsonModule: false,
3104
+ strict: false
3105
+ }
3106
+ });
3107
+ }
3108
+ const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3109
+ for (const f of files) {
3110
+ project.addSourceFileAtPath(f);
3111
+ }
3112
+ const routes = [];
3113
+ setDiscoveryContext(project, {
3114
+ projectRoot: cwd,
3115
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3116
+ });
3117
+ for (const sourceFile of project.getSourceFiles()) {
3118
+ routes.push(...extractFromSourceFile(sourceFile, project));
3119
+ }
3120
+ return routes;
3121
+ }
3122
+ function decoratorStringArg(decoratorExpr) {
3123
+ if (!decoratorExpr) return void 0;
3124
+ if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3125
+ if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
3126
+ const first = decoratorExpr.getElements()[0];
3127
+ if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
3128
+ }
3129
+ return void 0;
3130
+ }
3131
+ function deriveClassSegment(className) {
3132
+ const noSuffix = className.replace(/Controller$/, "");
3133
+ if (!noSuffix) {
3134
+ throw new Error(
3135
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3136
+ );
3137
+ }
3138
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3139
+ }
3140
+ function resolveRouteName(className, methodName, classAs, methodAs) {
3141
+ const classPortion = classAs ?? deriveClassSegment(className);
3142
+ const methodPortion = methodAs ?? methodName;
3143
+ return `${classPortion}.${methodPortion}`;
3144
+ }
3145
+ function joinPaths(prefix, suffix) {
3146
+ if (!prefix && !suffix) return "/";
3147
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3148
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3149
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3150
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3151
+ const combined = p + s;
3152
+ return combined === "" ? "/" : combined;
3153
+ }
3154
+ function extractParams(path) {
3155
+ const matches = path.matchAll(/:(\w+)/g);
3156
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3157
+ }
3604
3158
  var HTTP_METHOD_DECORATORS = {
3605
3159
  Get: "GET",
3606
3160
  Post: "POST",
@@ -3611,176 +3165,186 @@ var HTTP_METHOD_DECORATORS = {
3611
3165
  Head: "HEAD",
3612
3166
  All: "ALL"
3613
3167
  };
3168
+ function resolveVerb(method) {
3169
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3170
+ const httpDecorator = method.getDecorator(decoratorName);
3171
+ if (httpDecorator) {
3172
+ const httpArgs = httpDecorator.getArguments();
3173
+ const pathArg = httpArgs[0];
3174
+ return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3175
+ }
3176
+ }
3177
+ return null;
3178
+ }
3179
+ function readAsDecorator(node, label) {
3180
+ const asDecorator = node.getDecorator("As");
3181
+ if (!asDecorator) return void 0;
3182
+ const asName = decoratorStringArg(asDecorator.getArguments()[0]);
3183
+ if (!asName) {
3184
+ throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
3185
+ }
3186
+ return asName;
3187
+ }
3188
+ function buildRoute(args) {
3189
+ const {
3190
+ className,
3191
+ methodName,
3192
+ resolvedMethod,
3193
+ combinedPath,
3194
+ classAs,
3195
+ methodAs,
3196
+ sourceFile,
3197
+ seenNames,
3198
+ contractSource
3199
+ } = args;
3200
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3201
+ const qualifiedRef = `${className}.${methodName}`;
3202
+ const existing = seenNames.get(routeName);
3203
+ if (existing !== void 0) {
3204
+ throw new Error(
3205
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3206
+ );
3207
+ }
3208
+ seenNames.set(routeName, qualifiedRef);
3209
+ return {
3210
+ method: resolvedMethod,
3211
+ path: combinedPath,
3212
+ name: routeName,
3213
+ params: extractParams(combinedPath),
3214
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3215
+ contract: { contractSource }
3216
+ };
3217
+ }
3218
+ function extractContractRoute(args) {
3219
+ const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
3220
+ const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3221
+ if (!firstDecoratorArg) return null;
3222
+ let contractDef = null;
3223
+ let bodyZodRef = null;
3224
+ let queryZodRef = null;
3225
+ if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
3226
+ contractDef = parseDefineContractCall(firstDecoratorArg);
3227
+ } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
3228
+ const identName = firstDecoratorArg.getText();
3229
+ const varDecl = sourceFile.getVariableDeclaration(identName);
3230
+ if (!varDecl) {
3231
+ console.warn(
3232
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3233
+ );
3234
+ return null;
3235
+ }
3236
+ const initializer = varDecl.getInitializer();
3237
+ if (!initializer) return null;
3238
+ contractDef = parseDefineContractCall(initializer);
3239
+ if (contractDef && varDecl.isExported()) {
3240
+ const filePath = sourceFile.getFilePath();
3241
+ if (contractDef.body !== null) {
3242
+ bodyZodRef = { name: `${identName}.body`, filePath };
3243
+ }
3244
+ if (contractDef.query !== null) {
3245
+ queryZodRef = { name: `${identName}.query`, filePath };
3246
+ }
3247
+ }
3248
+ } else {
3249
+ console.warn(
3250
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3251
+ );
3252
+ return null;
3253
+ }
3254
+ if (!contractDef) return null;
3255
+ if (!verb) return null;
3256
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
3257
+ const methodName = method.getName();
3258
+ const classAs = readAsDecorator(cls, `class ${className}`);
3259
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3260
+ return buildRoute({
3261
+ className,
3262
+ methodName,
3263
+ resolvedMethod: verb.httpMethod,
3264
+ combinedPath: resolvedPath,
3265
+ classAs,
3266
+ methodAs,
3267
+ sourceFile,
3268
+ seenNames,
3269
+ contractSource: {
3270
+ query: contractDef.query,
3271
+ body: contractDef.body,
3272
+ response: contractDef.response,
3273
+ // Path A: capture both the importable ref and the raw text. The emitter
3274
+ // prefers inlining the text (client-safe — re-exporting from a controller
3275
+ // would drag server-only deps into the client bundle).
3276
+ bodyZodRef,
3277
+ bodyZodText: contractDef.bodyZodText,
3278
+ queryZodRef,
3279
+ queryZodText: contractDef.queryZodText
3280
+ }
3281
+ });
3282
+ }
3283
+ function extractDtoRoute(args) {
3284
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
3285
+ if (!verb) return null;
3286
+ const combined = joinPaths(prefix, verb.handlerPath);
3287
+ const methodName = method.getName();
3288
+ const classAs = readAsDecorator(cls, `class ${className}`);
3289
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3290
+ const dtoContract = extractDtoContract(method, sourceFile, project);
3291
+ return buildRoute({
3292
+ className,
3293
+ methodName,
3294
+ resolvedMethod: verb.httpMethod,
3295
+ combinedPath: combined,
3296
+ classAs,
3297
+ methodAs,
3298
+ sourceFile,
3299
+ seenNames,
3300
+ contractSource: {
3301
+ query: dtoContract?.query ?? null,
3302
+ body: dtoContract?.body ?? null,
3303
+ response: dtoContract?.response ?? "unknown",
3304
+ queryRef: dtoContract?.queryRef ?? null,
3305
+ bodyRef: dtoContract?.bodyRef ?? null,
3306
+ responseRef: dtoContract?.responseRef ?? null,
3307
+ filterFields: dtoContract?.filterFields ?? null,
3308
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3309
+ filterSource: dtoContract?.filterSource ?? null,
3310
+ formWarnings: dtoContract?.formWarnings ?? [],
3311
+ bodySchema: dtoContract?.bodySchema ?? null,
3312
+ querySchema: dtoContract?.querySchema ?? null
3313
+ }
3314
+ });
3315
+ }
3614
3316
  function extractFromSourceFile(sourceFile, project) {
3615
3317
  const routes = [];
3616
3318
  const seenNames = /* @__PURE__ */ new Map();
3617
- const classes = sourceFile.getClasses();
3618
- for (const cls of classes) {
3319
+ for (const cls of sourceFile.getClasses()) {
3619
3320
  const controllerDecorator = cls.getDecorator("Controller");
3620
3321
  if (!controllerDecorator) continue;
3621
- const controllerArgs = controllerDecorator.getArguments();
3622
- const firstArg3 = controllerArgs[0];
3623
- const prefix = decoratorStringArg(firstArg3) ?? "";
3322
+ const firstArg2 = controllerDecorator.getArguments()[0];
3323
+ const prefix = decoratorStringArg(firstArg2) ?? "";
3624
3324
  const className = cls.getName() ?? "Unknown";
3625
3325
  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
- }
3326
+ const verb = resolveVerb(method);
3638
3327
  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
- }
3328
+ const route = applyContractDecorator ? extractContractRoute({
3329
+ cls,
3330
+ method,
3331
+ applyContractDecorator,
3332
+ verb,
3333
+ prefix,
3334
+ className,
3335
+ sourceFile,
3336
+ seenNames
3337
+ }) : extractDtoRoute({
3338
+ cls,
3339
+ method,
3340
+ verb,
3341
+ prefix,
3342
+ className,
3343
+ sourceFile,
3344
+ project,
3345
+ seenNames
3346
+ });
3347
+ if (route) routes.push(route);
3784
3348
  }
3785
3349
  }
3786
3350
  return routes;
@@ -3789,7 +3353,7 @@ function extractFromSourceFile(sourceFile, project) {
3789
3353
  // src/watch/lock-file.ts
3790
3354
  var import_promises10 = require("fs/promises");
3791
3355
  var import_promises11 = require("fs/promises");
3792
- var import_node_path12 = require("path");
3356
+ var import_node_path13 = require("path");
3793
3357
  var LOCK_FILE = ".watcher.lock";
3794
3358
  function isProcessAlive(pid) {
3795
3359
  try {
@@ -3801,7 +3365,7 @@ function isProcessAlive(pid) {
3801
3365
  }
3802
3366
  async function acquireLock(outDir) {
3803
3367
  await (0, import_promises11.mkdir)(outDir, { recursive: true });
3804
- const lockPath = (0, import_node_path12.join)(outDir, LOCK_FILE);
3368
+ const lockPath = (0, import_node_path13.join)(outDir, LOCK_FILE);
3805
3369
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3806
3370
  try {
3807
3371
  const fd = await (0, import_promises10.open)(lockPath, "wx");
@@ -3841,7 +3405,7 @@ async function watch(config, onChange) {
3841
3405
  if (lock === null) {
3842
3406
  let holderPid = "unknown";
3843
3407
  try {
3844
- const raw = await (0, import_promises12.readFile)((0, import_node_path13.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3408
+ const raw = await (0, import_promises12.readFile)((0, import_node_path14.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3845
3409
  const data = JSON.parse(raw);
3846
3410
  if (data.pid !== void 0) holderPid = String(data.pid);
3847
3411
  } catch {
@@ -3869,7 +3433,7 @@ async function watch(config, onChange) {
3869
3433
  }
3870
3434
  let pagesDebounceTimer;
3871
3435
  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), {
3436
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, pagesGlob), {
3873
3437
  ignoreInitial: true,
3874
3438
  persistent: true,
3875
3439
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3895,7 +3459,7 @@ async function watch(config, onChange) {
3895
3459
  pagesWatcher.on("change", schedulePagesRegenerate);
3896
3460
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3897
3461
  let contractsDebounceTimer;
3898
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.contracts.glob), {
3462
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.contracts.glob), {
3899
3463
  ignoreInitial: true,
3900
3464
  persistent: true,
3901
3465
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3925,7 +3489,7 @@ async function watch(config, onChange) {
3925
3489
  contractsWatcher.on("add", scheduleContractsRegenerate);
3926
3490
  contractsWatcher.on("change", scheduleContractsRegenerate);
3927
3491
  contractsWatcher.on("unlink", scheduleContractsRegenerate);
3928
- const formsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.forms.watch), {
3492
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.forms.watch), {
3929
3493
  ignoreInitial: true,
3930
3494
  persistent: true,
3931
3495
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3951,8 +3515,57 @@ async function watch(config, onChange) {
3951
3515
  };
3952
3516
  }
3953
3517
 
3518
+ // src/ir/render-ts-type.ts
3519
+ function tsKey(name) {
3520
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
3521
+ }
3522
+ function renderTsType(node, ctx) {
3523
+ switch (node.kind) {
3524
+ case "string":
3525
+ return "string";
3526
+ case "number":
3527
+ return "number";
3528
+ case "boolean":
3529
+ return "boolean";
3530
+ case "date":
3531
+ return "Date";
3532
+ case "unknown":
3533
+ return "unknown";
3534
+ case "instanceof":
3535
+ return node.ctor;
3536
+ case "enum":
3537
+ return node.literals.join(" | ");
3538
+ case "literal":
3539
+ return node.raw;
3540
+ case "union":
3541
+ return node.options.map((o) => renderTsType(o, ctx)).join(" | ");
3542
+ case "array":
3543
+ return `Array<${renderTsType(node.element, ctx)}>`;
3544
+ case "optional":
3545
+ return `${renderTsType(node.inner, ctx)} | undefined`;
3546
+ case "annotated":
3547
+ return renderTsType(node.inner, ctx);
3548
+ case "object": {
3549
+ if (node.fields.length === 0) return node.passthrough ? "Record<string, unknown>" : "{}";
3550
+ const inner = node.fields.map((f) => {
3551
+ if (f.value.kind === "optional") {
3552
+ return `${tsKey(f.key)}?: ${renderTsType(f.value.inner, ctx)}`;
3553
+ }
3554
+ return `${tsKey(f.key)}: ${renderTsType(f.value, ctx)}`;
3555
+ }).join("; ");
3556
+ return `{ ${inner} }`;
3557
+ }
3558
+ case "ref":
3559
+ case "lazyRef": {
3560
+ if (ctx.recursive.has(node.name)) return ctx.typeNameFor(node.name);
3561
+ const target = ctx.named.get(node.name);
3562
+ return target ? renderTsType(target, ctx) : "unknown";
3563
+ }
3564
+ }
3565
+ }
3566
+
3954
3567
  // src/index.ts
3955
- var VERSION = "0.2.1";
3568
+ var VERSION = "0.4.0";
3956
3569
  // Annotate the CommonJS export names for ESM import in node:
3957
3570
  0 && (module.exports = {
3958
3571
  CodegenError,
@@ -3967,9 +3580,9 @@ var VERSION = "0.2.1";
3967
3580
  extractSchemaFromDto,
3968
3581
  generate,
3969
3582
  loadConfig,
3583
+ renderTsType,
3970
3584
  resolveAdapter,
3971
3585
  resolveConfig,
3972
- watch,
3973
- zodAdapter
3586
+ watch
3974
3587
  });
3975
3588
  //# sourceMappingURL=index.cjs.map