@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/cli/main.js CHANGED
@@ -20,112 +20,9 @@ var CodegenError = class extends Error {
20
20
  }
21
21
  };
22
22
 
23
- // src/adapters/zod.ts
24
- function toObjectKey(name) {
25
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
26
- }
27
- function messageSuffix(messageRaw2) {
28
- return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
29
- }
30
- function renderStringChecks(checks) {
31
- let out = "";
32
- for (const c of checks) {
33
- switch (c.check) {
34
- case "email":
35
- out += `.email(${messageSuffix(c.messageRaw)})`;
36
- break;
37
- case "url":
38
- out += `.url(${messageSuffix(c.messageRaw)})`;
39
- break;
40
- case "uuid":
41
- out += `.uuid(${messageSuffix(c.messageRaw)})`;
42
- break;
43
- case "regex":
44
- out += `.regex(${c.pattern})`;
45
- break;
46
- case "min":
47
- out += `.min(${c.value})`;
48
- break;
49
- case "max":
50
- out += `.max(${c.value})`;
51
- break;
52
- }
53
- }
54
- return out;
55
- }
56
- function render(node, ctx) {
57
- switch (node.kind) {
58
- case "string":
59
- return `z.string()${renderStringChecks(node.checks)}`;
60
- case "number": {
61
- let out = "z.number()";
62
- for (const c of node.checks) {
63
- if (c.check === "int") out += ".int()";
64
- else if (c.check === "positive") out += ".positive()";
65
- else if (c.check === "negative") out += ".negative()";
66
- else if (c.check === "min") out += `.min(${c.value})`;
67
- else if (c.check === "max") out += `.max(${c.value})`;
68
- }
69
- return out;
70
- }
71
- case "boolean":
72
- return "z.boolean()";
73
- case "date":
74
- return "z.coerce.date()";
75
- case "unknown":
76
- return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
77
- case "instanceof":
78
- return `z.instanceof(${node.ctor})`;
79
- case "enum":
80
- return `z.enum([${node.literals.join(", ")}])`;
81
- case "literal":
82
- return `z.literal(${node.raw})`;
83
- case "union":
84
- return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
85
- case "object": {
86
- if (node.fields.length === 0) {
87
- return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
88
- }
89
- const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
90
- return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
91
- }
92
- case "array":
93
- return `z.array(${render(node.element, ctx)})`;
94
- case "optional":
95
- return `${render(node.inner, ctx)}.optional()`;
96
- case "ref":
97
- return node.name;
98
- case "lazyRef":
99
- return `z.lazy(() => ${node.name})`;
100
- case "annotated": {
101
- const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
102
- return `${render(node.inner, ctx)} ${comments}`;
103
- }
104
- }
105
- }
106
- var zodAdapter = {
107
- name: "zod",
108
- importStatements(usage) {
109
- return usage.used ? ["import { z } from 'zod';"] : [];
110
- },
111
- render,
112
- inferType(schemaConst) {
113
- return `z.infer<typeof ${schemaConst}>`;
114
- },
115
- renderModule(mod) {
116
- const ctx = { named: mod.named };
117
- const namedNestedSchemas = /* @__PURE__ */ new Map();
118
- for (const [name, node] of mod.named) {
119
- namedNestedSchemas.set(name, render(node, ctx));
120
- }
121
- return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
122
- }
123
- };
124
-
125
23
  // src/adapters/registry.ts
126
24
  function resolveAdapter(option) {
127
25
  if (typeof option !== "string") return option;
128
- if (option === "zod") return zodAdapter;
129
26
  const pkg = `@dudousxd/nestjs-codegen-${option}`;
130
27
  const named = `${option}Adapter`;
131
28
  throw new ConfigError(
@@ -175,8 +72,21 @@ If this is intentional, move the file inside your project directory.`
175
72
  );
176
73
  }
177
74
  }
75
+ function validateUserConfig(userConfig) {
76
+ if (userConfig.validation == null) {
77
+ throw new ConfigError(
78
+ "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
79
+ );
80
+ }
81
+ if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
82
+ throw new ConfigError(
83
+ "Config validation failed: `pages.glob` must be a string when `pages` is set"
84
+ );
85
+ }
86
+ }
178
87
  function applyDefaults(userConfig, cwd) {
179
- const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-inertia");
88
+ validateUserConfig(userConfig);
89
+ const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
180
90
  const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
181
91
  let app = null;
182
92
  if (userConfig.app) {
@@ -194,7 +104,8 @@ function applyDefaults(userConfig, cwd) {
194
104
  }
195
105
  return {
196
106
  extensions: userConfig.extensions ?? [],
197
- validation: resolveAdapter(userConfig.validation ?? "zod"),
107
+ // Non-null: validateUserConfig() above throws when `validation` is absent.
108
+ validation: resolveAdapter(userConfig.validation),
198
109
  pages: userConfig.pages ? {
199
110
  glob: userConfig.pages.glob,
200
111
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
@@ -248,18 +159,12 @@ Run \`nestjs-codegen init\` to create a starter config.`
248
159
  `Config file must have a default export. Did you forget \`export default defineConfig({...})\`? (${configPath})`
249
160
  );
250
161
  }
251
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
252
- throw new ConfigError(
253
- `Config validation failed: \`pages.glob\` must be a string when \`pages\` is set (${configPath})`
254
- );
255
- }
256
162
  return applyDefaults(userConfig, resolvedCwd);
257
163
  }
258
164
 
259
165
  // src/generate.ts
260
166
  import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
261
- import { dirname as dirname2, join as join9 } from "path";
262
- import { Project as Project2 } from "ts-morph";
167
+ import { dirname as dirname2, join as join10 } from "path";
263
168
 
264
169
  // src/discovery/pages.ts
265
170
  import { readFile } from "fs/promises";
@@ -314,7 +219,8 @@ function extractPropsSource(source, exportName) {
314
219
  }
315
220
 
316
221
  // src/discovery/shared-props.ts
317
- import { Node, SyntaxKind } from "ts-morph";
222
+ import { join as join3 } from "path";
223
+ import { Node, Project, SyntaxKind } from "ts-morph";
318
224
  function discoverSharedProps(project, moduleEntry) {
319
225
  try {
320
226
  let sourceFile = project.getSourceFile(moduleEntry);
@@ -334,6 +240,31 @@ function discoverSharedProps(project, moduleEntry) {
334
240
  return null;
335
241
  }
336
242
  }
243
+ function discoverSharedPropsFromConfig(config) {
244
+ if (!config.app?.moduleEntry) return null;
245
+ try {
246
+ const tsconfigPath = config.app.tsconfig ?? join3(config.codegen.cwd, "tsconfig.json");
247
+ let project;
248
+ try {
249
+ project = new Project({
250
+ tsConfigFilePath: tsconfigPath,
251
+ skipAddingFilesFromTsConfig: true,
252
+ skipLoadingLibFiles: true,
253
+ skipFileDependencyResolution: true
254
+ });
255
+ } catch {
256
+ project = new Project({
257
+ skipAddingFilesFromTsConfig: true,
258
+ skipLoadingLibFiles: true,
259
+ skipFileDependencyResolution: true,
260
+ compilerOptions: { allowJs: true, strict: false }
261
+ });
262
+ }
263
+ return discoverSharedProps(project, config.app.moduleEntry);
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
337
268
  function findForRootCall(sourceFile) {
338
269
  const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
339
270
  for (const call of callExpressions) {
@@ -353,9 +284,9 @@ function findForRootCall(sourceFile) {
353
284
  function findShareInitializer(forRootCall) {
354
285
  if (!Node.isCallExpression(forRootCall)) return null;
355
286
  const args = forRootCall.getArguments();
356
- const firstArg3 = args[0];
357
- if (!firstArg3 || !Node.isObjectLiteralExpression(firstArg3)) return null;
358
- for (const prop of firstArg3.getProperties()) {
287
+ const firstArg2 = args[0];
288
+ if (!firstArg2 || !Node.isObjectLiteralExpression(firstArg2)) return null;
289
+ for (const prop of firstArg2.getProperties()) {
359
290
  if (Node.isPropertyAssignment(prop) && prop.getName() === "share") {
360
291
  return prop.getInitializer() ?? null;
361
292
  }
@@ -454,9 +385,9 @@ function extractFromReturnTypeAnnotation(typeNode) {
454
385
  const typeName = typeNode.getTypeName();
455
386
  if (Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
456
387
  const typeArgs = typeNode.getTypeArguments();
457
- const firstArg3 = typeArgs[0];
458
- if (firstArg3) {
459
- return extractFromReturnTypeAnnotation(firstArg3);
388
+ const firstArg2 = typeArgs[0];
389
+ if (firstArg2) {
390
+ return extractFromReturnTypeAnnotation(firstArg2);
460
391
  }
461
392
  return null;
462
393
  }
@@ -562,25 +493,14 @@ function inferExpressionType(node) {
562
493
 
563
494
  // src/emit/emit-api.ts
564
495
  import { mkdir, writeFile } from "fs/promises";
565
- import { isAbsolute as isAbsolute2, join as join3, relative as relative3 } from "path";
496
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
566
497
 
567
498
  // src/extension/registry.ts
568
- import { Project } from "ts-morph";
499
+ import { Project as Project2 } from "ts-morph";
569
500
  function resolveApiSlots(extensions) {
570
- let transport;
571
- let transportOwner;
572
501
  let layer;
573
502
  let layerOwner;
574
503
  for (const ext of extensions) {
575
- if (ext.apiTransport) {
576
- if (transport) {
577
- throw new CodegenError(
578
- `api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
579
- );
580
- }
581
- transport = ext.apiTransport;
582
- transportOwner = ext.name;
583
- }
584
504
  if (ext.apiClientLayer) {
585
505
  if (layer) {
586
506
  throw new CodegenError(
@@ -592,11 +512,22 @@ function resolveApiSlots(extensions) {
592
512
  }
593
513
  }
594
514
  return {
595
- ...transport ? { transport } : {},
596
515
  ...layer ? { layer } : {}
597
516
  };
598
517
  }
599
518
  var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
519
+ function mergeExclusive(target, incoming, {
520
+ owner,
521
+ describe
522
+ }) {
523
+ for (const [key, value] of incoming) {
524
+ const prev = target.get(key);
525
+ if (prev !== void 0) {
526
+ throw new CodegenError(describe(key, prev.owner, owner));
527
+ }
528
+ target.set(key, { value, owner });
529
+ }
530
+ }
600
531
  function createExtensionContext(config, getRoutes) {
601
532
  let project;
602
533
  return {
@@ -608,7 +539,7 @@ function createExtensionContext(config, getRoutes) {
608
539
  },
609
540
  project() {
610
541
  if (!project) {
611
- project = new Project({
542
+ project = new Project2({
612
543
  skipAddingFilesFromTsConfig: true,
613
544
  skipLoadingLibFiles: true,
614
545
  skipFileDependencyResolution: true,
@@ -641,29 +572,36 @@ async function collectEmittedFiles(extensions, ctx) {
641
572
  `Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
642
573
  );
643
574
  }
644
- const prev = owners.get(key);
645
- if (prev !== void 0) {
646
- throw new CodegenError(
647
- `Output file "${file.path}" is emitted by both "${prev}" and "${ext.name}". Two extensions cannot write the same file.`
648
- );
649
- }
650
- owners.set(key, ext.name);
575
+ mergeExclusive(owners, [[key, file]], {
576
+ owner: ext.name,
577
+ describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
578
+ });
651
579
  files.push(file);
652
580
  }
653
581
  }
654
582
  return files;
655
583
  }
656
584
 
585
+ // src/extension/types.ts
586
+ function requestShape(route) {
587
+ const cs = route.contract?.contractSource;
588
+ const isGet = route.method.toUpperCase() === "GET";
589
+ const isQuery = isGet || !!cs?.filterFields?.length;
590
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
591
+ const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
592
+ return { isGet, isQuery, hasBody, hasQuery };
593
+ }
594
+
657
595
  // src/emit/emit-api.ts
658
596
  async function emitApi(routes, outDir, opts = {}) {
659
597
  await mkdir(outDir, { recursive: true });
660
598
  const content = buildApiFile(routes, outDir, opts);
661
- await writeFile(join3(outDir, "api.ts"), content, "utf8");
599
+ await writeFile(join4(outDir, "api.ts"), content, "utf8");
662
600
  }
663
601
  function splitName(name) {
664
602
  return name.split(".");
665
603
  }
666
- function toObjectKey2(segment) {
604
+ function toObjectKey(segment) {
667
605
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
668
606
  return segment;
669
607
  }
@@ -770,7 +708,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
770
708
  const pad = " ".repeat(indent);
771
709
  const lines = [];
772
710
  for (const [key, node] of tree) {
773
- const objKey = toObjectKey2(key);
711
+ const objKey = toObjectKey(key);
774
712
  if (node.kind === "leaf") {
775
713
  const c = node;
776
714
  const method = c.method.toUpperCase();
@@ -796,15 +734,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
796
734
  return lines;
797
735
  }
798
736
  function buildRequestModel(c) {
799
- const isGet = c.method.toUpperCase() === "GET";
800
737
  const m = c.method.toLowerCase();
801
738
  const flat = JSON.stringify(c.name);
802
739
  const path = JSON.stringify(c.path);
803
740
  const TA = buildRouterTypeAccess(c.name);
804
741
  const withParams = hasPathParams(c.params);
805
- const hasBody = !!c.contractSource.bodyRef || c.contractSource.body != null && c.contractSource.body !== "never";
806
- const isQuery = isGet || !!c.contractSource.filterFields?.length;
807
- const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
742
+ const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
808
743
  const fields = [];
809
744
  if (withParams) fields.push(`params: ${TA}['params']`);
810
745
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -826,7 +761,6 @@ function buildRequestModel(c) {
826
761
  urlExpr,
827
762
  optsExpr,
828
763
  responseType: `${TA}['response']`,
829
- bodyType: `${TA}['body']`,
830
764
  queryKeyExpr: `[${flat}, input] as const`
831
765
  };
832
766
  }
@@ -874,7 +808,7 @@ function emitApiObjectBlock(tree, indent, p) {
874
808
  const pad = " ".repeat(indent);
875
809
  const lines = [];
876
810
  for (const [key, node] of tree) {
877
- const objKey = toObjectKey2(key);
811
+ const objKey = toObjectKey(key);
878
812
  if (node.kind === "branch") {
879
813
  lines.push(`${pad}${objKey}: {`);
880
814
  lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
@@ -882,30 +816,28 @@ function emitApiObjectBlock(tree, indent, p) {
882
816
  continue;
883
817
  }
884
818
  const req = buildRequestModel(node);
885
- const route = {
886
- method: node.method,
887
- path: node.path,
888
- name: node.name,
889
- params: node.params,
890
- contract: { contractSource: node.contractSource },
891
- ...node.controllerRef ? { controllerRef: node.controllerRef } : {}
819
+ const leaf = {
820
+ route: node.route,
821
+ request: req,
822
+ requestExpr: renderFetcherRequest(req)
892
823
  };
893
- const leaf = { route, request: req, requestExpr: "" };
894
- leaf.requestExpr = p.transport ? p.transport.renderRequest(leaf, p.ctx) : renderFetcherRequest(req);
895
- const members = {};
896
- if (p.layer) Object.assign(members, p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx));
824
+ const owned = /* @__PURE__ */ new Map();
825
+ if (p.layer) {
826
+ mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
827
+ owner: p.layer.name,
828
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
829
+ });
830
+ }
897
831
  for (const ext of p.memberExts) {
898
832
  const extra = ext.apiMembers?.(leaf, p.ctx);
899
833
  if (!extra) continue;
900
- for (const [name, value] of Object.entries(extra)) {
901
- if (name in members) {
902
- throw new Error(
903
- `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict at "${ext.name}").`
904
- );
905
- }
906
- members[name] = value;
907
- }
834
+ mergeExclusive(owned, Object.entries(extra), {
835
+ owner: ext.name,
836
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
837
+ });
908
838
  }
839
+ const members = {};
840
+ for (const [name, { value }] of owned) members[name] = value;
909
841
  lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
910
842
  }
911
843
  return lines;
@@ -914,10 +846,82 @@ function buildRouterTypeAccess(name) {
914
846
  const segments = splitName(name);
915
847
  return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
916
848
  }
849
+ var RESOLVER_HELPERS = [
850
+ // --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
851
+ "type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
852
+ " ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
853
+ " : P extends keyof R ? R[P] : never;",
854
+ "",
855
+ // --- ResolveByName: resolve a field from a dot-path name ---
856
+ "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
857
+ "",
858
+ // --- ResolveByPath: scan all leaves for matching method + url ---
859
+ // Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
860
+ "type _LeafValues<T> = T extends { method: string; url: string }",
861
+ " ? T",
862
+ " : T extends object ? _LeafValues<T[keyof T]> : never;",
863
+ "",
864
+ "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
865
+ " ? L extends { method: M; url: U }",
866
+ " ? Field extends keyof L ? L[Field] : never",
867
+ " : never",
868
+ " : never;",
869
+ ""
870
+ ];
871
+ var ROUTE_NAMESPACE = [
872
+ "export namespace Route {",
873
+ ' export type Response<K extends string> = ResolveByName<K, "response">;',
874
+ ' export type Body<K extends string> = ResolveByName<K, "body">;',
875
+ ' export type Query<K extends string> = ResolveByName<K, "query">;',
876
+ ' export type Params<K extends string> = ResolveByName<K, "params">;',
877
+ ' export type Error<K extends string> = ResolveByName<K, "error">;',
878
+ ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
879
+ " export type Request<K extends string> = {",
880
+ " body: Body<K>;",
881
+ " query: Query<K>;",
882
+ " params: Params<K>;",
883
+ " };",
884
+ "}",
885
+ ""
886
+ ];
887
+ var PATH_NAMESPACE = [
888
+ "export namespace Path {",
889
+ ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
890
+ ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
891
+ ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
892
+ ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
893
+ ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
894
+ ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
895
+ "}",
896
+ ""
897
+ ];
898
+ var EMPTY_ROUTE_NAMESPACE = [
899
+ "export namespace Route {",
900
+ " export type Response<K extends string> = never;",
901
+ " export type Body<K extends string> = never;",
902
+ " export type Query<K extends string> = never;",
903
+ " export type Params<K extends string> = never;",
904
+ " export type Error<K extends string> = never;",
905
+ " export type FilterFields<K extends string> = never;",
906
+ " export type Request<K extends string> = { body: never; query: never; params: never };",
907
+ "}",
908
+ ""
909
+ ];
910
+ var EMPTY_PATH_NAMESPACE = [
911
+ "export namespace Path {",
912
+ " export type Response<M extends string, U extends string> = never;",
913
+ " export type Body<M extends string, U extends string> = never;",
914
+ " export type Query<M extends string, U extends string> = never;",
915
+ " export type Params<M extends string, U extends string> = never;",
916
+ " export type Error<M extends string, U extends string> = never;",
917
+ " export type FilterFields<M extends string, U extends string> = never;",
918
+ "}",
919
+ ""
920
+ ];
917
921
  function buildApiFile(routes, outDir, opts = {}) {
918
922
  const fetcherImportPath = opts.fetcherImportPath;
919
923
  const extensions = opts.extensions ?? [];
920
- const { transport, layer } = resolveApiSlots(extensions);
924
+ const { layer } = resolveApiSlots(extensions);
921
925
  const memberExts = extensions.filter((e) => e.apiMembers);
922
926
  const headerExts = extensions.filter((e) => e.apiHeader);
923
927
  const contracted = routes.filter((r) => r.contract);
@@ -962,7 +966,6 @@ function buildApiFile(routes, outDir, opts = {}) {
962
966
  seenImports.add(imp);
963
967
  extImports.push(imp);
964
968
  };
965
- for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
966
969
  for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
967
970
  for (const ext of headerExts) {
968
971
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
@@ -1007,27 +1010,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1007
1010
  lines.push("}");
1008
1011
  lines.push("export type Api = ReturnType<typeof createApi>;");
1009
1012
  lines.push("");
1010
- lines.push("export namespace Route {");
1011
- lines.push(" export type Response<K extends string> = never;");
1012
- lines.push(" export type Body<K extends string> = never;");
1013
- lines.push(" export type Query<K extends string> = never;");
1014
- lines.push(" export type Params<K extends string> = never;");
1015
- lines.push(" export type Error<K extends string> = never;");
1016
- lines.push(" export type FilterFields<K extends string> = never;");
1017
- lines.push(
1018
- " export type Request<K extends string> = { body: never; query: never; params: never };"
1019
- );
1020
- lines.push("}");
1021
- lines.push("");
1022
- lines.push("export namespace Path {");
1023
- lines.push(" export type Response<M extends string, U extends string> = never;");
1024
- lines.push(" export type Body<M extends string, U extends string> = never;");
1025
- lines.push(" export type Query<M extends string, U extends string> = never;");
1026
- lines.push(" export type Params<M extends string, U extends string> = never;");
1027
- lines.push(" export type Error<M extends string, U extends string> = never;");
1028
- lines.push(" export type FilterFields<M extends string, U extends string> = never;");
1029
- lines.push("}");
1030
- lines.push("");
1013
+ lines.push(...EMPTY_ROUTE_NAMESPACE);
1014
+ lines.push(...EMPTY_PATH_NAMESPACE);
1031
1015
  return lines.join("\n");
1032
1016
  }
1033
1017
  const tree = /* @__PURE__ */ new Map();
@@ -1045,7 +1029,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1045
1029
  path: r.path,
1046
1030
  params: r.params,
1047
1031
  controllerRef: r.controllerRef,
1048
- contractSource: c.contractSource
1032
+ contractSource: c.contractSource,
1033
+ route: r
1049
1034
  };
1050
1035
  insertIntoTree(tree, segments, leaf, name);
1051
1036
  }
@@ -1058,7 +1043,6 @@ function buildApiFile(routes, outDir, opts = {}) {
1058
1043
  lines.push(" return {");
1059
1044
  lines.push(
1060
1045
  ...emitApiObjectBlock(tree, 4, {
1061
- ...transport ? { transport } : {},
1062
1046
  ...layer ? { layer } : {},
1063
1047
  memberExts,
1064
1048
  ctx
@@ -1069,61 +1053,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1069
1053
  lines.push("");
1070
1054
  lines.push("export type Api = ReturnType<typeof createApi>;");
1071
1055
  lines.push("");
1072
- lines.push("type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`");
1073
- lines.push(" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never");
1074
- lines.push(" : P extends keyof R ? R[P] : never;");
1075
- lines.push("");
1076
- lines.push(
1077
- "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
1078
- );
1079
- lines.push("");
1080
- lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
1081
- lines.push(" ? T");
1082
- lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
1083
- lines.push("");
1084
- lines.push(
1085
- "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
1086
- );
1087
- lines.push(" ? L extends { method: M; url: U }");
1088
- lines.push(" ? Field extends keyof L ? L[Field] : never");
1089
- lines.push(" : never");
1090
- lines.push(" : never;");
1091
- lines.push("");
1092
- lines.push("export namespace Route {");
1093
- lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
1094
- lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
1095
- lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
1096
- lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
1097
- lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
1098
- lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
1099
- lines.push(" export type Request<K extends string> = {");
1100
- lines.push(" body: Body<K>;");
1101
- lines.push(" query: Query<K>;");
1102
- lines.push(" params: Params<K>;");
1103
- lines.push(" };");
1104
- lines.push("}");
1105
- lines.push("");
1106
- lines.push("export namespace Path {");
1107
- lines.push(
1108
- ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
1109
- );
1110
- lines.push(
1111
- ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
1112
- );
1113
- lines.push(
1114
- ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
1115
- );
1116
- lines.push(
1117
- ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
1118
- );
1119
- lines.push(
1120
- ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
1121
- );
1122
- lines.push(
1123
- ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
1124
- );
1125
- lines.push("}");
1126
- lines.push("");
1056
+ lines.push(...RESOLVER_HELPERS);
1057
+ lines.push(...ROUTE_NAMESPACE);
1058
+ lines.push(...PATH_NAMESPACE);
1127
1059
  for (const ext of headerExts) {
1128
1060
  const statements = ext.apiHeader?.(ctx)?.statements;
1129
1061
  if (statements?.length) {
@@ -1135,7 +1067,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1135
1067
 
1136
1068
  // src/emit/emit-cache.ts
1137
1069
  import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
1138
- import { join as join4 } from "path";
1070
+ import { join as join5 } from "path";
1139
1071
  async function emitCache(pages, outDir) {
1140
1072
  await mkdir2(outDir, { recursive: true });
1141
1073
  const entries = await Promise.all(
@@ -1149,95 +1081,21 @@ async function emitCache(pages, outDir) {
1149
1081
  })
1150
1082
  );
1151
1083
  const cache = { pages: entries };
1152
- await writeFile2(join4(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1084
+ await writeFile2(join5(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1153
1085
  `, "utf8");
1154
1086
  }
1155
1087
 
1156
1088
  // src/emit/emit-forms.ts
1157
1089
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1158
- import { join as join5, relative as relative4 } from "path";
1090
+ import { join as join6, relative as relative4 } from "path";
1159
1091
  async function emitForms(routes, outDir, config, adapter) {
1160
1092
  if (config && config.enabled === false) return false;
1161
- if (adapter && adapter.name !== "zod") {
1162
- const content2 = buildFormsFileWithAdapter(routes, adapter);
1163
- if (content2 === null) return false;
1164
- await mkdir3(outDir, { recursive: true });
1165
- await writeFile3(join5(outDir, "forms.ts"), content2, "utf8");
1166
- return true;
1167
- }
1168
- const entries = collectFormEntries(routes);
1169
- if (entries.length === 0) return false;
1093
+ const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
1094
+ if (content === null) return false;
1170
1095
  await mkdir3(outDir, { recursive: true });
1171
- const content = buildFormsFile(entries, outDir, config);
1172
- await writeFile3(join5(outDir, "forms.ts"), content, "utf8");
1096
+ await writeFile3(join6(outDir, "forms.ts"), content, "utf8");
1173
1097
  return true;
1174
1098
  }
1175
- function buildFormsFileWithAdapter(routes, adapter) {
1176
- const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1177
- const methodNameCounts = /* @__PURE__ */ new Map();
1178
- for (const route of sorted) {
1179
- const cs = route.contract.contractSource;
1180
- if (!cs.bodySchema && !cs.querySchema) continue;
1181
- methodNameCounts.set(
1182
- deriveBaseName(route.name).method,
1183
- (methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
1184
- );
1185
- }
1186
- const named = /* @__PURE__ */ new Map();
1187
- const decls = [];
1188
- const mapEntries = [];
1189
- let used = false;
1190
- for (const route of sorted) {
1191
- const cs = route.contract.contractSource;
1192
- const { method, full } = deriveBaseName(route.name);
1193
- const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
1194
- const block = [];
1195
- if (cs.bodyZodText && !cs.bodySchema) {
1196
- block.push(
1197
- `// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1198
- );
1199
- }
1200
- let bodyConst;
1201
- if (cs.bodySchema) {
1202
- used = true;
1203
- const r = adapter.renderModule(cs.bodySchema);
1204
- for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
1205
- bodyConst = `${base}BodySchema`;
1206
- block.push(`export const ${bodyConst} = ${r.schemaText};`);
1207
- block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
1208
- }
1209
- if (cs.querySchema) {
1210
- used = true;
1211
- const r = adapter.renderModule(cs.querySchema);
1212
- for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
1213
- const queryConst = `${base}QuerySchema`;
1214
- block.push(`export const ${queryConst} = ${r.schemaText};`);
1215
- block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
1216
- }
1217
- if (block.length === 0) continue;
1218
- decls.push(`// ${route.name}`, ...block, "");
1219
- if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
1220
- }
1221
- if (!used) return null;
1222
- const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1223
- for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1224
- lines.push("");
1225
- if (named.size > 0) {
1226
- lines.push("// Hoisted nested schemas (shared across endpoints).");
1227
- for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
1228
- lines.push("");
1229
- }
1230
- lines.push(...decls);
1231
- lines.push("/** Route name \u2192 body schema map. */");
1232
- lines.push("export const formSchemas = {");
1233
- lines.push(...mapEntries);
1234
- lines.push("} as const;");
1235
- lines.push("");
1236
- return lines.join("\n");
1237
- }
1238
- function hasSchema(src) {
1239
- return !!src && (src.ref !== null || src.text !== null);
1240
- }
1241
1099
  function pascal(segment) {
1242
1100
  return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1243
1101
  }
@@ -1247,37 +1105,6 @@ function deriveBaseName(routeName) {
1247
1105
  const full = segments.map(pascal).join("");
1248
1106
  return { method, full };
1249
1107
  }
1250
- function collectFormEntries(routes) {
1251
- const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1252
- const methodNameCounts = /* @__PURE__ */ new Map();
1253
- const candidates = [];
1254
- for (const route of sorted) {
1255
- const cs = route.contract.contractSource;
1256
- const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
1257
- const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
1258
- if (!hasSchema(body) && !hasSchema(query)) continue;
1259
- const { method, full } = deriveBaseName(route.name);
1260
- methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1261
- candidates.push({ route, method, full });
1262
- }
1263
- const entries = [];
1264
- for (const { route, method, full } of candidates) {
1265
- const cs = route.contract.contractSource;
1266
- const collision = (methodNameCounts.get(method) ?? 0) > 1;
1267
- const baseName = collision ? full : method;
1268
- const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
1269
- const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
1270
- entries.push({
1271
- routeName: route.name,
1272
- baseName,
1273
- body: hasSchema(body) ? body : void 0,
1274
- query: hasSchema(query) ? query : void 0,
1275
- nestedSchemas: cs.formNestedSchemas ?? null,
1276
- warnings: cs.formWarnings ?? []
1277
- });
1278
- }
1279
- return entries;
1280
- }
1281
1108
  function relImport(outDir, filePath) {
1282
1109
  let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
1283
1110
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
@@ -1286,85 +1113,8 @@ function relImport(outDir, filePath) {
1286
1113
  function refRootIdentifier(refName) {
1287
1114
  return refName.split(".")[0] ?? refName;
1288
1115
  }
1289
- function buildFormsFile(entries, outDir, config) {
1290
- const zodImport = config?.zodImport ?? "zod";
1291
- const lines = [
1292
- "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1293
- `import { z } from '${zodImport}';`
1294
- ];
1295
- const importsByFile = /* @__PURE__ */ new Map();
1296
- const refAlias = /* @__PURE__ */ new Map();
1297
- for (const entry of entries) {
1298
- for (const src of [entry.body, entry.query]) {
1299
- if (src?.ref && !src.text) {
1300
- const root = refRootIdentifier(src.ref.name);
1301
- const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
1302
- set.add(root);
1303
- importsByFile.set(src.ref.filePath, set);
1304
- }
1305
- }
1306
- }
1307
- if (importsByFile.size > 0) {
1308
- const emitted = /* @__PURE__ */ new Set();
1309
- for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1310
- const relPath = relImport(outDir, filePath);
1311
- const specifiers = [];
1312
- for (const root of [...roots].sort()) {
1313
- if (emitted.has(root)) {
1314
- const alias = `${root}_${emitted.size}`;
1315
- specifiers.push(`${root} as ${alias}`);
1316
- emitted.add(alias);
1317
- refAlias.set(`${filePath}\0${root}`, alias);
1318
- } else {
1319
- specifiers.push(root);
1320
- emitted.add(root);
1321
- refAlias.set(`${filePath}\0${root}`, root);
1322
- }
1323
- }
1324
- lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1325
- }
1326
- }
1327
- lines.push("");
1328
- const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1329
- if (globalSchemas.size > 0) {
1330
- lines.push("// Hoisted nested schemas (shared across endpoints).");
1331
- for (const [name, text] of globalSchemas) {
1332
- lines.push(`const ${name} = ${text};`);
1333
- }
1334
- lines.push("");
1335
- }
1336
- const mapEntries = [];
1337
- for (const entry of entries) {
1338
- lines.push(`// ${entry.routeName}`);
1339
- if (entry.warnings && entry.warnings.length > 0) {
1340
- for (const w of entry.warnings) {
1341
- lines.push(`// warning: ${w}`);
1342
- }
1343
- }
1344
- const rename = renamesByEntry.get(entry) ?? null;
1345
- if (entry.body) {
1346
- const schemaName = `${entry.baseName}BodySchema`;
1347
- const typeName = `${entry.baseName}Body`;
1348
- const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
1349
- lines.push(`export const ${schemaName} = ${text};`);
1350
- lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
1351
- mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
1352
- }
1353
- if (entry.query) {
1354
- const schemaName = `${entry.baseName}QuerySchema`;
1355
- const typeName = `${entry.baseName}Query`;
1356
- const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
1357
- lines.push(`export const ${schemaName} = ${text};`);
1358
- lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
1359
- }
1360
- lines.push("");
1361
- }
1362
- lines.push("/** Route name \u2192 body schema map. */");
1363
- lines.push("export const formSchemas = {");
1364
- lines.push(...mapEntries);
1365
- lines.push("} as const;");
1366
- lines.push("");
1367
- return lines.join("\n");
1116
+ function hasSource(src) {
1117
+ return !!(src.schema || src.zodText || src.zodRef);
1368
1118
  }
1369
1119
  function applyRenames(text, renames) {
1370
1120
  if (!renames || renames.size === 0) return text;
@@ -1430,38 +1180,200 @@ function planNestedSchemas(entries) {
1430
1180
  }
1431
1181
  return { globalSchemas, renamesByEntry };
1432
1182
  }
1433
- function renderSchema(src, outDir, refAlias) {
1434
- if (src.text) return src.text;
1435
- if (src.ref) {
1436
- const root = refRootIdentifier(src.ref.name);
1437
- const alias = refAlias.get(`${src.ref.filePath}\0${root}`) ?? root;
1438
- const member = src.ref.name.slice(root.length);
1439
- return `${alias}${member}`;
1183
+ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1184
+ const acceptsRawZod = adapter.acceptsRawZodSource === true;
1185
+ const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1186
+ const methodNameCounts = /* @__PURE__ */ new Map();
1187
+ const candidates = [];
1188
+ for (const route of sorted) {
1189
+ const cs = route.contract.contractSource;
1190
+ const body = {
1191
+ schema: cs.bodySchema ?? null,
1192
+ zodText: cs.bodyZodText ?? null,
1193
+ zodRef: cs.bodyZodRef ?? null
1194
+ };
1195
+ const query = {
1196
+ schema: cs.querySchema ?? null,
1197
+ zodText: cs.queryZodText ?? null,
1198
+ zodRef: cs.queryZodRef ?? null
1199
+ };
1200
+ if (!hasSource(body) && !hasSource(query)) continue;
1201
+ const { method, full } = deriveBaseName(route.name);
1202
+ methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1203
+ candidates.push({
1204
+ routeName: route.name,
1205
+ baseName: full,
1206
+ // resolved below
1207
+ body: hasSource(body) ? body : void 0,
1208
+ query: hasSource(query) ? query : void 0,
1209
+ nestedSchemas: cs.formNestedSchemas ?? null,
1210
+ warnings: cs.formWarnings ?? []
1211
+ });
1440
1212
  }
1441
- return "z.unknown()";
1442
- }
1443
-
1444
- // src/emit/emit-index.ts
1445
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1446
- import { join as join6 } from "path";
1447
- async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1448
- await mkdir4(outDir, { recursive: true });
1449
- const exports = ["export * from './pages.js';", "export * from './routes.js';"];
1450
- if (hasContracts) {
1451
- exports.push("export * from './api.js';");
1213
+ const entries = candidates.map((c) => {
1214
+ const { method, full } = deriveBaseName(c.routeName);
1215
+ const collision = (methodNameCounts.get(method) ?? 0) > 1;
1216
+ return { ...c, baseName: collision ? full : method };
1217
+ });
1218
+ if (entries.length === 0) return null;
1219
+ const importsByFile = /* @__PURE__ */ new Map();
1220
+ const refAlias = /* @__PURE__ */ new Map();
1221
+ for (const entry of entries) {
1222
+ for (const src of [entry.body, entry.query]) {
1223
+ if (src?.zodRef && !src.zodText && !src.schema) {
1224
+ const root = refRootIdentifier(src.zodRef.name);
1225
+ const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
1226
+ set.add(root);
1227
+ importsByFile.set(src.zodRef.filePath, set);
1228
+ }
1229
+ }
1452
1230
  }
1453
- if (hasForms) {
1454
- exports.push("export * from './forms.js';");
1231
+ const importLines = [];
1232
+ if (importsByFile.size > 0) {
1233
+ const emitted = /* @__PURE__ */ new Set();
1234
+ for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1235
+ const relPath = relImport(outDir, filePath);
1236
+ const specifiers = [];
1237
+ for (const root of [...roots].sort()) {
1238
+ if (emitted.has(root)) {
1239
+ const alias = `${root}_${emitted.size}`;
1240
+ specifiers.push(`${root} as ${alias}`);
1241
+ emitted.add(alias);
1242
+ refAlias.set(`${filePath}\0${root}`, alias);
1243
+ } else {
1244
+ specifiers.push(root);
1245
+ emitted.add(root);
1246
+ refAlias.set(`${filePath}\0${root}`, root);
1247
+ }
1248
+ }
1249
+ importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1250
+ }
1455
1251
  }
1456
- const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
1457
- "\n"
1458
- );
1459
- await writeFile4(join6(outDir, "index.d.ts"), content, "utf8");
1460
- }
1461
-
1462
- // src/emit/emit-pages.ts
1252
+ const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1253
+ const irNamed = /* @__PURE__ */ new Map();
1254
+ const irTypeAliases = /* @__PURE__ */ new Map();
1255
+ const irAnnotations = /* @__PURE__ */ new Map();
1256
+ const decls = [];
1257
+ const mapEntries = [];
1258
+ let used = false;
1259
+ const renderSource = (src, rename) => {
1260
+ if (src.schema) {
1261
+ const r = adapter.renderModule(src.schema);
1262
+ for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1263
+ if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
1264
+ if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
1265
+ return { text: r.schemaText };
1266
+ }
1267
+ if (src.zodText) {
1268
+ if (!acceptsRawZod) {
1269
+ return {
1270
+ text: "",
1271
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1272
+ };
1273
+ }
1274
+ return { text: applyRenames(src.zodText, rename) };
1275
+ }
1276
+ if (src.zodRef) {
1277
+ if (!acceptsRawZod) {
1278
+ return {
1279
+ text: "",
1280
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1281
+ };
1282
+ }
1283
+ const root = refRootIdentifier(src.zodRef.name);
1284
+ const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
1285
+ const member = src.zodRef.name.slice(root.length);
1286
+ return { text: `${alias}${member}` };
1287
+ }
1288
+ return null;
1289
+ };
1290
+ for (const entry of entries) {
1291
+ const block = [];
1292
+ const rename = renamesByEntry.get(entry) ?? null;
1293
+ let bodyConst;
1294
+ if (entry.warnings && entry.warnings.length > 0) {
1295
+ for (const w of entry.warnings) block.push(`// warning: ${w}`);
1296
+ }
1297
+ if (entry.body) {
1298
+ const rendered = renderSource(entry.body, rename);
1299
+ if (rendered?.warn) {
1300
+ block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
1301
+ } else if (rendered) {
1302
+ used = true;
1303
+ bodyConst = `${entry.baseName}BodySchema`;
1304
+ block.push(`export const ${bodyConst} = ${rendered.text};`);
1305
+ block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
1306
+ }
1307
+ }
1308
+ if (entry.query) {
1309
+ const rendered = renderSource(entry.query, rename);
1310
+ if (rendered?.warn) {
1311
+ block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
1312
+ } else if (rendered) {
1313
+ used = true;
1314
+ const queryConst = `${entry.baseName}QuerySchema`;
1315
+ block.push(`export const ${queryConst} = ${rendered.text};`);
1316
+ block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
1317
+ }
1318
+ }
1319
+ if (block.length === 0) continue;
1320
+ decls.push(`// ${entry.routeName}`, ...block, "");
1321
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
1322
+ }
1323
+ if (!used) return null;
1324
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1325
+ if (acceptsRawZod) {
1326
+ const zodImport = config?.zodImport ?? "zod";
1327
+ lines.push(`import { z } from '${zodImport}';`);
1328
+ } else {
1329
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1330
+ }
1331
+ lines.push(...importLines);
1332
+ lines.push("");
1333
+ const allNested = /* @__PURE__ */ new Map();
1334
+ for (const [n, t] of globalSchemas) allNested.set(n, t);
1335
+ for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1336
+ if (allNested.size > 0) {
1337
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
1338
+ for (const [n, alias] of irTypeAliases) {
1339
+ if (allNested.has(n)) lines.push(`${alias};`);
1340
+ }
1341
+ for (const [n, t] of allNested) {
1342
+ const annotation = irAnnotations.get(n);
1343
+ lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
1344
+ }
1345
+ lines.push("");
1346
+ }
1347
+ lines.push(...decls);
1348
+ lines.push("/** Route name \u2192 body schema map. */");
1349
+ lines.push("export const formSchemas = {");
1350
+ lines.push(...mapEntries);
1351
+ lines.push("} as const;");
1352
+ lines.push("");
1353
+ return lines.join("\n");
1354
+ }
1355
+
1356
+ // src/emit/emit-index.ts
1357
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1358
+ import { join as join7 } from "path";
1359
+ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1360
+ await mkdir4(outDir, { recursive: true });
1361
+ const exports = ["export * from './pages.js';", "export * from './routes.js';"];
1362
+ if (hasContracts) {
1363
+ exports.push("export * from './api.js';");
1364
+ }
1365
+ if (hasForms) {
1366
+ exports.push("export * from './forms.js';");
1367
+ }
1368
+ const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
1369
+ "\n"
1370
+ );
1371
+ await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
1372
+ }
1373
+
1374
+ // src/emit/emit-pages.ts
1463
1375
  import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
1464
- import { join as join7, relative as relative5 } from "path";
1376
+ import { join as join8, relative as relative5 } from "path";
1465
1377
  async function emitPages(pages, outDir, _options = {}) {
1466
1378
  await mkdir5(outDir, { recursive: true });
1467
1379
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -1482,7 +1394,7 @@ ${augBody}
1482
1394
  }
1483
1395
  ${sharedPropsBlock}}
1484
1396
  `;
1485
- await writeFile5(join7(outDir, "pages.d.ts"), content, "utf8");
1397
+ await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
1486
1398
  }
1487
1399
  function buildSharedPropsBlock(sharedProps) {
1488
1400
  if (!sharedProps) return "";
@@ -1513,11 +1425,11 @@ function needsQuotes(name) {
1513
1425
 
1514
1426
  // src/emit/emit-routes.ts
1515
1427
  import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1516
- import { join as join8 } from "path";
1428
+ import { join as join9 } from "path";
1517
1429
  async function emitRoutes(routes, outDir) {
1518
1430
  await mkdir6(outDir, { recursive: true });
1519
1431
  const content = buildRoutesFile(routes);
1520
- await writeFile6(join8(outDir, "routes.ts"), content, "utf8");
1432
+ await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
1521
1433
  }
1522
1434
  function buildRoutesFile(routes) {
1523
1435
  if (routes.length === 0) {
@@ -1645,30 +1557,7 @@ async function generate(config, inputRoutes = []) {
1645
1557
  propsExport: pagesConfig.propsExport,
1646
1558
  componentNameStrategy: pagesConfig.componentNameStrategy
1647
1559
  });
1648
- let sharedProps = null;
1649
- if (config.app?.moduleEntry) {
1650
- try {
1651
- const tsconfigPath = config.app.tsconfig ?? join9(config.codegen.cwd, "tsconfig.json");
1652
- let project;
1653
- try {
1654
- project = new Project2({
1655
- tsConfigFilePath: tsconfigPath,
1656
- skipAddingFilesFromTsConfig: true,
1657
- skipLoadingLibFiles: true,
1658
- skipFileDependencyResolution: true
1659
- });
1660
- } catch {
1661
- project = new Project2({
1662
- skipAddingFilesFromTsConfig: true,
1663
- skipLoadingLibFiles: true,
1664
- skipFileDependencyResolution: true,
1665
- compilerOptions: { allowJs: true, strict: false }
1666
- });
1667
- }
1668
- sharedProps = discoverSharedProps(project, config.app.moduleEntry);
1669
- } catch {
1670
- }
1671
- }
1560
+ const sharedProps = discoverSharedPropsFromConfig(config);
1672
1561
  await emitPages(pages, config.codegen.outDir, {
1673
1562
  propsExport: pagesConfig.propsExport,
1674
1563
  sharedProps
@@ -1692,7 +1581,7 @@ async function generate(config, inputRoutes = []) {
1692
1581
  if (extensions.length > 0) {
1693
1582
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1694
1583
  for (const file of extraFiles) {
1695
- const dest = join9(config.codegen.outDir, file.path);
1584
+ const dest = join10(config.codegen.outDir, file.path);
1696
1585
  await mkdir7(dirname2(dest), { recursive: true });
1697
1586
  await writeFile7(dest, file.contents, "utf8");
1698
1587
  }
@@ -1701,15 +1590,20 @@ async function generate(config, inputRoutes = []) {
1701
1590
 
1702
1591
  // src/watch/watcher.ts
1703
1592
  import { readFile as readFile3 } from "fs/promises";
1704
- import { join as join12 } from "path";
1593
+ import { join as join13 } from "path";
1705
1594
  import chokidar from "chokidar";
1706
1595
 
1707
1596
  // src/discovery/contracts-fast.ts
1708
- import { join as join10, resolve as resolve3 } from "path";
1597
+ import { join as join11, resolve as resolve3 } from "path";
1709
1598
  import fg2 from "fast-glob";
1710
1599
  import {
1711
- Node as Node7,
1712
- Project as Project3,
1600
+ Node as Node8,
1601
+ Project as Project3
1602
+ } from "ts-morph";
1603
+
1604
+ // src/discovery/dto-type-resolver.ts
1605
+ import {
1606
+ Node as Node6,
1713
1607
  SyntaxKind as SyntaxKind3
1714
1608
  } from "ts-morph";
1715
1609
 
@@ -1724,20 +1618,13 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
1724
1618
  import {
1725
1619
  Node as Node2
1726
1620
  } from "ts-morph";
1727
- var _ctx = { projectRoot: "", tsconfigPaths: null };
1728
- function setDiscoveryContext(ctx) {
1729
- const prev = _ctx;
1730
- _ctx = ctx;
1731
- return prev;
1732
- }
1733
- function restoreDiscoveryContext(ctx) {
1734
- _ctx = ctx;
1621
+ var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1622
+ var _ctxByProject = /* @__PURE__ */ new WeakMap();
1623
+ function setDiscoveryContext(project, ctx) {
1624
+ _ctxByProject.set(project, ctx);
1735
1625
  }
1736
- function _projectRoot() {
1737
- return _ctx.projectRoot;
1738
- }
1739
- function _tsconfigPaths() {
1740
- return _ctx.tsconfigPaths;
1626
+ function _ctxFor(project) {
1627
+ return _ctxByProject.get(project) ?? _EMPTY_CTX;
1741
1628
  }
1742
1629
  var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
1743
1630
  function dbg(...args) {
@@ -1779,7 +1666,7 @@ function findTypeInFile(name, file) {
1779
1666
  }
1780
1667
  return null;
1781
1668
  }
1782
- function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1669
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1783
1670
  if (moduleSpecifier.startsWith(".")) {
1784
1671
  const dir = dirname3(sourceFile.getFilePath());
1785
1672
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
@@ -1789,8 +1676,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1789
1676
  resolve2(dir, moduleSpecifier, "index.ts")
1790
1677
  ];
1791
1678
  }
1792
- const baseUrl = _projectRoot();
1793
- const tsconfigPaths = _tsconfigPaths();
1679
+ const ctx = _ctxFor(project);
1680
+ const baseUrl = ctx.projectRoot;
1681
+ const tsconfigPaths = ctx.tsconfigPaths;
1794
1682
  dbg(
1795
1683
  "resolveModuleSpecifier",
1796
1684
  moduleSpecifier,
@@ -1936,7 +1824,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1936
1824
  if (!namedImport) continue;
1937
1825
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
1938
1826
  if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
1939
- const tsconfigPaths = _tsconfigPaths();
1827
+ const tsconfigPaths = _ctxFor(project).tsconfigPaths;
1940
1828
  const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
1941
1829
  const prefix = p.replace("*", "");
1942
1830
  return moduleSpecifier.startsWith(prefix);
@@ -2005,10 +1893,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
2005
1893
  depth: 0
2006
1894
  };
2007
1895
  const root = buildObject(classDecl, sourceFile, ctx);
2008
- for (const schemaName of ctx.recursiveSchemas) {
2009
- ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
2010
- }
2011
- return { root, named: ctx.named, warnings: ctx.warnings };
1896
+ return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
2012
1897
  }
2013
1898
  function buildObject(classDecl, classFile, ctx) {
2014
1899
  const props = classDecl.getProperties();
@@ -2028,7 +1913,7 @@ function buildProperty(prop, classFile, ctx) {
2028
1913
  const dec = (n) => decorators.get(n);
2029
1914
  const typeNode = prop.getTypeNode();
2030
1915
  const typeText = typeNode?.getText() ?? "unknown";
2031
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
1916
+ const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
2032
1917
  const typeRefName = resolveTypeFactoryName(dec("Type"));
2033
1918
  if (has("ValidateNested") || typeRefName) {
2034
1919
  const childName = typeRefName ?? singularClassName(typeText);
@@ -2159,18 +2044,27 @@ function baseFromType(typeText, isArrayType) {
2159
2044
  }
2160
2045
  }
2161
2046
  function buildNestedReference(className, fromFile, ctx) {
2162
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2047
+ if (ctx.visiting.has(className)) {
2163
2048
  const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2164
2049
  ctx.emittedClasses.set(className, reserved);
2165
2050
  ctx.recursiveSchemas.add(reserved);
2166
2051
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2167
2052
  ctx.warnedDecorators.add(`recursive:${reserved}`);
2168
- const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
2053
+ const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
2169
2054
  ctx.warnings.push(msg);
2170
2055
  console.warn(`[nestjs-codegen] ${msg}`);
2171
2056
  }
2172
2057
  return { kind: "lazyRef", name: reserved };
2173
2058
  }
2059
+ if (ctx.depth >= 8) {
2060
+ if (!ctx.warnedDecorators.has(`deep:${className}`)) {
2061
+ ctx.warnedDecorators.add(`deep:${className}`);
2062
+ const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
2063
+ ctx.warnings.push(msg);
2064
+ console.warn(`[nestjs-codegen] ${msg}`);
2065
+ }
2066
+ return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2067
+ }
2174
2068
  const existing = ctx.emittedClasses.get(className);
2175
2069
  if (existing) return { kind: "ref", name: existing };
2176
2070
  const schemaName = aliasFor(className, ctx);
@@ -2289,465 +2183,135 @@ function inSchemaFromDecorator(decorator) {
2289
2183
  return null;
2290
2184
  }
2291
2185
 
2292
- // src/discovery/dto-to-zod.ts
2186
+ // src/discovery/filter-for.ts
2293
2187
  import {
2294
- Node as Node4
2188
+ Node as Node5
2295
2189
  } from "ts-morph";
2296
- var KNOWN_DECORATORS2 = /* @__PURE__ */ new Set([
2297
- "IsString",
2298
- "IsNumber",
2299
- "IsInt",
2300
- "IsBoolean",
2301
- "IsDate",
2302
- "IsEmail",
2303
- "IsUrl",
2304
- "IsUUID",
2305
- "MinLength",
2306
- "MaxLength",
2307
- "Length",
2308
- "Min",
2309
- "Max",
2310
- "IsPositive",
2311
- "IsNegative",
2312
- "Matches",
2313
- "IsEnum",
2314
- "IsIn",
2315
- "IsOptional",
2316
- "IsNotEmpty",
2317
- "IsArray",
2318
- "ValidateNested",
2319
- "Type",
2320
- "IsObject",
2321
- "Allow",
2322
- "IsDefined"
2323
- ]);
2324
- function extractZodFromDto(classDecl, sourceFile, project) {
2325
- const ctx = {
2326
- sourceFile,
2327
- project,
2328
- namedNestedSchemas: /* @__PURE__ */ new Map(),
2329
- warnings: [],
2330
- warnedDecorators: /* @__PURE__ */ new Set(),
2331
- emittedClasses: /* @__PURE__ */ new Map(),
2332
- visiting: /* @__PURE__ */ new Set(),
2333
- recursiveSchemas: /* @__PURE__ */ new Set(),
2334
- depth: 0
2335
- };
2336
- const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
2337
- for (const schemaName of ctx.recursiveSchemas) {
2338
- ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
2339
- }
2340
- return {
2341
- schemaText,
2342
- namedNestedSchemas: ctx.namedNestedSchemas,
2343
- warnings: ctx.warnings
2344
- };
2190
+
2191
+ // src/discovery/filter-field-types.ts
2192
+ import {
2193
+ Node as Node4,
2194
+ SyntaxKind as SyntaxKind2
2195
+ } from "ts-morph";
2196
+
2197
+ // src/discovery/enum-resolution.ts
2198
+ function resolveEnumValues(name, sourceFile, project) {
2199
+ const resolved = findType(name, sourceFile, project);
2200
+ if (!resolved || resolved.kind !== "enum") return null;
2201
+ let numeric = true;
2202
+ const values = resolved.members.map((m) => {
2203
+ const parsed = JSON.parse(m);
2204
+ if (typeof parsed === "string") numeric = false;
2205
+ return String(parsed);
2206
+ });
2207
+ if (values.length === 0) return null;
2208
+ return { values, numeric };
2345
2209
  }
2346
- function buildObjectSchema(classDecl, classFile, ctx) {
2347
- const props = classDecl.getProperties();
2348
- if (props.length === 0) {
2349
- return "z.object({}).passthrough()";
2350
- }
2351
- const fields = [];
2352
- for (const prop of props) {
2353
- const name = prop.getName();
2354
- const expr = buildPropertySchema(prop, classFile, ctx);
2355
- fields.push(`${toObjectKey3(name)}: ${expr}`);
2356
- }
2357
- return `z.object({ ${fields.join(", ")} })`;
2210
+
2211
+ // src/discovery/filter-field-types.ts
2212
+ var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2213
+ var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2214
+ var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2215
+ var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2216
+ var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2217
+ function classifyTypeKeyword(raw) {
2218
+ const t = raw.toLowerCase();
2219
+ if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2220
+ if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2221
+ if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2222
+ if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2223
+ if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2224
+ return null;
2358
2225
  }
2359
- function toObjectKey3(name) {
2360
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2226
+ function markNullable(r, nullable) {
2227
+ return nullable ? { ...r, nullable: true } : r;
2361
2228
  }
2362
- function buildPropertySchema(prop, classFile, ctx) {
2363
- const decorators = /* @__PURE__ */ new Map();
2364
- for (const d of prop.getDecorators()) decorators.set(d.getName(), d);
2365
- const has = (n) => decorators.has(n);
2366
- const dec = (n) => decorators.get(n);
2367
- const typeNode = prop.getTypeNode();
2368
- const typeText = typeNode?.getText() ?? "unknown";
2369
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
2370
- const comments = [];
2371
- const typeRefName = resolveTypeFactoryName2(dec("Type"));
2372
- if (has("ValidateNested") || typeRefName) {
2373
- const childName = typeRefName ?? singularClassName2(typeText);
2374
- if (childName) {
2375
- const childExpr = buildNestedReference2(childName, classFile, ctx);
2376
- const wrapArray = has("IsArray") || isArrayType;
2377
- let expr2 = wrapArray ? `z.array(${childExpr})` : childExpr;
2378
- expr2 = applyPresence2(expr2, decorators);
2379
- return expr2;
2380
- }
2381
- }
2382
- let base = baseFromType2(typeText, isArrayType, ctx, classFile);
2383
- const refinements = [];
2384
- if (has("IsString")) base = "z.string()";
2385
- if (has("IsBoolean")) base = "z.boolean()";
2386
- if (has("IsDate")) base = "z.coerce.date()";
2387
- if (has("IsNumber")) base = "z.number()";
2388
- if (has("IsInt")) base = "z.number().int()";
2389
- if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
2390
- if (has("Allow")) base = "z.unknown()";
2391
- if (has("IsEmail")) {
2392
- base = ensureStringBase(base);
2393
- refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
2394
- }
2395
- if (has("IsUrl")) {
2396
- base = ensureStringBase(base);
2397
- refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
2398
- }
2399
- if (has("IsUUID")) {
2400
- base = ensureStringBase(base);
2401
- refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
2402
- }
2403
- if (has("Matches")) {
2404
- const re = firstArgText2(dec("Matches"));
2405
- if (re) {
2406
- base = ensureStringBase(base);
2407
- refinements.push(`.regex(${re})`);
2408
- }
2409
- }
2410
- if (has("MinLength")) {
2411
- const n = numericArg2(dec("MinLength"));
2412
- if (n !== null) refinements.push(`.min(${n})`);
2413
- }
2414
- if (has("MaxLength")) {
2415
- const n = numericArg2(dec("MaxLength"));
2416
- if (n !== null) refinements.push(`.max(${n})`);
2417
- }
2418
- if (has("Length")) {
2419
- const [min, max] = numericArgs2(dec("Length"));
2420
- if (min !== null) refinements.push(`.min(${min})`);
2421
- if (max !== null) refinements.push(`.max(${max})`);
2422
- }
2423
- if (has("Min")) {
2424
- const n = numericArg2(dec("Min"));
2425
- if (n !== null) refinements.push(`.min(${n})`);
2426
- }
2427
- if (has("Max")) {
2428
- const n = numericArg2(dec("Max"));
2429
- if (n !== null) refinements.push(`.max(${n})`);
2430
- }
2431
- if (has("IsPositive")) refinements.push(".positive()");
2432
- if (has("IsNegative")) refinements.push(".negative()");
2433
- if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
2434
- if (has("IsEnum")) {
2435
- const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
2436
- if (enumExpr) base = enumExpr;
2437
- }
2438
- if (has("IsIn")) {
2439
- const inExpr = inSchemaFromDecorator2(dec("IsIn"));
2440
- if (inExpr) base = inExpr;
2441
- }
2442
- for (const name of decorators.keys()) {
2443
- if (!KNOWN_DECORATORS2.has(name)) {
2444
- comments.push(`/* @${name}: not translatable to zod (server-only) */`);
2445
- if (!ctx.warnedDecorators.has(name)) {
2446
- ctx.warnedDecorators.add(name);
2447
- const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
2448
- ctx.warnings.push(msg);
2449
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2229
+ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2230
+ if (Node4.isUnionTypeNode(typeNode)) {
2231
+ let nullable = false;
2232
+ const stringLits = [];
2233
+ const numberLits = [];
2234
+ const others = [];
2235
+ for (const member of typeNode.getTypeNodes()) {
2236
+ const kind = member.getKind();
2237
+ if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
2238
+ nullable = true;
2239
+ continue;
2450
2240
  }
2241
+ if (Node4.isLiteralTypeNode(member)) {
2242
+ const lit = member.getLiteral();
2243
+ if (Node4.isStringLiteral(lit)) {
2244
+ stringLits.push(lit.getLiteralValue());
2245
+ continue;
2246
+ }
2247
+ if (Node4.isNumericLiteral(lit)) {
2248
+ numberLits.push(lit.getText());
2249
+ continue;
2250
+ }
2251
+ if (lit.getKind() === SyntaxKind2.NullKeyword) {
2252
+ nullable = true;
2253
+ continue;
2254
+ }
2255
+ }
2256
+ others.push(member);
2257
+ }
2258
+ if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2259
+ return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2451
2260
  }
2261
+ if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2262
+ return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2263
+ }
2264
+ if (others.length === 1) {
2265
+ const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2266
+ return markNullable(inner, nullable || inner.nullable === true);
2267
+ }
2268
+ return markNullable({ kind: "unknown" }, nullable);
2452
2269
  }
2453
- let expr = base + refinements.join("");
2454
- if (isArrayType && !expr.startsWith("z.array(")) {
2455
- expr = `z.array(${expr})`;
2270
+ switch (typeNode.getKind()) {
2271
+ case SyntaxKind2.StringKeyword:
2272
+ return { kind: "string" };
2273
+ case SyntaxKind2.NumberKeyword:
2274
+ return { kind: "number" };
2275
+ case SyntaxKind2.BooleanKeyword:
2276
+ return { kind: "boolean" };
2277
+ case SyntaxKind2.AnyKeyword:
2278
+ case SyntaxKind2.UnknownKeyword:
2279
+ return { kind: "unknown" };
2280
+ default:
2281
+ break;
2456
2282
  }
2457
- expr = applyPresence2(expr, decorators);
2458
- if (comments.length > 0) {
2459
- expr = `${expr} ${comments.join(" ")}`;
2283
+ if (Node4.isTypeReference(typeNode)) {
2284
+ const refName = typeNode.getTypeName().getText();
2285
+ if (refName === "Date") return { kind: "date" };
2286
+ if (refName === "Record" || refName === "Object") return { kind: "json" };
2287
+ const typeRef = opts?.resolveRef?.(refName) ?? null;
2288
+ const en = resolveEnumValues(refName, sourceFile, project);
2289
+ if (en) {
2290
+ const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
2291
+ return typeRef ? { ...base, typeRef } : base;
2292
+ }
2293
+ if (typeRef) return { kind: "unknown", typeRef };
2294
+ return { kind: "unknown" };
2460
2295
  }
2461
- return expr;
2462
- }
2463
- function applyPresence2(expr, decorators) {
2464
- if (decorators.has("IsDefined")) return expr;
2465
- if (decorators.has("IsOptional")) return `${expr}.optional()`;
2466
- return expr;
2467
- }
2468
- function baseFromType2(typeText, isArrayType, ctx, classFile) {
2469
- const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
2470
- switch (inner) {
2471
- case "string":
2472
- return "z.string()";
2473
- case "number":
2474
- return "z.number()";
2475
- case "boolean":
2476
- return "z.boolean()";
2477
- case "Date":
2478
- return "z.coerce.date()";
2479
- case "File":
2480
- case "Express.Multer.File":
2481
- return "z.instanceof(File)";
2482
- default:
2483
- return "z.unknown()";
2484
- }
2485
- }
2486
- function ensureStringBase(base) {
2487
- return isStringBase(base) ? base : "z.string()";
2488
- }
2489
- function isStringBase(base) {
2490
- return base.startsWith("z.string(");
2491
- }
2492
- function buildNestedReference2(className, fromFile, ctx) {
2493
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2494
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
2495
- ctx.emittedClasses.set(className, reserved);
2496
- ctx.recursiveSchemas.add(reserved);
2497
- if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2498
- ctx.warnedDecorators.add(`recursive:${reserved}`);
2499
- const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
2500
- ctx.warnings.push(msg);
2501
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2502
- }
2503
- return `z.lazy(() => ${reserved})`;
2504
- }
2505
- const existing = ctx.emittedClasses.get(className);
2506
- if (existing) return existing;
2507
- const schemaName = aliasFor2(className, ctx);
2508
- const resolved = findType(className, fromFile, ctx.project);
2509
- if (!resolved || resolved.kind !== "class") {
2510
- return "z.object({}).passthrough()";
2511
- }
2512
- ctx.emittedClasses.set(className, schemaName);
2513
- ctx.visiting.add(className);
2514
- ctx.depth += 1;
2515
- const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
2516
- ctx.depth -= 1;
2517
- ctx.visiting.delete(className);
2518
- ctx.namedNestedSchemas.set(schemaName, childText);
2519
- return schemaName;
2520
- }
2521
- function aliasFor2(className, ctx) {
2522
- const baseName = `${className}Schema`;
2523
- let candidate = baseName;
2524
- let i = 1;
2525
- const used = new Set(ctx.namedNestedSchemas.keys());
2526
- for (const v of ctx.emittedClasses.values()) used.add(v);
2527
- while (used.has(candidate)) {
2528
- candidate = `${baseName}_${i}`;
2529
- i += 1;
2530
- }
2531
- return candidate;
2532
- }
2533
- function firstArg2(decorator) {
2534
- return decorator?.getArguments()[0];
2535
- }
2536
- function firstArgText2(decorator) {
2537
- const arg = firstArg2(decorator);
2538
- return arg ? arg.getText() : null;
2539
- }
2540
- function numericArg2(decorator) {
2541
- const arg = firstArg2(decorator);
2542
- if (arg && Node4.isNumericLiteral(arg)) return arg.getText();
2543
- return null;
2544
- }
2545
- function numericArgs2(decorator) {
2546
- const args = decorator?.getArguments() ?? [];
2547
- const num = (n) => n && Node4.isNumericLiteral(n) ? n.getText() : null;
2548
- return [num(args[0]), num(args[1])];
2549
- }
2550
- function messageArg(decorator) {
2551
- const args = decorator?.getArguments() ?? [];
2552
- for (const arg of args) {
2553
- if (Node4.isObjectLiteralExpression(arg)) {
2554
- for (const prop of arg.getProperties()) {
2555
- if (Node4.isPropertyAssignment(prop) && prop.getName() === "message") {
2556
- const init = prop.getInitializer();
2557
- if (init && Node4.isStringLiteral(init)) {
2558
- return `{ message: ${init.getText()} }`;
2559
- }
2560
- }
2561
- }
2562
- }
2563
- }
2564
- return "";
2565
- }
2566
- function resolveTypeFactoryName2(decorator) {
2567
- const arg = firstArg2(decorator);
2568
- if (!arg) return null;
2569
- if (Node4.isArrowFunction(arg)) {
2570
- const body = arg.getBody();
2571
- if (Node4.isIdentifier(body)) return body.getText();
2572
- }
2573
- return null;
2574
- }
2575
- function singularClassName2(typeText) {
2576
- const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2577
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2578
- }
2579
- function enumSchemaFromDecorator2(decorator, classFile, ctx) {
2580
- const arg = firstArg2(decorator);
2581
- if (!arg) return null;
2582
- if (Node4.isIdentifier(arg)) {
2583
- const name = arg.getText();
2584
- const resolved = findType(name, classFile, ctx.project);
2585
- if (resolved && resolved.kind === "enum") {
2586
- return `z.enum([${resolved.members.join(", ")}])`;
2587
- }
2588
- 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().`;
2589
- if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
2590
- ctx.warnedDecorators.add(`IsEnum:${name}`);
2591
- ctx.warnings.push(msg);
2592
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2593
- }
2594
- return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
2595
- }
2596
- if (Node4.isObjectLiteralExpression(arg)) {
2597
- const values = [];
2598
- for (const p of arg.getProperties()) {
2599
- if (!Node4.isPropertyAssignment(p)) continue;
2600
- const init = p.getInitializer();
2601
- if (init && Node4.isStringLiteral(init)) values.push(init.getText());
2602
- }
2603
- if (values.length > 0) return `z.enum([${values.join(", ")}])`;
2604
- }
2605
- return null;
2606
- }
2607
- function inSchemaFromDecorator2(decorator) {
2608
- const arg = firstArg2(decorator);
2609
- if (arg && Node4.isArrayLiteralExpression(arg)) {
2610
- const elements = arg.getElements();
2611
- const allStrings = elements.every((e) => Node4.isStringLiteral(e));
2612
- if (allStrings && elements.length > 0) {
2613
- return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
2614
- }
2615
- if (elements.length > 0) {
2616
- return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
2617
- }
2618
- }
2619
- return null;
2620
- }
2621
-
2622
- // src/discovery/filter-for.ts
2623
- import {
2624
- Node as Node6
2625
- } from "ts-morph";
2626
-
2627
- // src/discovery/filter-field-types.ts
2628
- import {
2629
- Node as Node5,
2630
- SyntaxKind as SyntaxKind2
2631
- } from "ts-morph";
2632
-
2633
- // src/discovery/enum-resolution.ts
2634
- function resolveEnumValues(name, sourceFile, project) {
2635
- const resolved = findType(name, sourceFile, project);
2636
- if (!resolved || resolved.kind !== "enum") return null;
2637
- let numeric = true;
2638
- const values = resolved.members.map((m) => {
2639
- const parsed = JSON.parse(m);
2640
- if (typeof parsed === "string") numeric = false;
2641
- return String(parsed);
2642
- });
2643
- if (values.length === 0) return null;
2644
- return { values, numeric };
2645
- }
2646
-
2647
- // src/discovery/filter-field-types.ts
2648
- var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2649
- var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2650
- var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2651
- var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2652
- var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2653
- function classifyTypeKeyword(raw) {
2654
- const t = raw.toLowerCase();
2655
- if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2656
- if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2657
- if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2658
- if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2659
- if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2660
- return null;
2661
- }
2662
- function markNullable(r, nullable) {
2663
- return nullable ? { ...r, nullable: true } : r;
2664
- }
2665
- function classifyTypeNode(typeNode, sourceFile, project, opts) {
2666
- if (Node5.isUnionTypeNode(typeNode)) {
2667
- let nullable = false;
2668
- const stringLits = [];
2669
- const numberLits = [];
2670
- const others = [];
2671
- for (const member of typeNode.getTypeNodes()) {
2672
- const kind = member.getKind();
2673
- if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
2674
- nullable = true;
2675
- continue;
2676
- }
2677
- if (Node5.isLiteralTypeNode(member)) {
2678
- const lit = member.getLiteral();
2679
- if (Node5.isStringLiteral(lit)) {
2680
- stringLits.push(lit.getLiteralValue());
2681
- continue;
2682
- }
2683
- if (Node5.isNumericLiteral(lit)) {
2684
- numberLits.push(lit.getText());
2685
- continue;
2686
- }
2687
- if (lit.getKind() === SyntaxKind2.NullKeyword) {
2688
- nullable = true;
2689
- continue;
2690
- }
2691
- }
2692
- others.push(member);
2693
- }
2694
- if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2695
- return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2696
- }
2697
- if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2698
- return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2699
- }
2700
- if (others.length === 1) {
2701
- const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2702
- return markNullable(inner, nullable || inner.nullable === true);
2703
- }
2704
- return markNullable({ kind: "unknown" }, nullable);
2705
- }
2706
- switch (typeNode.getKind()) {
2707
- case SyntaxKind2.StringKeyword:
2708
- return { kind: "string" };
2709
- case SyntaxKind2.NumberKeyword:
2710
- return { kind: "number" };
2711
- case SyntaxKind2.BooleanKeyword:
2712
- return { kind: "boolean" };
2713
- case SyntaxKind2.AnyKeyword:
2714
- case SyntaxKind2.UnknownKeyword:
2715
- return { kind: "unknown" };
2716
- default:
2717
- break;
2718
- }
2719
- if (Node5.isTypeReference(typeNode)) {
2720
- const refName = typeNode.getTypeName().getText();
2721
- if (refName === "Date") return { kind: "date" };
2722
- if (refName === "Record" || refName === "Object") return { kind: "json" };
2723
- const typeRef = opts?.resolveRef?.(refName) ?? null;
2724
- const en = resolveEnumValues(refName, sourceFile, project);
2725
- if (en) {
2726
- const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
2727
- return typeRef ? { ...base, typeRef } : base;
2728
- }
2729
- if (typeRef) return { kind: "unknown", typeRef };
2730
- return { kind: "unknown" };
2731
- }
2732
- if (Node5.isTypeLiteral(typeNode)) return { kind: "json" };
2733
- return { kind: "unknown" };
2296
+ if (Node4.isTypeLiteral(typeNode)) return { kind: "json" };
2297
+ return { kind: "unknown" };
2734
2298
  }
2735
2299
  function enumFromDecoratorArgs(args, sourceFile, project) {
2736
2300
  for (const arg of args) {
2737
- if (Node5.isArrowFunction(arg)) {
2301
+ if (Node4.isArrowFunction(arg)) {
2738
2302
  const body = arg.getBody();
2739
- if (Node5.isIdentifier(body)) {
2303
+ if (Node4.isIdentifier(body)) {
2740
2304
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2741
2305
  if (en) return en;
2742
2306
  }
2743
2307
  }
2744
- if (Node5.isObjectLiteralExpression(arg)) {
2308
+ if (Node4.isObjectLiteralExpression(arg)) {
2745
2309
  const itemsProp = arg.getProperty("items");
2746
- if (itemsProp && Node5.isPropertyAssignment(itemsProp)) {
2310
+ if (itemsProp && Node4.isPropertyAssignment(itemsProp)) {
2747
2311
  const init = itemsProp.getInitializer();
2748
- if (init && Node5.isArrowFunction(init)) {
2312
+ if (init && Node4.isArrowFunction(init)) {
2749
2313
  const body = init.getBody();
2750
- if (Node5.isIdentifier(body)) {
2314
+ if (Node4.isIdentifier(body)) {
2751
2315
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2752
2316
  if (en) return en;
2753
2317
  }
@@ -2770,7 +2334,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2770
2334
  return { kind: "string" };
2771
2335
  }
2772
2336
  for (const arg of args) {
2773
- if (Node5.isStringLiteral(arg)) {
2337
+ if (Node4.isStringLiteral(arg)) {
2774
2338
  const raw = arg.getLiteralValue();
2775
2339
  const kind = classifyTypeKeyword(raw);
2776
2340
  if (kind) {
@@ -2778,11 +2342,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2778
2342
  return { kind };
2779
2343
  }
2780
2344
  }
2781
- if (Node5.isObjectLiteralExpression(arg)) {
2345
+ if (Node4.isObjectLiteralExpression(arg)) {
2782
2346
  const enumProp = arg.getProperty("enum");
2783
- if (enumProp && Node5.isPropertyAssignment(enumProp)) {
2347
+ if (enumProp && Node4.isPropertyAssignment(enumProp)) {
2784
2348
  const init = enumProp.getInitializer();
2785
- if (init && Node5.isIdentifier(init)) {
2349
+ if (init && Node4.isIdentifier(init)) {
2786
2350
  const en = resolveEnumValues(init.getText(), sourceFile, project);
2787
2351
  if (en) {
2788
2352
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -2791,9 +2355,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2791
2355
  }
2792
2356
  }
2793
2357
  const typeProp = arg.getProperty("type");
2794
- if (typeProp && Node5.isPropertyAssignment(typeProp)) {
2358
+ if (typeProp && Node4.isPropertyAssignment(typeProp)) {
2795
2359
  const init = typeProp.getInitializer();
2796
- if (init && Node5.isStringLiteral(init)) {
2360
+ if (init && Node4.isStringLiteral(init)) {
2797
2361
  const kind = classifyTypeKeyword(init.getLiteralValue());
2798
2362
  if (kind) return { kind };
2799
2363
  }
@@ -2829,7 +2393,7 @@ function toFilterFieldType(name, r) {
2829
2393
 
2830
2394
  // src/discovery/filter-for.ts
2831
2395
  function classifyFilterForHint(typeInit) {
2832
- if (Node6.isStringLiteral(typeInit)) {
2396
+ if (Node5.isStringLiteral(typeInit)) {
2833
2397
  switch (typeInit.getLiteralValue()) {
2834
2398
  case "string":
2835
2399
  return { kind: "string" };
@@ -2843,10 +2407,10 @@ function classifyFilterForHint(typeInit) {
2843
2407
  return null;
2844
2408
  }
2845
2409
  }
2846
- if (Node6.isArrayLiteralExpression(typeInit)) {
2410
+ if (Node5.isArrayLiteralExpression(typeInit)) {
2847
2411
  const values = [];
2848
2412
  for (const el of typeInit.getElements()) {
2849
- if (!Node6.isStringLiteral(el)) return null;
2413
+ if (!Node5.isStringLiteral(el)) return null;
2850
2414
  values.push(el.getLiteralValue());
2851
2415
  }
2852
2416
  if (values.length === 0) return null;
@@ -2881,11 +2445,11 @@ function extractFilterForHints(classDecl, project) {
2881
2445
  if (!filterForDec) continue;
2882
2446
  const args = filterForDec.getArguments();
2883
2447
  const keyArg = args[0];
2884
- const inputKey = keyArg && Node6.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2448
+ const inputKey = keyArg && Node5.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2885
2449
  const optsArg = args[1];
2886
- if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
2450
+ if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
2887
2451
  const typeProp = optsArg.getProperty("type");
2888
- if (typeProp && Node6.isPropertyAssignment(typeProp)) {
2452
+ if (typeProp && Node5.isPropertyAssignment(typeProp)) {
2889
2453
  const typeInit = typeProp.getInitializer();
2890
2454
  if (typeInit) {
2891
2455
  const classified = classifyFilterForHint(typeInit);
@@ -2908,14 +2472,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
2908
2472
  const args = filterDecorator.getArguments();
2909
2473
  if (args.length === 0) continue;
2910
2474
  const filterClassArg = args[0];
2911
- if (!filterClassArg || !Node6.isIdentifier(filterClassArg)) continue;
2475
+ if (!filterClassArg || !Node5.isIdentifier(filterClassArg)) continue;
2912
2476
  let source = "query";
2913
2477
  const optionsArg = args[1];
2914
- if (optionsArg && Node6.isObjectLiteralExpression(optionsArg)) {
2478
+ if (optionsArg && Node5.isObjectLiteralExpression(optionsArg)) {
2915
2479
  const sourceProp = optionsArg.getProperty("source");
2916
- if (sourceProp && Node6.isPropertyAssignment(sourceProp)) {
2480
+ if (sourceProp && Node5.isPropertyAssignment(sourceProp)) {
2917
2481
  const init = sourceProp.getInitializer();
2918
- if (init && Node6.isStringLiteral(init) && init.getLiteralValue() === "body") {
2482
+ if (init && Node5.isStringLiteral(init) && init.getLiteralValue() === "body") {
2919
2483
  source = "body";
2920
2484
  }
2921
2485
  }
@@ -2978,22 +2542,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
2978
2542
  const args = dec.getArguments();
2979
2543
  if (args.length === 0) continue;
2980
2544
  const arg = args[0];
2981
- if (Node6.isObjectLiteralExpression(arg)) {
2545
+ if (Node5.isObjectLiteralExpression(arg)) {
2982
2546
  const entityProp = arg.getProperty("entity");
2983
- if (entityProp && Node6.isPropertyAssignment(entityProp)) {
2547
+ if (entityProp && Node5.isPropertyAssignment(entityProp)) {
2984
2548
  const init = entityProp.getInitializer();
2985
- if (init && Node6.isArrowFunction(init)) {
2549
+ if (init && Node5.isArrowFunction(init)) {
2986
2550
  const body = init.getBody();
2987
- if (Node6.isIdentifier(body)) {
2551
+ if (Node5.isIdentifier(body)) {
2988
2552
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
2989
2553
  if (resolved?.kind === "class") return resolved.decl;
2990
2554
  }
2991
2555
  }
2992
2556
  }
2993
2557
  }
2994
- if (Node6.isArrowFunction(arg)) {
2558
+ if (Node5.isArrowFunction(arg)) {
2995
2559
  const body = arg.getBody();
2996
- if (Node6.isIdentifier(body)) {
2560
+ if (Node5.isIdentifier(body)) {
2997
2561
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
2998
2562
  if (resolved?.kind === "class") return resolved.decl;
2999
2563
  }
@@ -3017,11 +2581,11 @@ function extractFilterableEntityFields(filterClass, project) {
3017
2581
  const args = filterableDecorator.getArguments();
3018
2582
  if (args.length === 0) return [];
3019
2583
  const optionsArg = args[0];
3020
- if (!Node6.isObjectLiteralExpression(optionsArg)) return [];
2584
+ if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
3021
2585
  const entityProp = optionsArg.getProperty("entity");
3022
- if (!entityProp || !Node6.isPropertyAssignment(entityProp)) return [];
2586
+ if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
3023
2587
  const entityInit = entityProp.getInitializer();
3024
- if (!entityInit || !Node6.isIdentifier(entityInit)) return [];
2588
+ if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
3025
2589
  const entityName = entityInit.getText();
3026
2590
  const filterSourceFile = filterClass.getSourceFile();
3027
2591
  const resolvedEntity = findType(entityName, filterSourceFile, project);
@@ -3037,17 +2601,17 @@ function extractFilterableEntityFields(filterClass, project) {
3037
2601
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
3038
2602
  if (relationsDecorator) {
3039
2603
  const relArgs = relationsDecorator.getArguments();
3040
- if (relArgs.length > 0 && Node6.isObjectLiteralExpression(relArgs[0])) {
2604
+ if (relArgs.length > 0 && Node5.isObjectLiteralExpression(relArgs[0])) {
3041
2605
  for (const relProp of relArgs[0].getProperties()) {
3042
- if (!Node6.isPropertyAssignment(relProp)) continue;
2606
+ if (!Node5.isPropertyAssignment(relProp)) continue;
3043
2607
  const relInit = relProp.getInitializer();
3044
- if (!relInit || !Node6.isObjectLiteralExpression(relInit)) continue;
2608
+ if (!relInit || !Node5.isObjectLiteralExpression(relInit)) continue;
3045
2609
  const keysProp = relInit.getProperty("keys");
3046
- if (!keysProp || !Node6.isPropertyAssignment(keysProp)) continue;
2610
+ if (!keysProp || !Node5.isPropertyAssignment(keysProp)) continue;
3047
2611
  const keysInit = keysProp.getInitializer();
3048
- if (!keysInit || !Node6.isArrayLiteralExpression(keysInit)) continue;
2612
+ if (!keysInit || !Node5.isArrayLiteralExpression(keysInit)) continue;
3049
2613
  for (const el of keysInit.getElements()) {
3050
- if (Node6.isStringLiteral(el)) {
2614
+ if (Node5.isStringLiteral(el)) {
3051
2615
  fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
3052
2616
  }
3053
2617
  }
@@ -3057,267 +2621,65 @@ function extractFilterableEntityFields(filterClass, project) {
3057
2621
  return fields;
3058
2622
  }
3059
2623
 
3060
- // src/discovery/contracts-fast.ts
3061
- async function discoverContractsFast(opts) {
3062
- const { cwd, glob, tsconfig } = opts;
3063
- const tsconfigPath = tsconfig ? resolve3(tsconfig) : join10(cwd, "tsconfig.json");
3064
- let project;
3065
- try {
3066
- project = new Project3({
3067
- tsConfigFilePath: tsconfigPath,
3068
- skipAddingFilesFromTsConfig: true,
3069
- skipLoadingLibFiles: true,
3070
- skipFileDependencyResolution: true
3071
- });
3072
- } catch {
3073
- project = new Project3({
3074
- skipAddingFilesFromTsConfig: true,
3075
- skipLoadingLibFiles: true,
3076
- skipFileDependencyResolution: true,
3077
- compilerOptions: {
3078
- allowJs: true,
3079
- resolveJsonModule: false,
3080
- strict: false
3081
- }
3082
- });
2624
+ // src/discovery/dto-type-resolver.ts
2625
+ var WRAPPER_TYPES = {
2626
+ // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
2627
+ // wrappers around related entities; the wire shape is just the referenced
2628
+ // entity. Unwrap to the type argument.
2629
+ Ref: "unwrap",
2630
+ Reference: "unwrap",
2631
+ LoadedReference: "unwrap",
2632
+ IdentifiedReference: "unwrap",
2633
+ // MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
2634
+ Opt: "unwrap",
2635
+ Loaded: "unwrap",
2636
+ // Promise<T> — unwrap
2637
+ Promise: "unwrap",
2638
+ // MikroORM Collection<T> serializes as an array of T on the wire.
2639
+ Collection: "arrayOf",
2640
+ // Array<T> generic form
2641
+ Array: "arrayOf"
2642
+ };
2643
+ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2644
+ "Record",
2645
+ "Omit",
2646
+ "Pick",
2647
+ "Partial",
2648
+ "Required",
2649
+ "Readonly",
2650
+ "Map",
2651
+ "Set"
2652
+ ]);
2653
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2654
+ if (depth <= 0) return "unknown";
2655
+ if (Node6.isArrayTypeNode(typeNode)) {
2656
+ const elementType = typeNode.getElementTypeNode();
2657
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3083
2658
  }
3084
- const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3085
- for (const f of files) {
3086
- project.addSourceFileAtPath(f);
2659
+ if (Node6.isUnionTypeNode(typeNode)) {
2660
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3087
2661
  }
3088
- const routes = [];
3089
- const prevCtx = setDiscoveryContext({
3090
- projectRoot: cwd,
3091
- tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3092
- });
3093
- try {
3094
- for (const sourceFile of project.getSourceFiles()) {
3095
- routes.push(...extractFromSourceFile(sourceFile, project));
3096
- }
3097
- } finally {
3098
- restoreDiscoveryContext(prevCtx);
2662
+ if (Node6.isIntersectionTypeNode(typeNode)) {
2663
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3099
2664
  }
3100
- return routes;
3101
- }
3102
- function zodAstToTs(node) {
3103
- if (!Node7.isCallExpression(node)) return "unknown";
3104
- const expr = node.getExpression();
3105
- if (Node7.isPropertyAccessExpression(expr)) {
3106
- const methodName = expr.getName();
3107
- const receiver = expr.getExpression();
3108
- if (methodName === "optional") {
3109
- return `${zodAstToTs(receiver)} | undefined`;
3110
- }
3111
- if (methodName === "nullable") {
3112
- return `${zodAstToTs(receiver)} | null`;
3113
- }
3114
- const args = node.getArguments();
3115
- switch (methodName) {
3116
- case "string":
3117
- return "string";
3118
- case "number":
3119
- return "number";
3120
- case "boolean":
3121
- return "boolean";
3122
- case "unknown":
3123
- return "unknown";
3124
- case "any":
3125
- return "unknown";
3126
- case "literal": {
3127
- const lit = args[0];
3128
- if (!lit) return "unknown";
3129
- if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
3130
- if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
3131
- if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
3132
- if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
3133
- return "unknown";
3134
- }
3135
- case "enum": {
3136
- const arrArg = args[0];
3137
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
3138
- const members = arrArg.getElements().map(
3139
- (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
3140
- );
3141
- return members.join(" | ");
3142
- }
3143
- case "array": {
3144
- const inner = args[0];
3145
- if (!inner) return "unknown";
3146
- return `Array<${zodAstToTs(inner)}>`;
3147
- }
3148
- case "object": {
3149
- const objArg = args[0];
3150
- if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
3151
- const lines = [];
3152
- for (const prop of objArg.getProperties()) {
3153
- if (!Node7.isPropertyAssignment(prop)) continue;
3154
- const key = prop.getName();
3155
- const valNode = prop.getInitializer();
3156
- if (!valNode) continue;
3157
- const tsType = zodAstToTs(valNode);
3158
- const isOpt = isOptionalChain(valNode);
3159
- lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
3160
- }
3161
- return `{ ${lines.join("; ")} }`;
3162
- }
3163
- case "union": {
3164
- const arrArg = args[0];
3165
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
3166
- return arrArg.getElements().map(zodAstToTs).join(" | ");
3167
- }
3168
- case "record": {
3169
- const valArg = args.length === 1 ? args[0] : args[1];
3170
- if (!valArg) return "unknown";
3171
- return `Record<string, ${zodAstToTs(valArg)}>`;
3172
- }
3173
- case "tuple": {
3174
- const arrArg = args[0];
3175
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
3176
- return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3177
- }
3178
- default:
3179
- return "unknown";
3180
- }
3181
- }
3182
- return "unknown";
3183
- }
3184
- function isOptionalChain(node) {
3185
- if (!Node7.isCallExpression(node)) return false;
3186
- const expr = node.getExpression();
3187
- return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3188
- }
3189
- function decoratorStringArg(decoratorExpr) {
3190
- if (!decoratorExpr) return void 0;
3191
- if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3192
- if (Node7.isArrayLiteralExpression(decoratorExpr)) {
3193
- const first = decoratorExpr.getElements()[0];
3194
- if (first && Node7.isStringLiteral(first)) return first.getLiteralValue();
3195
- }
3196
- return void 0;
3197
- }
3198
- function parseDefineContractCall(callExpr) {
3199
- if (!Node7.isCallExpression(callExpr)) return null;
3200
- const callee = callExpr.getExpression();
3201
- const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
3202
- if (calleeName !== "defineContract") return null;
3203
- const args = callExpr.getArguments();
3204
- const optsArg = args[0];
3205
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3206
- let query = null;
3207
- let body = null;
3208
- let response = "unknown";
3209
- let bodyZodText = null;
3210
- let queryZodText = null;
3211
- for (const prop of optsArg.getProperties()) {
3212
- if (!Node7.isPropertyAssignment(prop)) continue;
3213
- const propName = prop.getName();
3214
- const val = prop.getInitializer();
3215
- if (!val) continue;
3216
- if (propName === "query") {
3217
- query = zodAstToTs(val);
3218
- queryZodText = val.getText();
3219
- } else if (propName === "body") {
3220
- body = zodAstToTs(val);
3221
- bodyZodText = val.getText();
3222
- } else if (propName === "response") {
3223
- response = zodAstToTs(val);
3224
- }
3225
- }
3226
- return { query, body, response, bodyZodText, queryZodText };
3227
- }
3228
- function deriveClassSegment(className) {
3229
- const noSuffix = className.replace(/Controller$/, "");
3230
- if (!noSuffix) {
3231
- throw new Error(
3232
- `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3233
- );
3234
- }
3235
- return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3236
- }
3237
- function resolveRouteName(className, methodName, classAs, methodAs) {
3238
- const classPortion = classAs ?? deriveClassSegment(className);
3239
- const methodPortion = methodAs ?? methodName;
3240
- return `${classPortion}.${methodPortion}`;
3241
- }
3242
- function joinPaths(prefix, suffix) {
3243
- if (!prefix && !suffix) return "/";
3244
- if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3245
- if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3246
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3247
- const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3248
- const combined = p + s;
3249
- return combined === "" ? "/" : combined;
3250
- }
3251
- function extractParams(path) {
3252
- const matches = path.matchAll(/:(\w+)/g);
3253
- return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3254
- }
3255
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3256
- if (depth <= 0) return "unknown";
3257
- if (Node7.isArrayTypeNode(typeNode)) {
3258
- const elementType = typeNode.getElementTypeNode();
3259
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3260
- }
3261
- if (Node7.isUnionTypeNode(typeNode)) {
3262
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3263
- }
3264
- if (Node7.isIntersectionTypeNode(typeNode)) {
3265
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3266
- }
3267
- if (Node7.isParenthesizedTypeNode(typeNode)) {
2665
+ if (Node6.isParenthesizedTypeNode(typeNode)) {
3268
2666
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3269
2667
  }
3270
- if (Node7.isTypeReference(typeNode)) {
2668
+ if (Node6.isTypeReference(typeNode)) {
3271
2669
  const typeName = typeNode.getTypeName();
3272
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2670
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3273
2671
  if (name === "string" || name === "number" || name === "boolean") return name;
3274
2672
  if (name === "Date") return "string";
3275
2673
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
3276
2674
  if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
3277
2675
  return "unknown";
3278
- if (name === "Ref" || name === "Reference" || name === "LoadedReference" || name === "IdentifiedReference") {
3279
- const typeArgs = typeNode.getTypeArguments();
3280
- const firstTypeArg = typeArgs[0];
3281
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3282
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3283
- }
3284
- return "unknown";
2676
+ const wrapperMode = WRAPPER_TYPES[name];
2677
+ if (wrapperMode) {
2678
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3285
2679
  }
3286
- if (name === "Collection") {
3287
- const typeArgs = typeNode.getTypeArguments();
3288
- const firstTypeArg = typeArgs[0];
3289
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3290
- return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
3291
- }
3292
- return "Array<unknown>";
3293
- }
3294
- if (name === "Opt" || name === "Loaded") {
3295
- const typeArgs = typeNode.getTypeArguments();
3296
- const firstTypeArg = typeArgs[0];
3297
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3298
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3299
- }
3300
- return "unknown";
3301
- }
3302
- if (name === "Array") {
3303
- const typeArgs = typeNode.getTypeArguments();
3304
- const firstTypeArg = typeArgs[0];
3305
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3306
- return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
3307
- }
3308
- return "Array<unknown>";
3309
- }
3310
- if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
2680
+ if (PASSTHROUGH_UTILITY.has(name)) {
3311
2681
  return typeNode.getText();
3312
2682
  }
3313
- if (name === "Promise") {
3314
- const typeArgs = typeNode.getTypeArguments();
3315
- const firstTypeArg = typeArgs[0];
3316
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3317
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3318
- }
3319
- return "unknown";
3320
- }
3321
2683
  const resolved = findType(name, sourceFile, project);
3322
2684
  if (resolved) {
3323
2685
  return expandTypeDecl(resolved, project, depth - 1);
@@ -3333,6 +2695,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3333
2695
  if (kind === SyntaxKind3.AnyKeyword) return "unknown";
3334
2696
  return typeNode.getText();
3335
2697
  }
2698
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
2699
+ const typeArgs = typeNode.getTypeArguments();
2700
+ const firstTypeArg = typeArgs[0];
2701
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2702
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
2703
+ return mode === "arrayOf" ? `Array<${inner}>` : inner;
2704
+ }
2705
+ return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2706
+ }
3336
2707
  function expandTypeDecl(result, project, depth) {
3337
2708
  if (depth < 0) return "unknown";
3338
2709
  switch (result.kind) {
@@ -3398,7 +2769,7 @@ function extractParamsType(method, sourceFile, project) {
3398
2769
  const paramArgs = paramDecorator.getArguments();
3399
2770
  if (paramArgs.length === 0) continue;
3400
2771
  const nameArg = paramArgs[0];
3401
- if (!Node7.isStringLiteral(nameArg)) continue;
2772
+ if (!Node6.isStringLiteral(nameArg)) continue;
3402
2773
  const paramName = nameArg.getLiteralValue();
3403
2774
  const typeNode = param.getTypeNode();
3404
2775
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -3411,13 +2782,13 @@ function extractResponseType(method, sourceFile, project) {
3411
2782
  if (apiResponseDecorator) {
3412
2783
  const args = apiResponseDecorator.getArguments();
3413
2784
  const optsArg = args[0];
3414
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
2785
+ if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
3415
2786
  for (const prop of optsArg.getProperties()) {
3416
- if (!Node7.isPropertyAssignment(prop)) continue;
2787
+ if (!Node6.isPropertyAssignment(prop)) continue;
3417
2788
  if (prop.getName() !== "type") continue;
3418
2789
  const val = prop.getInitializer();
3419
2790
  if (!val) continue;
3420
- if (Node7.isArrayLiteralExpression(val)) {
2791
+ if (Node6.isArrayLiteralExpression(val)) {
3421
2792
  const elements = val.getElements();
3422
2793
  const firstEl = elements[0];
3423
2794
  if (elements.length > 0 && firstEl !== void 0) {
@@ -3437,7 +2808,7 @@ function extractResponseType(method, sourceFile, project) {
3437
2808
  return "unknown";
3438
2809
  }
3439
2810
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
3440
- if (!Node7.isIdentifier(node)) return "unknown";
2811
+ if (!Node6.isIdentifier(node)) return "unknown";
3441
2812
  const name = node.getText();
3442
2813
  const resolved = findType(name, sourceFile, project);
3443
2814
  if (resolved) {
@@ -3484,11 +2855,11 @@ function extractDtoContract(method, sourceFile, project) {
3484
2855
  if (apiResp) {
3485
2856
  const args = apiResp.getArguments();
3486
2857
  const optsArg = args[0];
3487
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
2858
+ if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
3488
2859
  for (const prop of optsArg.getProperties()) {
3489
- if (Node7.isPropertyAssignment(prop) && prop.getName() === "type") {
2860
+ if (Node6.isPropertyAssignment(prop) && prop.getName() === "type") {
3490
2861
  const val = prop.getInitializer();
3491
- if (val && Node7.isIdentifier(val)) {
2862
+ if (val && Node6.isIdentifier(val)) {
3492
2863
  const name = val.getText();
3493
2864
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3494
2865
  if (localDecl?.isExported()) {
@@ -3505,60 +2876,243 @@ function extractDtoContract(method, sourceFile, project) {
3505
2876
  }
3506
2877
  }
3507
2878
  }
3508
- let bodyZodText = null;
3509
- let queryZodText = null;
3510
- let bodySchema = null;
3511
- let querySchema = null;
3512
- const formNested = {};
3513
- const formWarnings = [];
3514
- const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
3515
- if (bodyClass) {
3516
- const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
3517
- bodyZodText = result.schemaText;
3518
- for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
3519
- formWarnings.push(...result.warnings);
3520
- bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2879
+ let bodySchema = null;
2880
+ let querySchema = null;
2881
+ const formWarnings = [];
2882
+ const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
2883
+ if (bodyClass) {
2884
+ bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2885
+ formWarnings.push(...bodySchema.warnings);
2886
+ }
2887
+ const queryClass = resolveParamClass(method, "Query", sourceFile, project);
2888
+ if (queryClass) {
2889
+ querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
2890
+ formWarnings.push(...querySchema.warnings);
2891
+ }
2892
+ return {
2893
+ query,
2894
+ body,
2895
+ response,
2896
+ params: paramsType,
2897
+ queryRef,
2898
+ bodyRef,
2899
+ responseRef,
2900
+ filterFields: filterInfo?.fieldNames ?? null,
2901
+ filterFieldTypes: filterInfo?.fieldTypes ?? null,
2902
+ filterSource: filterInfo?.source ?? null,
2903
+ formWarnings,
2904
+ bodySchema,
2905
+ querySchema
2906
+ };
2907
+ }
2908
+ function resolveParamClass(method, decoratorName, sourceFile, project) {
2909
+ for (const param of method.getParameters()) {
2910
+ if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
2911
+ const typeNode = param.getTypeNode();
2912
+ if (!typeNode) continue;
2913
+ const text = typeNode.getText().replace(/\[\]$/, "");
2914
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
2915
+ const resolved = findType(text, sourceFile, project);
2916
+ if (resolved && resolved.kind === "class") {
2917
+ return { decl: resolved.decl, file: resolved.file };
2918
+ }
2919
+ }
2920
+ return null;
2921
+ }
2922
+
2923
+ // src/discovery/zod-ast-to-ts.ts
2924
+ import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
2925
+ function zodAstToTs(node) {
2926
+ if (!Node7.isCallExpression(node)) return "unknown";
2927
+ const expr = node.getExpression();
2928
+ if (Node7.isPropertyAccessExpression(expr)) {
2929
+ const methodName = expr.getName();
2930
+ const receiver = expr.getExpression();
2931
+ if (methodName === "optional") {
2932
+ return `${zodAstToTs(receiver)} | undefined`;
2933
+ }
2934
+ if (methodName === "nullable") {
2935
+ return `${zodAstToTs(receiver)} | null`;
2936
+ }
2937
+ const args = node.getArguments();
2938
+ switch (methodName) {
2939
+ case "string":
2940
+ return "string";
2941
+ case "number":
2942
+ return "number";
2943
+ case "boolean":
2944
+ return "boolean";
2945
+ case "unknown":
2946
+ return "unknown";
2947
+ case "any":
2948
+ return "unknown";
2949
+ case "literal": {
2950
+ const lit = args[0];
2951
+ if (!lit) return "unknown";
2952
+ if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2953
+ if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2954
+ if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
2955
+ if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
2956
+ return "unknown";
2957
+ }
2958
+ case "enum": {
2959
+ const arrArg = args[0];
2960
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
2961
+ const members = arrArg.getElements().map(
2962
+ (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2963
+ );
2964
+ return members.join(" | ");
2965
+ }
2966
+ case "array": {
2967
+ const inner = args[0];
2968
+ if (!inner) return "unknown";
2969
+ return `Array<${zodAstToTs(inner)}>`;
2970
+ }
2971
+ case "object": {
2972
+ const objArg = args[0];
2973
+ if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
2974
+ const lines = [];
2975
+ for (const prop of objArg.getProperties()) {
2976
+ if (!Node7.isPropertyAssignment(prop)) continue;
2977
+ const key = prop.getName();
2978
+ const valNode = prop.getInitializer();
2979
+ if (!valNode) continue;
2980
+ const tsType = zodAstToTs(valNode);
2981
+ const isOpt = isOptionalChain(valNode);
2982
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
2983
+ }
2984
+ return `{ ${lines.join("; ")} }`;
2985
+ }
2986
+ case "union": {
2987
+ const arrArg = args[0];
2988
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
2989
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
2990
+ }
2991
+ case "record": {
2992
+ const valArg = args.length === 1 ? args[0] : args[1];
2993
+ if (!valArg) return "unknown";
2994
+ return `Record<string, ${zodAstToTs(valArg)}>`;
2995
+ }
2996
+ case "tuple": {
2997
+ const arrArg = args[0];
2998
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
2999
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3000
+ }
3001
+ default:
3002
+ return "unknown";
3003
+ }
3004
+ }
3005
+ return "unknown";
3006
+ }
3007
+ function isOptionalChain(node) {
3008
+ if (!Node7.isCallExpression(node)) return false;
3009
+ const expr = node.getExpression();
3010
+ return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3011
+ }
3012
+ function parseDefineContractCall(callExpr) {
3013
+ if (!Node7.isCallExpression(callExpr)) return null;
3014
+ const callee = callExpr.getExpression();
3015
+ const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
3016
+ if (calleeName !== "defineContract") return null;
3017
+ const args = callExpr.getArguments();
3018
+ const optsArg = args[0];
3019
+ if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3020
+ let query = null;
3021
+ let body = null;
3022
+ let response = "unknown";
3023
+ let bodyZodText = null;
3024
+ let queryZodText = null;
3025
+ for (const prop of optsArg.getProperties()) {
3026
+ if (!Node7.isPropertyAssignment(prop)) continue;
3027
+ const propName = prop.getName();
3028
+ const val = prop.getInitializer();
3029
+ if (!val) continue;
3030
+ if (propName === "query") {
3031
+ query = zodAstToTs(val);
3032
+ queryZodText = val.getText();
3033
+ } else if (propName === "body") {
3034
+ body = zodAstToTs(val);
3035
+ bodyZodText = val.getText();
3036
+ } else if (propName === "response") {
3037
+ response = zodAstToTs(val);
3038
+ }
3039
+ }
3040
+ return { query, body, response, bodyZodText, queryZodText };
3041
+ }
3042
+
3043
+ // src/discovery/contracts-fast.ts
3044
+ async function discoverContractsFast(opts) {
3045
+ const { cwd, glob, tsconfig } = opts;
3046
+ const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
3047
+ let project;
3048
+ try {
3049
+ project = new Project3({
3050
+ tsConfigFilePath: tsconfigPath,
3051
+ skipAddingFilesFromTsConfig: true,
3052
+ skipLoadingLibFiles: true,
3053
+ skipFileDependencyResolution: true
3054
+ });
3055
+ } catch {
3056
+ project = new Project3({
3057
+ skipAddingFilesFromTsConfig: true,
3058
+ skipLoadingLibFiles: true,
3059
+ skipFileDependencyResolution: true,
3060
+ compilerOptions: {
3061
+ allowJs: true,
3062
+ resolveJsonModule: false,
3063
+ strict: false
3064
+ }
3065
+ });
3066
+ }
3067
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3068
+ for (const f of files) {
3069
+ project.addSourceFileAtPath(f);
3070
+ }
3071
+ const routes = [];
3072
+ setDiscoveryContext(project, {
3073
+ projectRoot: cwd,
3074
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3075
+ });
3076
+ for (const sourceFile of project.getSourceFiles()) {
3077
+ routes.push(...extractFromSourceFile(sourceFile, project));
3521
3078
  }
3522
- const queryClass = resolveParamClass(method, "Query", sourceFile, project);
3523
- if (queryClass) {
3524
- const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
3525
- queryZodText = result.schemaText;
3526
- for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
3527
- formWarnings.push(...result.warnings);
3528
- querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
3079
+ return routes;
3080
+ }
3081
+ function decoratorStringArg(decoratorExpr) {
3082
+ if (!decoratorExpr) return void 0;
3083
+ if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3084
+ if (Node8.isArrayLiteralExpression(decoratorExpr)) {
3085
+ const first = decoratorExpr.getElements()[0];
3086
+ if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
3529
3087
  }
3530
- return {
3531
- query,
3532
- body,
3533
- response,
3534
- params: paramsType,
3535
- queryRef,
3536
- bodyRef,
3537
- responseRef,
3538
- filterFields: filterInfo?.fieldNames ?? null,
3539
- filterFieldTypes: filterInfo?.fieldTypes ?? null,
3540
- filterSource: filterInfo?.source ?? null,
3541
- bodyZodText,
3542
- queryZodText,
3543
- formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
3544
- formWarnings,
3545
- bodySchema,
3546
- querySchema
3547
- };
3088
+ return void 0;
3548
3089
  }
3549
- function resolveParamClass(method, decoratorName, sourceFile, project) {
3550
- for (const param of method.getParameters()) {
3551
- if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
3552
- const typeNode = param.getTypeNode();
3553
- if (!typeNode) continue;
3554
- const text = typeNode.getText().replace(/\[\]$/, "");
3555
- if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
3556
- const resolved = findType(text, sourceFile, project);
3557
- if (resolved && resolved.kind === "class") {
3558
- return { decl: resolved.decl, file: resolved.file };
3559
- }
3090
+ function deriveClassSegment(className) {
3091
+ const noSuffix = className.replace(/Controller$/, "");
3092
+ if (!noSuffix) {
3093
+ throw new Error(
3094
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3095
+ );
3560
3096
  }
3561
- return null;
3097
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3098
+ }
3099
+ function resolveRouteName(className, methodName, classAs, methodAs) {
3100
+ const classPortion = classAs ?? deriveClassSegment(className);
3101
+ const methodPortion = methodAs ?? methodName;
3102
+ return `${classPortion}.${methodPortion}`;
3103
+ }
3104
+ function joinPaths(prefix, suffix) {
3105
+ if (!prefix && !suffix) return "/";
3106
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3107
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3108
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3109
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3110
+ const combined = p + s;
3111
+ return combined === "" ? "/" : combined;
3112
+ }
3113
+ function extractParams(path) {
3114
+ const matches = path.matchAll(/:(\w+)/g);
3115
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3562
3116
  }
3563
3117
  var HTTP_METHOD_DECORATORS = {
3564
3118
  Get: "GET",
@@ -3570,176 +3124,186 @@ var HTTP_METHOD_DECORATORS = {
3570
3124
  Head: "HEAD",
3571
3125
  All: "ALL"
3572
3126
  };
3127
+ function resolveVerb(method) {
3128
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3129
+ const httpDecorator = method.getDecorator(decoratorName);
3130
+ if (httpDecorator) {
3131
+ const httpArgs = httpDecorator.getArguments();
3132
+ const pathArg = httpArgs[0];
3133
+ return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3134
+ }
3135
+ }
3136
+ return null;
3137
+ }
3138
+ function readAsDecorator(node, label) {
3139
+ const asDecorator = node.getDecorator("As");
3140
+ if (!asDecorator) return void 0;
3141
+ const asName = decoratorStringArg(asDecorator.getArguments()[0]);
3142
+ if (!asName) {
3143
+ throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
3144
+ }
3145
+ return asName;
3146
+ }
3147
+ function buildRoute(args) {
3148
+ const {
3149
+ className,
3150
+ methodName,
3151
+ resolvedMethod,
3152
+ combinedPath,
3153
+ classAs,
3154
+ methodAs,
3155
+ sourceFile,
3156
+ seenNames,
3157
+ contractSource
3158
+ } = args;
3159
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3160
+ const qualifiedRef = `${className}.${methodName}`;
3161
+ const existing = seenNames.get(routeName);
3162
+ if (existing !== void 0) {
3163
+ throw new Error(
3164
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3165
+ );
3166
+ }
3167
+ seenNames.set(routeName, qualifiedRef);
3168
+ return {
3169
+ method: resolvedMethod,
3170
+ path: combinedPath,
3171
+ name: routeName,
3172
+ params: extractParams(combinedPath),
3173
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3174
+ contract: { contractSource }
3175
+ };
3176
+ }
3177
+ function extractContractRoute(args) {
3178
+ const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
3179
+ const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3180
+ if (!firstDecoratorArg) return null;
3181
+ let contractDef = null;
3182
+ let bodyZodRef = null;
3183
+ let queryZodRef = null;
3184
+ if (Node8.isCallExpression(firstDecoratorArg)) {
3185
+ contractDef = parseDefineContractCall(firstDecoratorArg);
3186
+ } else if (Node8.isIdentifier(firstDecoratorArg)) {
3187
+ const identName = firstDecoratorArg.getText();
3188
+ const varDecl = sourceFile.getVariableDeclaration(identName);
3189
+ if (!varDecl) {
3190
+ console.warn(
3191
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3192
+ );
3193
+ return null;
3194
+ }
3195
+ const initializer = varDecl.getInitializer();
3196
+ if (!initializer) return null;
3197
+ contractDef = parseDefineContractCall(initializer);
3198
+ if (contractDef && varDecl.isExported()) {
3199
+ const filePath = sourceFile.getFilePath();
3200
+ if (contractDef.body !== null) {
3201
+ bodyZodRef = { name: `${identName}.body`, filePath };
3202
+ }
3203
+ if (contractDef.query !== null) {
3204
+ queryZodRef = { name: `${identName}.query`, filePath };
3205
+ }
3206
+ }
3207
+ } else {
3208
+ console.warn(
3209
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3210
+ );
3211
+ return null;
3212
+ }
3213
+ if (!contractDef) return null;
3214
+ if (!verb) return null;
3215
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
3216
+ const methodName = method.getName();
3217
+ const classAs = readAsDecorator(cls, `class ${className}`);
3218
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3219
+ return buildRoute({
3220
+ className,
3221
+ methodName,
3222
+ resolvedMethod: verb.httpMethod,
3223
+ combinedPath: resolvedPath,
3224
+ classAs,
3225
+ methodAs,
3226
+ sourceFile,
3227
+ seenNames,
3228
+ contractSource: {
3229
+ query: contractDef.query,
3230
+ body: contractDef.body,
3231
+ response: contractDef.response,
3232
+ // Path A: capture both the importable ref and the raw text. The emitter
3233
+ // prefers inlining the text (client-safe — re-exporting from a controller
3234
+ // would drag server-only deps into the client bundle).
3235
+ bodyZodRef,
3236
+ bodyZodText: contractDef.bodyZodText,
3237
+ queryZodRef,
3238
+ queryZodText: contractDef.queryZodText
3239
+ }
3240
+ });
3241
+ }
3242
+ function extractDtoRoute(args) {
3243
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
3244
+ if (!verb) return null;
3245
+ const combined = joinPaths(prefix, verb.handlerPath);
3246
+ const methodName = method.getName();
3247
+ const classAs = readAsDecorator(cls, `class ${className}`);
3248
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3249
+ const dtoContract = extractDtoContract(method, sourceFile, project);
3250
+ return buildRoute({
3251
+ className,
3252
+ methodName,
3253
+ resolvedMethod: verb.httpMethod,
3254
+ combinedPath: combined,
3255
+ classAs,
3256
+ methodAs,
3257
+ sourceFile,
3258
+ seenNames,
3259
+ contractSource: {
3260
+ query: dtoContract?.query ?? null,
3261
+ body: dtoContract?.body ?? null,
3262
+ response: dtoContract?.response ?? "unknown",
3263
+ queryRef: dtoContract?.queryRef ?? null,
3264
+ bodyRef: dtoContract?.bodyRef ?? null,
3265
+ responseRef: dtoContract?.responseRef ?? null,
3266
+ filterFields: dtoContract?.filterFields ?? null,
3267
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3268
+ filterSource: dtoContract?.filterSource ?? null,
3269
+ formWarnings: dtoContract?.formWarnings ?? [],
3270
+ bodySchema: dtoContract?.bodySchema ?? null,
3271
+ querySchema: dtoContract?.querySchema ?? null
3272
+ }
3273
+ });
3274
+ }
3573
3275
  function extractFromSourceFile(sourceFile, project) {
3574
3276
  const routes = [];
3575
3277
  const seenNames = /* @__PURE__ */ new Map();
3576
- const classes = sourceFile.getClasses();
3577
- for (const cls of classes) {
3278
+ for (const cls of sourceFile.getClasses()) {
3578
3279
  const controllerDecorator = cls.getDecorator("Controller");
3579
3280
  if (!controllerDecorator) continue;
3580
- const controllerArgs = controllerDecorator.getArguments();
3581
- const firstArg3 = controllerArgs[0];
3582
- const prefix = decoratorStringArg(firstArg3) ?? "";
3281
+ const firstArg2 = controllerDecorator.getArguments()[0];
3282
+ const prefix = decoratorStringArg(firstArg2) ?? "";
3583
3283
  const className = cls.getName() ?? "Unknown";
3584
3284
  for (const method of cls.getMethods()) {
3585
- let httpMethod;
3586
- let handlerPath = "";
3587
- for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3588
- const httpDecorator = method.getDecorator(decoratorName);
3589
- if (httpDecorator) {
3590
- httpMethod = verb;
3591
- const httpArgs = httpDecorator.getArguments();
3592
- const pathArg = httpArgs[0];
3593
- handlerPath = decoratorStringArg(pathArg) ?? "";
3594
- break;
3595
- }
3596
- }
3285
+ const verb = resolveVerb(method);
3597
3286
  const applyContractDecorator = method.getDecorator("ApplyContract");
3598
- if (applyContractDecorator) {
3599
- const decoratorArgs = applyContractDecorator.getArguments();
3600
- const firstDecoratorArg = decoratorArgs[0];
3601
- if (!firstDecoratorArg) continue;
3602
- let contractDef = null;
3603
- let bodyZodRef = null;
3604
- let queryZodRef = null;
3605
- if (Node7.isCallExpression(firstDecoratorArg)) {
3606
- contractDef = parseDefineContractCall(firstDecoratorArg);
3607
- } else if (Node7.isIdentifier(firstDecoratorArg)) {
3608
- const identName = firstDecoratorArg.getText();
3609
- const varDecl = sourceFile.getVariableDeclaration(identName);
3610
- if (!varDecl) {
3611
- console.warn(
3612
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3613
- );
3614
- continue;
3615
- }
3616
- const initializer = varDecl.getInitializer();
3617
- if (!initializer) continue;
3618
- contractDef = parseDefineContractCall(initializer);
3619
- if (contractDef && varDecl.isExported()) {
3620
- const filePath = sourceFile.getFilePath();
3621
- if (contractDef.body !== null) {
3622
- bodyZodRef = { name: `${identName}.body`, filePath };
3623
- }
3624
- if (contractDef.query !== null) {
3625
- queryZodRef = { name: `${identName}.query`, filePath };
3626
- }
3627
- }
3628
- } else {
3629
- console.warn(
3630
- `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3631
- );
3632
- continue;
3633
- }
3634
- if (!contractDef) continue;
3635
- if (!httpMethod) continue;
3636
- const resolvedMethod = httpMethod;
3637
- const resolvedPath = joinPaths(prefix, handlerPath);
3638
- const combined = resolvedPath;
3639
- const params = extractParams(combined);
3640
- const methodName = method.getName();
3641
- const classAsDecorator = cls.getDecorator("As");
3642
- let classAs;
3643
- if (classAsDecorator) {
3644
- const classAsArgs = classAsDecorator.getArguments();
3645
- const classAsName = decoratorStringArg(classAsArgs[0]);
3646
- if (!classAsName) {
3647
- throw new Error(
3648
- `@As decorator on class ${className} must have a non-empty string argument.`
3649
- );
3650
- }
3651
- classAs = classAsName;
3652
- }
3653
- const methodAsDecorator = method.getDecorator("As");
3654
- let methodAs;
3655
- if (methodAsDecorator) {
3656
- const methodAsArgs = methodAsDecorator.getArguments();
3657
- const methodAsName = decoratorStringArg(methodAsArgs[0]);
3658
- if (!methodAsName) {
3659
- throw new Error(
3660
- `@As decorator on ${className}.${methodName} must have a non-empty string argument.`
3661
- );
3662
- }
3663
- methodAs = methodAsName;
3664
- }
3665
- const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3666
- const qualifiedRef = `${className}.${methodName}`;
3667
- const existing = seenNames.get(routeName);
3668
- if (existing !== void 0) {
3669
- throw new Error(
3670
- `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3671
- );
3672
- }
3673
- seenNames.set(routeName, qualifiedRef);
3674
- routes.push({
3675
- method: resolvedMethod,
3676
- path: combined,
3677
- name: routeName,
3678
- params,
3679
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3680
- contract: {
3681
- contractSource: {
3682
- query: contractDef.query,
3683
- body: contractDef.body,
3684
- response: contractDef.response,
3685
- // Path A: capture both the importable ref and the raw text. The
3686
- // emitter prefers inlining the text (client-safe — re-exporting from
3687
- // a controller would drag server-only deps into the client bundle).
3688
- bodyZodRef,
3689
- bodyZodText: contractDef.bodyZodText,
3690
- queryZodRef,
3691
- queryZodText: contractDef.queryZodText
3692
- }
3693
- }
3694
- });
3695
- } else {
3696
- if (!httpMethod) continue;
3697
- const combined = joinPaths(prefix, handlerPath);
3698
- const params = extractParams(combined);
3699
- const methodName = method.getName();
3700
- const classAsDecorator = cls.getDecorator("As");
3701
- let classAs;
3702
- if (classAsDecorator) {
3703
- const classAsArgs = classAsDecorator.getArguments();
3704
- const classAsName = decoratorStringArg(classAsArgs[0]);
3705
- if (classAsName) classAs = classAsName;
3706
- }
3707
- const methodAsDecorator = method.getDecorator("As");
3708
- let methodAs;
3709
- if (methodAsDecorator) {
3710
- const methodAsArgs = methodAsDecorator.getArguments();
3711
- const methodAsName = decoratorStringArg(methodAsArgs[0]);
3712
- if (methodAsName) methodAs = methodAsName;
3713
- }
3714
- const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3715
- const dtoContract = extractDtoContract(method, sourceFile, project);
3716
- routes.push({
3717
- method: httpMethod,
3718
- path: combined,
3719
- name: routeName,
3720
- params,
3721
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3722
- contract: {
3723
- contractSource: {
3724
- query: dtoContract?.query ?? null,
3725
- body: dtoContract?.body ?? null,
3726
- response: dtoContract?.response ?? "unknown",
3727
- queryRef: dtoContract?.queryRef ?? null,
3728
- bodyRef: dtoContract?.bodyRef ?? null,
3729
- responseRef: dtoContract?.responseRef ?? null,
3730
- filterFields: dtoContract?.filterFields ?? null,
3731
- filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3732
- filterSource: dtoContract?.filterSource ?? null,
3733
- bodyZodText: dtoContract?.bodyZodText ?? null,
3734
- queryZodText: dtoContract?.queryZodText ?? null,
3735
- formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
3736
- formWarnings: dtoContract?.formWarnings ?? [],
3737
- bodySchema: dtoContract?.bodySchema ?? null,
3738
- querySchema: dtoContract?.querySchema ?? null
3739
- }
3740
- }
3741
- });
3742
- }
3287
+ const route = applyContractDecorator ? extractContractRoute({
3288
+ cls,
3289
+ method,
3290
+ applyContractDecorator,
3291
+ verb,
3292
+ prefix,
3293
+ className,
3294
+ sourceFile,
3295
+ seenNames
3296
+ }) : extractDtoRoute({
3297
+ cls,
3298
+ method,
3299
+ verb,
3300
+ prefix,
3301
+ className,
3302
+ sourceFile,
3303
+ project,
3304
+ seenNames
3305
+ });
3306
+ if (route) routes.push(route);
3743
3307
  }
3744
3308
  }
3745
3309
  return routes;
@@ -3748,7 +3312,7 @@ function extractFromSourceFile(sourceFile, project) {
3748
3312
  // src/watch/lock-file.ts
3749
3313
  import { open } from "fs/promises";
3750
3314
  import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3751
- import { join as join11 } from "path";
3315
+ import { join as join12 } from "path";
3752
3316
  var LOCK_FILE = ".watcher.lock";
3753
3317
  function isProcessAlive(pid) {
3754
3318
  try {
@@ -3760,7 +3324,7 @@ function isProcessAlive(pid) {
3760
3324
  }
3761
3325
  async function acquireLock(outDir) {
3762
3326
  await mkdir8(outDir, { recursive: true });
3763
- const lockPath = join11(outDir, LOCK_FILE);
3327
+ const lockPath = join12(outDir, LOCK_FILE);
3764
3328
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3765
3329
  try {
3766
3330
  const fd = await open(lockPath, "wx");
@@ -3800,7 +3364,7 @@ async function watch(config, onChange) {
3800
3364
  if (lock === null) {
3801
3365
  let holderPid = "unknown";
3802
3366
  try {
3803
- const raw = await readFile3(join12(config.codegen.outDir, ".watcher.lock"), "utf8");
3367
+ const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
3804
3368
  const data = JSON.parse(raw);
3805
3369
  if (data.pid !== void 0) holderPid = String(data.pid);
3806
3370
  } catch {
@@ -3828,7 +3392,7 @@ async function watch(config, onChange) {
3828
3392
  }
3829
3393
  let pagesDebounceTimer;
3830
3394
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3831
- const pagesWatcher = chokidar.watch(join12(config.codegen.cwd, pagesGlob), {
3395
+ const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
3832
3396
  ignoreInitial: true,
3833
3397
  persistent: true,
3834
3398
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3854,7 +3418,7 @@ async function watch(config, onChange) {
3854
3418
  pagesWatcher.on("change", schedulePagesRegenerate);
3855
3419
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3856
3420
  let contractsDebounceTimer;
3857
- const contractsWatcher = chokidar.watch(join12(config.codegen.cwd, config.contracts.glob), {
3421
+ const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
3858
3422
  ignoreInitial: true,
3859
3423
  persistent: true,
3860
3424
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3884,7 +3448,7 @@ async function watch(config, onChange) {
3884
3448
  contractsWatcher.on("add", scheduleContractsRegenerate);
3885
3449
  contractsWatcher.on("change", scheduleContractsRegenerate);
3886
3450
  contractsWatcher.on("unlink", scheduleContractsRegenerate);
3887
- const formsWatcher = chokidar.watch(join12(config.codegen.cwd, config.forms.watch), {
3451
+ const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
3888
3452
  ignoreInitial: true,
3889
3453
  persistent: true,
3890
3454
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3911,7 +3475,7 @@ async function watch(config, onChange) {
3911
3475
  }
3912
3476
 
3913
3477
  // src/index.ts
3914
- var VERSION = "0.2.1";
3478
+ var VERSION = "0.4.0";
3915
3479
 
3916
3480
  // src/cli/codegen.ts
3917
3481
  async function runCodegen(opts = {}) {
@@ -3939,15 +3503,51 @@ async function runCodegen(opts = {}) {
3939
3503
 
3940
3504
  // src/cli/doctor.ts
3941
3505
  import { execFileSync as execFileSync2 } from "child_process";
3942
- import { appendFileSync, existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3943
- import { join as join14 } from "path";
3506
+ import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
3507
+ import { join as join15 } from "path";
3944
3508
 
3945
3509
  // src/cli/init.ts
3946
3510
  import { execFileSync } from "child_process";
3947
- import { readFileSync as readFileSync2, writeFileSync } from "fs";
3511
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3948
3512
  import { access as access2, mkdir as mkdir9, readFile as readFile4, writeFile as writeFile8 } from "fs/promises";
3949
- import { join as join13 } from "path";
3513
+ import { join as join14 } from "path";
3950
3514
  import { createInterface } from "readline";
3515
+
3516
+ // src/cli/patch-utils.ts
3517
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
3518
+ function patchJsonFile(filePath, mutator, parse = (raw) => raw) {
3519
+ let raw;
3520
+ try {
3521
+ raw = readFileSync2(filePath, "utf8");
3522
+ } catch {
3523
+ return "skipped";
3524
+ }
3525
+ const json = JSON.parse(parse(raw));
3526
+ if (!mutator(json)) return "already";
3527
+ writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
3528
+ `, "utf8");
3529
+ return "patched";
3530
+ }
3531
+ function findAfterLastImport(content) {
3532
+ const lastImportIndex = content.lastIndexOf("\nimport ");
3533
+ if (lastImportIndex !== -1) {
3534
+ const endOfLine = content.indexOf("\n", lastImportIndex + 1);
3535
+ return endOfLine !== -1 ? endOfLine + 1 : content.length;
3536
+ }
3537
+ if (content.startsWith("import ")) {
3538
+ const endOfLine = content.indexOf("\n");
3539
+ return endOfLine !== -1 ? endOfLine + 1 : content.length;
3540
+ }
3541
+ return 0;
3542
+ }
3543
+ function insertImport(content, stmt) {
3544
+ const insertAt = findAfterLastImport(content);
3545
+ if (insertAt <= 0) return content;
3546
+ return `${content.slice(0, insertAt)}${stmt}
3547
+ ${content.slice(insertAt)}`;
3548
+ }
3549
+
3550
+ // src/cli/init.ts
3951
3551
  var GITIGNORE_ENTRY = ".nestjs-inertia/";
3952
3552
  var green = (s) => `\x1B[32m${s}\x1B[0m`;
3953
3553
  var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
@@ -3972,7 +3572,7 @@ ${bold(title)}`);
3972
3572
  }
3973
3573
  async function readPackageJson(cwd) {
3974
3574
  try {
3975
- const raw = await readFile4(join13(cwd, "package.json"), "utf8");
3575
+ const raw = await readFile4(join14(cwd, "package.json"), "utf8");
3976
3576
  return JSON.parse(raw);
3977
3577
  } catch {
3978
3578
  return {};
@@ -4003,7 +3603,7 @@ async function detectTemplateEngine(cwd) {
4003
3603
  async function detectPackageManager(cwd) {
4004
3604
  async function exists(file) {
4005
3605
  try {
4006
- await access2(join13(cwd, file));
3606
+ await access2(join14(cwd, file));
4007
3607
  return true;
4008
3608
  } catch {
4009
3609
  return false;
@@ -4050,7 +3650,7 @@ async function writeIfNotExists(filePath, content, label) {
4050
3650
  logCreated(label);
4051
3651
  }
4052
3652
  async function handleViteConfig(cwd, framework) {
4053
- const filePath = join13(cwd, "vite.config.ts");
3653
+ const filePath = join14(cwd, "vite.config.ts");
4054
3654
  if (await fileExists2(filePath)) {
4055
3655
  const existing = await readFile4(filePath, "utf8");
4056
3656
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
@@ -4108,7 +3708,7 @@ function installDeps(pkgManager, deps, dev) {
4108
3708
  }
4109
3709
  }
4110
3710
  async function patchPackageJsonScripts(cwd, scripts) {
4111
- const pkgPath = join13(cwd, "package.json");
3711
+ const pkgPath = join14(cwd, "package.json");
4112
3712
  let pkg = {};
4113
3713
  try {
4114
3714
  pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
@@ -4133,32 +3733,16 @@ async function patchPackageJsonScripts(cwd, scripts) {
4133
3733
  await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
4134
3734
  `, "utf8");
4135
3735
  }
4136
- function findAfterLastImport(content) {
4137
- const lastImportIndex = content.lastIndexOf("\nimport ");
4138
- if (lastImportIndex !== -1) {
4139
- const endOfLine = content.indexOf("\n", lastImportIndex + 1);
4140
- return endOfLine !== -1 ? endOfLine + 1 : content.length;
4141
- }
4142
- if (content.startsWith("import ")) {
4143
- const endOfLine = content.indexOf("\n");
4144
- return endOfLine !== -1 ? endOfLine + 1 : content.length;
4145
- }
4146
- return 0;
4147
- }
4148
3736
  function patchAppModule(filePath, rootView) {
4149
3737
  let content;
4150
3738
  try {
4151
- content = readFileSync2(filePath, "utf8");
3739
+ content = readFileSync3(filePath, "utf8");
4152
3740
  } catch {
4153
3741
  return "skipped";
4154
3742
  }
4155
3743
  let changed = false;
4156
3744
  if (!content.includes("InertiaModule")) {
4157
- const insertAt = findAfterLastImport(content);
4158
- if (insertAt > 0) {
4159
- content = `${content.slice(0, insertAt)}import { InertiaModule } from '@dudousxd/nestjs-inertia';
4160
- ${content.slice(insertAt)}`;
4161
- }
3745
+ content = insertImport(content, "import { InertiaModule } from '@dudousxd/nestjs-inertia';");
4162
3746
  if (!content.includes("from 'node:path'") && !content.includes('from "node:path"')) {
4163
3747
  const insertAt2 = findAfterLastImport(content);
4164
3748
  content = `${content.slice(0, insertAt2)}import { resolve } from 'node:path';
@@ -4176,11 +3760,7 @@ ${indent}}),${content.slice(bracketPos)}`;
4176
3760
  }
4177
3761
  }
4178
3762
  if (!content.includes("HomeController")) {
4179
- const insertAt = findAfterLastImport(content);
4180
- if (insertAt > 0) {
4181
- content = `${content.slice(0, insertAt)}import { HomeController } from './home.controller';
4182
- ${content.slice(insertAt)}`;
4183
- }
3763
+ content = insertImport(content, "import { HomeController } from './home.controller';");
4184
3764
  const controllersMatch = content.match(/controllers\s*:\s*\[/);
4185
3765
  if (controllersMatch?.index !== void 0) {
4186
3766
  const bracketPos = content.indexOf("[", controllersMatch.index) + 1;
@@ -4191,22 +3771,21 @@ ${indent}HomeController,${content.slice(bracketPos)}`;
4191
3771
  }
4192
3772
  }
4193
3773
  if (!changed) return "already";
4194
- writeFileSync(filePath, content, "utf8");
3774
+ writeFileSync2(filePath, content, "utf8");
4195
3775
  return "patched";
4196
3776
  }
4197
3777
  function patchMainTs(filePath) {
4198
3778
  let content;
4199
3779
  try {
4200
- content = readFileSync2(filePath, "utf8");
3780
+ content = readFileSync3(filePath, "utf8");
4201
3781
  } catch {
4202
3782
  return "skipped";
4203
3783
  }
4204
3784
  if (content.includes("setupInertiaVite")) return "already";
4205
- const insertAt = findAfterLastImport(content);
4206
- if (insertAt > 0) {
4207
- content = `${content.slice(0, insertAt)}import { setupInertiaVite } from '@dudousxd/nestjs-inertia-vite';
4208
- ${content.slice(insertAt)}`;
4209
- }
3785
+ content = insertImport(
3786
+ content,
3787
+ "import { setupInertiaVite } from '@dudousxd/nestjs-inertia-vite';"
3788
+ );
4210
3789
  const createMatch = content.match(
4211
3790
  /(?:const|let)\s+(\w+)\s*=\s*await\s+NestFactory\.create[^;]+;/
4212
3791
  );
@@ -4223,7 +3802,7 @@ ${content.slice(insertAt)}`;
4223
3802
  });`;
4224
3803
  content = `${content.slice(0, insertAfterPos)}
4225
3804
  ${viteSetup}${content.slice(insertAfterPos)}`;
4226
- writeFileSync(filePath, content, "utf8");
3805
+ writeFileSync2(filePath, content, "utf8");
4227
3806
  return "patched";
4228
3807
  }
4229
3808
  function configTemplate(framework) {
@@ -4446,107 +4025,84 @@ export class HomeController {
4446
4025
  }
4447
4026
  `;
4448
4027
  function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4449
- const filePath = join13(cwd, filename);
4450
- let raw;
4451
- try {
4452
- raw = readFileSync2(filePath, "utf8");
4453
- } catch {
4454
- return "skipped";
4455
- }
4456
- const stripped = raw.replace(/\/\/.*$/gm, "");
4457
- const json = JSON.parse(stripped);
4458
- const exclude = json.exclude ?? [];
4459
- if (exclude.includes(dir)) return "already";
4460
- exclude.push(dir);
4461
- json.exclude = exclude;
4462
- writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
4463
- `, "utf8");
4464
- return "patched";
4028
+ const filePath = join14(cwd, filename);
4029
+ return patchJsonFile(
4030
+ filePath,
4031
+ (json) => {
4032
+ const exclude = json.exclude ?? [];
4033
+ if (exclude.includes(dir)) return false;
4034
+ exclude.push(dir);
4035
+ json.exclude = exclude;
4036
+ return true;
4037
+ },
4038
+ // Strip single-line comments before JSON.parse
4039
+ (raw) => raw.replace(/\/\/.*$/gm, "")
4040
+ );
4465
4041
  }
4466
4042
  function patchNestCliJson(cwd, shellDir) {
4467
- const filePath = join13(cwd, "nest-cli.json");
4468
- let raw;
4469
- try {
4470
- raw = readFileSync2(filePath, "utf8");
4471
- } catch {
4472
- return "skipped";
4473
- }
4474
- const json = JSON.parse(raw);
4475
- const compiler = json.compilerOptions ?? {};
4476
- const assets = compiler.assets ?? [];
4477
- const alreadyHas = assets.some((a) => {
4478
- if (typeof a === "string") return a.includes(shellDir);
4479
- return String(a.include ?? "").includes(shellDir);
4480
- });
4481
- if (alreadyHas) return "already";
4482
- assets.push({
4483
- include: `../${shellDir}/**/*`,
4484
- outDir: `dist/${shellDir}`,
4485
- watchAssets: true
4043
+ const filePath = join14(cwd, "nest-cli.json");
4044
+ return patchJsonFile(filePath, (json) => {
4045
+ const compiler = json.compilerOptions ?? {};
4046
+ const assets = compiler.assets ?? [];
4047
+ const alreadyHas = assets.some((a) => {
4048
+ if (typeof a === "string") return a.includes(shellDir);
4049
+ return String(a.include ?? "").includes(shellDir);
4050
+ });
4051
+ if (alreadyHas) return false;
4052
+ assets.push({
4053
+ include: `../${shellDir}/**/*`,
4054
+ outDir: `dist/${shellDir}`,
4055
+ watchAssets: true
4056
+ });
4057
+ compiler.assets = assets;
4058
+ json.compilerOptions = compiler;
4059
+ return true;
4486
4060
  });
4487
- compiler.assets = assets;
4488
- json.compilerOptions = compiler;
4489
- writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
4490
- `, "utf8");
4491
- return "patched";
4492
4061
  }
4493
- async function runInit(opts = {}) {
4494
- const cwd = opts.cwd ?? process.cwd();
4495
- console.log(`
4496
- ${bold("nestjs-inertia init")}`);
4497
- let framework = await detectFramework(cwd);
4498
- if (!framework) {
4499
- framework = await promptFramework();
4500
- }
4501
- const engine = await detectTemplateEngine(cwd);
4502
- const engineLabel = engine === "html" ? "plain HTML" : engine;
4503
- const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
4504
- console.log(`
4505
- Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
4506
- const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
4507
- const entryExt = framework === "react" ? "tsx" : "ts";
4508
- const pageExt = framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte";
4062
+ async function scaffoldFiles(ctx) {
4063
+ const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
4509
4064
  logSection("Scaffold files");
4510
4065
  await writeIfNotExists(
4511
- join13(cwd, "nestjs-inertia.config.ts"),
4066
+ join14(cwd, "nestjs-inertia.config.ts"),
4512
4067
  configTemplate(framework),
4513
4068
  "nestjs-inertia.config.ts"
4514
4069
  );
4515
- await writeIfNotExists(join13(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4070
+ await writeIfNotExists(join14(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4516
4071
  await writeIfNotExists(
4517
- join13(cwd, "tsconfig.inertia.json"),
4072
+ join14(cwd, "tsconfig.inertia.json"),
4518
4073
  TSCONFIG_INERTIA_TEMPLATE,
4519
4074
  "tsconfig.inertia.json"
4520
4075
  );
4521
4076
  await writeIfNotExists(
4522
- join13(cwd, "inertia", "tsconfig.json"),
4077
+ join14(cwd, "inertia", "tsconfig.json"),
4523
4078
  INERTIA_TSCONFIG_TEMPLATE,
4524
4079
  "inertia/tsconfig.json"
4525
4080
  );
4526
4081
  await writeIfNotExists(
4527
- join13(cwd, "inertia", shellFileName),
4082
+ join14(cwd, "inertia", shellFileName),
4528
4083
  htmlShellTemplate(framework, engine),
4529
4084
  `inertia/${shellFileName}`
4530
4085
  );
4531
4086
  await handleViteConfig(cwd, framework);
4532
4087
  await writeIfNotExists(
4533
- join13(cwd, "inertia", "app", `client.${entryExt}`),
4088
+ join14(cwd, "inertia", "app", `client.${entryExt}`),
4534
4089
  entryPointTemplate(framework),
4535
4090
  `inertia/app/client.${entryExt}`
4536
4091
  );
4537
4092
  await writeIfNotExists(
4538
- join13(cwd, "inertia", "pages", `Home.${pageExt}`),
4093
+ join14(cwd, "inertia", "pages", `Home.${pageExt}`),
4539
4094
  samplePageTemplate(framework),
4540
4095
  `inertia/pages/Home.${pageExt}`
4541
4096
  );
4542
4097
  await writeIfNotExists(
4543
- join13(cwd, "src", "home.controller.ts"),
4098
+ join14(cwd, "src", "home.controller.ts"),
4544
4099
  SAMPLE_CONTROLLER,
4545
4100
  "src/home.controller.ts"
4546
4101
  );
4547
- logSection("Patch existing files");
4548
- const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
4549
- const appModulePath = join13(cwd, "src", "app.module.ts");
4102
+ }
4103
+ function patchServerAppModule(ctx) {
4104
+ const { cwd, rootView } = ctx;
4105
+ const appModulePath = join14(cwd, "src", "app.module.ts");
4550
4106
  const appModuleResult = patchAppModule(appModulePath, rootView);
4551
4107
  if (appModuleResult === "patched") {
4552
4108
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -4558,7 +4114,9 @@ ${bold("nestjs-inertia init")}`);
4558
4114
  } else {
4559
4115
  logWarning("src/app.module.ts not found \u2014 add InertiaModule.forRoot() manually");
4560
4116
  }
4561
- const mainTsPath = join13(cwd, "src", "main.ts");
4117
+ }
4118
+ function patchServerMainTs(ctx) {
4119
+ const mainTsPath = join14(ctx.cwd, "src", "main.ts");
4562
4120
  const mainTsResult = patchMainTs(mainTsPath);
4563
4121
  if (mainTsResult === "patched") {
4564
4122
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -4567,7 +4125,9 @@ ${bold("nestjs-inertia init")}`);
4567
4125
  } else {
4568
4126
  logWarning("src/main.ts not found \u2014 add setupInertiaVite() manually");
4569
4127
  }
4570
- const shellDir = rootView.split("/")[0];
4128
+ }
4129
+ function patchBuildConfigs(ctx) {
4130
+ const { cwd, shellDir } = ctx;
4571
4131
  const nestCliResult = patchNestCliJson(cwd, shellDir);
4572
4132
  if (nestCliResult === "patched") {
4573
4133
  logPatched("nest-cli.json", `added asset copy for ${shellDir}/ \u2192 dist/${shellDir}/`);
@@ -4588,18 +4148,26 @@ ${bold("nestjs-inertia init")}`);
4588
4148
  );
4589
4149
  }
4590
4150
  }
4591
- await patchGitignore(join13(cwd, ".gitignore"));
4151
+ }
4152
+ async function patchGitignoreAndDist(ctx) {
4153
+ const { cwd } = ctx;
4154
+ await patchGitignore(join14(cwd, ".gitignore"));
4592
4155
  const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
4593
4156
  if (tsconfigDistResult === "patched") {
4594
4157
  logPatched("tsconfig.json", "excluded dist/ from server compilation");
4595
4158
  } else if (tsconfigDistResult === "already") {
4596
4159
  console.log(` ${cyan("\u2192")} tsconfig.json ${dim("(dist/ already excluded, skipped)")}`);
4597
4160
  }
4598
- await patchPackageJsonScripts(cwd, {
4161
+ }
4162
+ async function scaffoldPackageScripts(ctx) {
4163
+ await patchPackageJsonScripts(ctx.cwd, {
4599
4164
  "build:client": "vite build",
4600
4165
  "build:ssr": "VITE_SSR=1 vite build --ssr",
4601
4166
  "typecheck:inertia": "tsc --noEmit -p tsconfig.inertia.json"
4602
4167
  });
4168
+ }
4169
+ async function installMissingDeps(ctx) {
4170
+ const { cwd, framework, opts } = ctx;
4603
4171
  logSection("Install dependencies");
4604
4172
  const pkg = await readPackageJson(cwd);
4605
4173
  const installedDeps = allDeps(pkg);
@@ -4633,6 +4201,55 @@ ${bold("nestjs-inertia init")}`);
4633
4201
  installDeps(pkgManager, depsToInstall, false);
4634
4202
  installDeps(pkgManager, devDepsToInstall, true);
4635
4203
  }
4204
+ }
4205
+ var INIT_STEPS = [
4206
+ { label: "scaffold files", run: scaffoldFiles },
4207
+ {
4208
+ label: "patch app.module.ts",
4209
+ run: (ctx) => {
4210
+ logSection("Patch existing files");
4211
+ patchServerAppModule(ctx);
4212
+ }
4213
+ },
4214
+ { label: "patch main.ts", run: patchServerMainTs },
4215
+ { label: "patch build configs", run: patchBuildConfigs },
4216
+ { label: "patch .gitignore + dist exclude", run: patchGitignoreAndDist },
4217
+ { label: "add package.json scripts", run: scaffoldPackageScripts },
4218
+ { label: "install dependencies", run: installMissingDeps }
4219
+ ];
4220
+ async function runInit(opts = {}) {
4221
+ const cwd = opts.cwd ?? process.cwd();
4222
+ console.log(`
4223
+ ${bold("nestjs-inertia init")}`);
4224
+ let framework = await detectFramework(cwd);
4225
+ if (!framework) {
4226
+ framework = await promptFramework();
4227
+ }
4228
+ const engine = await detectTemplateEngine(cwd);
4229
+ const engineLabel = engine === "html" ? "plain HTML" : engine;
4230
+ const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
4231
+ console.log(`
4232
+ Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
4233
+ const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
4234
+ const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
4235
+ const ctx = {
4236
+ opts,
4237
+ cwd,
4238
+ framework,
4239
+ engine,
4240
+ shellFileName,
4241
+ entryExt: framework === "react" ? "tsx" : "ts",
4242
+ pageExt: framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte",
4243
+ rootView,
4244
+ shellDir: rootView.split("/")[0]
4245
+ // e.g. "inertia" from "inertia/index.html"
4246
+ };
4247
+ for (const step of INIT_STEPS) {
4248
+ if (process.env.NESTJS_CODEGEN_DEBUG) {
4249
+ console.log(dim(` \xB7 ${step.label}`));
4250
+ }
4251
+ await step.run(ctx);
4252
+ }
4636
4253
  console.log(`
4637
4254
  ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4638
4255
  `);
@@ -4640,18 +4257,18 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4640
4257
 
4641
4258
  // src/cli/doctor.ts
4642
4259
  function checkFileExists(cwd, file) {
4643
- return existsSync(join14(cwd, file));
4260
+ return existsSync(join15(cwd, file));
4644
4261
  }
4645
4262
  function readJson(path) {
4646
4263
  try {
4647
- const raw = readFileSync3(path, "utf8").replace(/\/\/.*$/gm, "");
4264
+ const raw = readFileSync4(path, "utf8").replace(/\/\/.*$/gm, "");
4648
4265
  return JSON.parse(raw);
4649
4266
  } catch {
4650
4267
  return null;
4651
4268
  }
4652
4269
  }
4653
4270
  function writeJsonField(filePath, dotPath, value) {
4654
- const raw = readFileSync3(filePath, "utf8");
4271
+ const raw = readFileSync4(filePath, "utf8");
4655
4272
  const stripped = raw.replace(/\/\/.*$/gm, "");
4656
4273
  const obj = JSON.parse(stripped);
4657
4274
  let target = obj;
@@ -4671,20 +4288,20 @@ function writeJsonField(filePath, dotPath, value) {
4671
4288
  } else {
4672
4289
  target[lastKey] = value;
4673
4290
  }
4674
- writeFileSync2(filePath, `${JSON.stringify(obj, null, 2)}
4291
+ writeFileSync3(filePath, `${JSON.stringify(obj, null, 2)}
4675
4292
  `, "utf8");
4676
4293
  }
4677
4294
  function getPackageVersion(cwd, pkg) {
4678
4295
  try {
4679
- const pkgJson = readJson(join14(cwd, "node_modules", pkg, "package.json"));
4296
+ const pkgJson = readJson(join15(cwd, "node_modules", pkg, "package.json"));
4680
4297
  return pkgJson?.version ?? null;
4681
4298
  } catch {
4682
4299
  return null;
4683
4300
  }
4684
4301
  }
4685
4302
  function detectPkgManager(cwd) {
4686
- if (existsSync(join14(cwd, "pnpm-lock.yaml"))) return "pnpm";
4687
- if (existsSync(join14(cwd, "yarn.lock"))) return "yarn";
4303
+ if (existsSync(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
4304
+ if (existsSync(join15(cwd, "yarn.lock"))) return "yarn";
4688
4305
  return "npm";
4689
4306
  }
4690
4307
  async function runDoctor(opts) {
@@ -4722,7 +4339,7 @@ async function runDoctor(opts) {
4722
4339
  autoFix: () => runInit({ cwd })
4723
4340
  });
4724
4341
  if (foundShellDir) {
4725
- const nestCliPath = join14(cwd, "nest-cli.json");
4342
+ const nestCliPath = join15(cwd, "nest-cli.json");
4726
4343
  const nestCli = readJson(nestCliPath);
4727
4344
  const compiler = nestCli?.compilerOptions ?? {};
4728
4345
  const assets = compiler.assets ?? [];
@@ -4761,7 +4378,7 @@ async function runDoctor(opts) {
4761
4378
  fix: "Run: nestjs-codegen codegen",
4762
4379
  autoFix: () => runCodegen({ cwd })
4763
4380
  });
4764
- const tsconfigPath = join14(cwd, "tsconfig.json");
4381
+ const tsconfigPath = join15(cwd, "tsconfig.json");
4765
4382
  const tsconfig = readJson(tsconfigPath);
4766
4383
  const paths = tsconfig?.compilerOptions?.paths;
4767
4384
  checks.push({
@@ -4772,7 +4389,7 @@ async function runDoctor(opts) {
4772
4389
  });
4773
4390
  const inertiaDir = foundShellDir ?? "inertia";
4774
4391
  for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
4775
- const tsc = readJson(join14(cwd, tsconfigFile));
4392
+ const tsc = readJson(join15(cwd, tsconfigFile));
4776
4393
  if (!tsc) continue;
4777
4394
  const excl = tsc.exclude ?? [];
4778
4395
  const excludesIt = excl.includes(inertiaDir);
@@ -4798,14 +4415,14 @@ async function runDoctor(opts) {
4798
4415
  }
4799
4416
  });
4800
4417
  }
4801
- const inertiaTsconfigPath = join14(cwd, "tsconfig.inertia.json");
4418
+ const inertiaTsconfigPath = join15(cwd, "tsconfig.inertia.json");
4802
4419
  const inertiaTsconfig = readJson(inertiaTsconfigPath);
4803
4420
  checks.push({
4804
4421
  name: "tsconfig.inertia.json exists",
4805
4422
  pass: !!inertiaTsconfig,
4806
4423
  fix: "Create tsconfig.inertia.json (typechecks inertia/ + .nestjs-inertia/)",
4807
4424
  autoFix: () => {
4808
- writeFileSync2(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
4425
+ writeFileSync3(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
4809
4426
  }
4810
4427
  });
4811
4428
  if (inertiaTsconfig) {
@@ -4850,17 +4467,17 @@ async function runDoctor(opts) {
4850
4467
  fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
4851
4468
  });
4852
4469
  }
4853
- const innerTsconfigPath = join14(cwd, "inertia", "tsconfig.json");
4470
+ const innerTsconfigPath = join15(cwd, "inertia", "tsconfig.json");
4854
4471
  checks.push({
4855
4472
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
4856
4473
  pass: existsSync(innerTsconfigPath),
4857
4474
  fix: "Create inertia/tsconfig.json that extends ../tsconfig.inertia.json",
4858
4475
  autoFix: () => {
4859
- writeFileSync2(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
4476
+ writeFileSync3(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
4860
4477
  }
4861
4478
  });
4862
4479
  if (checkFileExists(cwd, "vite.config.ts")) {
4863
- const viteContent = readFileSync3(join14(cwd, "vite.config.ts"), "utf8");
4480
+ const viteContent = readFileSync4(join15(cwd, "vite.config.ts"), "utf8");
4864
4481
  checks.push({
4865
4482
  name: "vite.config.ts has resolve.alias",
4866
4483
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -4925,8 +4542,8 @@ async function runDoctor(opts) {
4925
4542
  });
4926
4543
  }
4927
4544
  if (checkFileExists(cwd, ".gitignore")) {
4928
- const gitignorePath = join14(cwd, ".gitignore");
4929
- const gitignore = readFileSync3(gitignorePath, "utf8");
4545
+ const gitignorePath = join15(cwd, ".gitignore");
4546
+ const gitignore = readFileSync4(gitignorePath, "utf8");
4930
4547
  checks.push({
4931
4548
  name: ".gitignore includes .nestjs-inertia/",
4932
4549
  pass: gitignore.includes(".nestjs-inertia"),
@@ -4934,7 +4551,7 @@ async function runDoctor(opts) {
4934
4551
  autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
4935
4552
  });
4936
4553
  }
4937
- const pkgJsonPath = join14(cwd, "package.json");
4554
+ const pkgJsonPath = join15(cwd, "package.json");
4938
4555
  const pkgJson = readJson(pkgJsonPath);
4939
4556
  const scripts = pkgJson?.scripts ?? {};
4940
4557
  checks.push({