@dudousxd/nestjs-codegen 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,190 @@ 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 decls = [];
1255
+ const mapEntries = [];
1256
+ let used = false;
1257
+ const renderSource = (src, rename) => {
1258
+ if (src.schema) {
1259
+ const r = adapter.renderModule(src.schema);
1260
+ for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1261
+ return { text: r.schemaText };
1262
+ }
1263
+ if (src.zodText) {
1264
+ if (!acceptsRawZod) {
1265
+ return {
1266
+ text: "",
1267
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1268
+ };
1269
+ }
1270
+ return { text: applyRenames(src.zodText, rename) };
1271
+ }
1272
+ if (src.zodRef) {
1273
+ if (!acceptsRawZod) {
1274
+ return {
1275
+ text: "",
1276
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1277
+ };
1278
+ }
1279
+ const root = refRootIdentifier(src.zodRef.name);
1280
+ const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
1281
+ const member = src.zodRef.name.slice(root.length);
1282
+ return { text: `${alias}${member}` };
1283
+ }
1284
+ return null;
1285
+ };
1286
+ for (const entry of entries) {
1287
+ const block = [];
1288
+ const rename = renamesByEntry.get(entry) ?? null;
1289
+ let bodyConst;
1290
+ if (entry.warnings && entry.warnings.length > 0) {
1291
+ for (const w of entry.warnings) block.push(`// warning: ${w}`);
1292
+ }
1293
+ if (entry.body) {
1294
+ const rendered = renderSource(entry.body, rename);
1295
+ if (rendered?.warn) {
1296
+ block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
1297
+ } else if (rendered) {
1298
+ used = true;
1299
+ bodyConst = `${entry.baseName}BodySchema`;
1300
+ block.push(`export const ${bodyConst} = ${rendered.text};`);
1301
+ block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
1302
+ }
1303
+ }
1304
+ if (entry.query) {
1305
+ const rendered = renderSource(entry.query, rename);
1306
+ if (rendered?.warn) {
1307
+ block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
1308
+ } else if (rendered) {
1309
+ used = true;
1310
+ const queryConst = `${entry.baseName}QuerySchema`;
1311
+ block.push(`export const ${queryConst} = ${rendered.text};`);
1312
+ block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
1313
+ }
1314
+ }
1315
+ if (block.length === 0) continue;
1316
+ decls.push(`// ${entry.routeName}`, ...block, "");
1317
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
1318
+ }
1319
+ if (!used) return null;
1320
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1321
+ if (acceptsRawZod) {
1322
+ const zodImport = config?.zodImport ?? "zod";
1323
+ lines.push(`import { z } from '${zodImport}';`);
1324
+ } else {
1325
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1326
+ }
1327
+ lines.push(...importLines);
1328
+ lines.push("");
1329
+ const allNested = /* @__PURE__ */ new Map();
1330
+ for (const [n, t] of globalSchemas) allNested.set(n, t);
1331
+ for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1332
+ if (allNested.size > 0) {
1333
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
1334
+ for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
1335
+ lines.push("");
1336
+ }
1337
+ lines.push(...decls);
1338
+ lines.push("/** Route name \u2192 body schema map. */");
1339
+ lines.push("export const formSchemas = {");
1340
+ lines.push(...mapEntries);
1341
+ lines.push("} as const;");
1342
+ lines.push("");
1343
+ return lines.join("\n");
1344
+ }
1345
+
1346
+ // src/emit/emit-index.ts
1347
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1348
+ import { join as join7 } from "path";
1349
+ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1350
+ await mkdir4(outDir, { recursive: true });
1351
+ const exports = ["export * from './pages.js';", "export * from './routes.js';"];
1352
+ if (hasContracts) {
1353
+ exports.push("export * from './api.js';");
1354
+ }
1355
+ if (hasForms) {
1356
+ exports.push("export * from './forms.js';");
1357
+ }
1358
+ const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
1359
+ "\n"
1360
+ );
1361
+ await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
1362
+ }
1363
+
1364
+ // src/emit/emit-pages.ts
1463
1365
  import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
1464
- import { join as join7, relative as relative5 } from "path";
1366
+ import { join as join8, relative as relative5 } from "path";
1465
1367
  async function emitPages(pages, outDir, _options = {}) {
1466
1368
  await mkdir5(outDir, { recursive: true });
1467
1369
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -1482,7 +1384,7 @@ ${augBody}
1482
1384
  }
1483
1385
  ${sharedPropsBlock}}
1484
1386
  `;
1485
- await writeFile5(join7(outDir, "pages.d.ts"), content, "utf8");
1387
+ await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
1486
1388
  }
1487
1389
  function buildSharedPropsBlock(sharedProps) {
1488
1390
  if (!sharedProps) return "";
@@ -1513,11 +1415,11 @@ function needsQuotes(name) {
1513
1415
 
1514
1416
  // src/emit/emit-routes.ts
1515
1417
  import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
1516
- import { join as join8 } from "path";
1418
+ import { join as join9 } from "path";
1517
1419
  async function emitRoutes(routes, outDir) {
1518
1420
  await mkdir6(outDir, { recursive: true });
1519
1421
  const content = buildRoutesFile(routes);
1520
- await writeFile6(join8(outDir, "routes.ts"), content, "utf8");
1422
+ await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
1521
1423
  }
1522
1424
  function buildRoutesFile(routes) {
1523
1425
  if (routes.length === 0) {
@@ -1645,30 +1547,7 @@ async function generate(config, inputRoutes = []) {
1645
1547
  propsExport: pagesConfig.propsExport,
1646
1548
  componentNameStrategy: pagesConfig.componentNameStrategy
1647
1549
  });
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
- }
1550
+ const sharedProps = discoverSharedPropsFromConfig(config);
1672
1551
  await emitPages(pages, config.codegen.outDir, {
1673
1552
  propsExport: pagesConfig.propsExport,
1674
1553
  sharedProps
@@ -1692,7 +1571,7 @@ async function generate(config, inputRoutes = []) {
1692
1571
  if (extensions.length > 0) {
1693
1572
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1694
1573
  for (const file of extraFiles) {
1695
- const dest = join9(config.codegen.outDir, file.path);
1574
+ const dest = join10(config.codegen.outDir, file.path);
1696
1575
  await mkdir7(dirname2(dest), { recursive: true });
1697
1576
  await writeFile7(dest, file.contents, "utf8");
1698
1577
  }
@@ -1701,15 +1580,20 @@ async function generate(config, inputRoutes = []) {
1701
1580
 
1702
1581
  // src/watch/watcher.ts
1703
1582
  import { readFile as readFile3 } from "fs/promises";
1704
- import { join as join12 } from "path";
1583
+ import { join as join13 } from "path";
1705
1584
  import chokidar from "chokidar";
1706
1585
 
1707
1586
  // src/discovery/contracts-fast.ts
1708
- import { join as join10, resolve as resolve3 } from "path";
1587
+ import { join as join11, resolve as resolve3 } from "path";
1709
1588
  import fg2 from "fast-glob";
1710
1589
  import {
1711
- Node as Node7,
1712
- Project as Project3,
1590
+ Node as Node8,
1591
+ Project as Project3
1592
+ } from "ts-morph";
1593
+
1594
+ // src/discovery/dto-type-resolver.ts
1595
+ import {
1596
+ Node as Node6,
1713
1597
  SyntaxKind as SyntaxKind3
1714
1598
  } from "ts-morph";
1715
1599
 
@@ -1724,20 +1608,13 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
1724
1608
  import {
1725
1609
  Node as Node2
1726
1610
  } 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;
1611
+ var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1612
+ var _ctxByProject = /* @__PURE__ */ new WeakMap();
1613
+ function setDiscoveryContext(project, ctx) {
1614
+ _ctxByProject.set(project, ctx);
1735
1615
  }
1736
- function _projectRoot() {
1737
- return _ctx.projectRoot;
1738
- }
1739
- function _tsconfigPaths() {
1740
- return _ctx.tsconfigPaths;
1616
+ function _ctxFor(project) {
1617
+ return _ctxByProject.get(project) ?? _EMPTY_CTX;
1741
1618
  }
1742
1619
  var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
1743
1620
  function dbg(...args) {
@@ -1779,7 +1656,7 @@ function findTypeInFile(name, file) {
1779
1656
  }
1780
1657
  return null;
1781
1658
  }
1782
- function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1659
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1783
1660
  if (moduleSpecifier.startsWith(".")) {
1784
1661
  const dir = dirname3(sourceFile.getFilePath());
1785
1662
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
@@ -1789,8 +1666,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1789
1666
  resolve2(dir, moduleSpecifier, "index.ts")
1790
1667
  ];
1791
1668
  }
1792
- const baseUrl = _projectRoot();
1793
- const tsconfigPaths = _tsconfigPaths();
1669
+ const ctx = _ctxFor(project);
1670
+ const baseUrl = ctx.projectRoot;
1671
+ const tsconfigPaths = ctx.tsconfigPaths;
1794
1672
  dbg(
1795
1673
  "resolveModuleSpecifier",
1796
1674
  moduleSpecifier,
@@ -1936,7 +1814,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1936
1814
  if (!namedImport) continue;
1937
1815
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
1938
1816
  if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
1939
- const tsconfigPaths = _tsconfigPaths();
1817
+ const tsconfigPaths = _ctxFor(project).tsconfigPaths;
1940
1818
  const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
1941
1819
  const prefix = p.replace("*", "");
1942
1820
  return moduleSpecifier.startsWith(prefix);
@@ -2289,465 +2167,135 @@ function inSchemaFromDecorator(decorator) {
2289
2167
  return null;
2290
2168
  }
2291
2169
 
2292
- // src/discovery/dto-to-zod.ts
2170
+ // src/discovery/filter-for.ts
2293
2171
  import {
2294
- Node as Node4
2172
+ Node as Node5
2295
2173
  } 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
- };
2174
+
2175
+ // src/discovery/filter-field-types.ts
2176
+ import {
2177
+ Node as Node4,
2178
+ SyntaxKind as SyntaxKind2
2179
+ } from "ts-morph";
2180
+
2181
+ // src/discovery/enum-resolution.ts
2182
+ function resolveEnumValues(name, sourceFile, project) {
2183
+ const resolved = findType(name, sourceFile, project);
2184
+ if (!resolved || resolved.kind !== "enum") return null;
2185
+ let numeric = true;
2186
+ const values = resolved.members.map((m) => {
2187
+ const parsed = JSON.parse(m);
2188
+ if (typeof parsed === "string") numeric = false;
2189
+ return String(parsed);
2190
+ });
2191
+ if (values.length === 0) return null;
2192
+ return { values, numeric };
2345
2193
  }
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(", ")} })`;
2194
+
2195
+ // src/discovery/filter-field-types.ts
2196
+ var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2197
+ var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2198
+ var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2199
+ var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2200
+ var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2201
+ function classifyTypeKeyword(raw) {
2202
+ const t = raw.toLowerCase();
2203
+ if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2204
+ if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2205
+ if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2206
+ if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2207
+ if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2208
+ return null;
2358
2209
  }
2359
- function toObjectKey3(name) {
2360
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2210
+ function markNullable(r, nullable) {
2211
+ return nullable ? { ...r, nullable: true } : r;
2361
2212
  }
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}`);
2213
+ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2214
+ if (Node4.isUnionTypeNode(typeNode)) {
2215
+ let nullable = false;
2216
+ const stringLits = [];
2217
+ const numberLits = [];
2218
+ const others = [];
2219
+ for (const member of typeNode.getTypeNodes()) {
2220
+ const kind = member.getKind();
2221
+ if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
2222
+ nullable = true;
2223
+ continue;
2224
+ }
2225
+ if (Node4.isLiteralTypeNode(member)) {
2226
+ const lit = member.getLiteral();
2227
+ if (Node4.isStringLiteral(lit)) {
2228
+ stringLits.push(lit.getLiteralValue());
2229
+ continue;
2230
+ }
2231
+ if (Node4.isNumericLiteral(lit)) {
2232
+ numberLits.push(lit.getText());
2233
+ continue;
2234
+ }
2235
+ if (lit.getKind() === SyntaxKind2.NullKeyword) {
2236
+ nullable = true;
2237
+ continue;
2238
+ }
2450
2239
  }
2240
+ others.push(member);
2241
+ }
2242
+ if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2243
+ return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2244
+ }
2245
+ if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2246
+ return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2247
+ }
2248
+ if (others.length === 1) {
2249
+ const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2250
+ return markNullable(inner, nullable || inner.nullable === true);
2451
2251
  }
2252
+ return markNullable({ kind: "unknown" }, nullable);
2452
2253
  }
2453
- let expr = base + refinements.join("");
2454
- if (isArrayType && !expr.startsWith("z.array(")) {
2455
- expr = `z.array(${expr})`;
2254
+ switch (typeNode.getKind()) {
2255
+ case SyntaxKind2.StringKeyword:
2256
+ return { kind: "string" };
2257
+ case SyntaxKind2.NumberKeyword:
2258
+ return { kind: "number" };
2259
+ case SyntaxKind2.BooleanKeyword:
2260
+ return { kind: "boolean" };
2261
+ case SyntaxKind2.AnyKeyword:
2262
+ case SyntaxKind2.UnknownKeyword:
2263
+ return { kind: "unknown" };
2264
+ default:
2265
+ break;
2456
2266
  }
2457
- expr = applyPresence2(expr, decorators);
2458
- if (comments.length > 0) {
2459
- expr = `${expr} ${comments.join(" ")}`;
2267
+ if (Node4.isTypeReference(typeNode)) {
2268
+ const refName = typeNode.getTypeName().getText();
2269
+ if (refName === "Date") return { kind: "date" };
2270
+ if (refName === "Record" || refName === "Object") return { kind: "json" };
2271
+ const typeRef = opts?.resolveRef?.(refName) ?? null;
2272
+ const en = resolveEnumValues(refName, sourceFile, project);
2273
+ if (en) {
2274
+ const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
2275
+ return typeRef ? { ...base, typeRef } : base;
2276
+ }
2277
+ if (typeRef) return { kind: "unknown", typeRef };
2278
+ return { kind: "unknown" };
2460
2279
  }
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" };
2280
+ if (Node4.isTypeLiteral(typeNode)) return { kind: "json" };
2281
+ return { kind: "unknown" };
2734
2282
  }
2735
2283
  function enumFromDecoratorArgs(args, sourceFile, project) {
2736
2284
  for (const arg of args) {
2737
- if (Node5.isArrowFunction(arg)) {
2285
+ if (Node4.isArrowFunction(arg)) {
2738
2286
  const body = arg.getBody();
2739
- if (Node5.isIdentifier(body)) {
2287
+ if (Node4.isIdentifier(body)) {
2740
2288
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2741
2289
  if (en) return en;
2742
2290
  }
2743
2291
  }
2744
- if (Node5.isObjectLiteralExpression(arg)) {
2292
+ if (Node4.isObjectLiteralExpression(arg)) {
2745
2293
  const itemsProp = arg.getProperty("items");
2746
- if (itemsProp && Node5.isPropertyAssignment(itemsProp)) {
2294
+ if (itemsProp && Node4.isPropertyAssignment(itemsProp)) {
2747
2295
  const init = itemsProp.getInitializer();
2748
- if (init && Node5.isArrowFunction(init)) {
2296
+ if (init && Node4.isArrowFunction(init)) {
2749
2297
  const body = init.getBody();
2750
- if (Node5.isIdentifier(body)) {
2298
+ if (Node4.isIdentifier(body)) {
2751
2299
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2752
2300
  if (en) return en;
2753
2301
  }
@@ -2770,7 +2318,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2770
2318
  return { kind: "string" };
2771
2319
  }
2772
2320
  for (const arg of args) {
2773
- if (Node5.isStringLiteral(arg)) {
2321
+ if (Node4.isStringLiteral(arg)) {
2774
2322
  const raw = arg.getLiteralValue();
2775
2323
  const kind = classifyTypeKeyword(raw);
2776
2324
  if (kind) {
@@ -2778,11 +2326,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2778
2326
  return { kind };
2779
2327
  }
2780
2328
  }
2781
- if (Node5.isObjectLiteralExpression(arg)) {
2329
+ if (Node4.isObjectLiteralExpression(arg)) {
2782
2330
  const enumProp = arg.getProperty("enum");
2783
- if (enumProp && Node5.isPropertyAssignment(enumProp)) {
2331
+ if (enumProp && Node4.isPropertyAssignment(enumProp)) {
2784
2332
  const init = enumProp.getInitializer();
2785
- if (init && Node5.isIdentifier(init)) {
2333
+ if (init && Node4.isIdentifier(init)) {
2786
2334
  const en = resolveEnumValues(init.getText(), sourceFile, project);
2787
2335
  if (en) {
2788
2336
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -2791,9 +2339,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2791
2339
  }
2792
2340
  }
2793
2341
  const typeProp = arg.getProperty("type");
2794
- if (typeProp && Node5.isPropertyAssignment(typeProp)) {
2342
+ if (typeProp && Node4.isPropertyAssignment(typeProp)) {
2795
2343
  const init = typeProp.getInitializer();
2796
- if (init && Node5.isStringLiteral(init)) {
2344
+ if (init && Node4.isStringLiteral(init)) {
2797
2345
  const kind = classifyTypeKeyword(init.getLiteralValue());
2798
2346
  if (kind) return { kind };
2799
2347
  }
@@ -2829,7 +2377,7 @@ function toFilterFieldType(name, r) {
2829
2377
 
2830
2378
  // src/discovery/filter-for.ts
2831
2379
  function classifyFilterForHint(typeInit) {
2832
- if (Node6.isStringLiteral(typeInit)) {
2380
+ if (Node5.isStringLiteral(typeInit)) {
2833
2381
  switch (typeInit.getLiteralValue()) {
2834
2382
  case "string":
2835
2383
  return { kind: "string" };
@@ -2843,10 +2391,10 @@ function classifyFilterForHint(typeInit) {
2843
2391
  return null;
2844
2392
  }
2845
2393
  }
2846
- if (Node6.isArrayLiteralExpression(typeInit)) {
2394
+ if (Node5.isArrayLiteralExpression(typeInit)) {
2847
2395
  const values = [];
2848
2396
  for (const el of typeInit.getElements()) {
2849
- if (!Node6.isStringLiteral(el)) return null;
2397
+ if (!Node5.isStringLiteral(el)) return null;
2850
2398
  values.push(el.getLiteralValue());
2851
2399
  }
2852
2400
  if (values.length === 0) return null;
@@ -2881,11 +2429,11 @@ function extractFilterForHints(classDecl, project) {
2881
2429
  if (!filterForDec) continue;
2882
2430
  const args = filterForDec.getArguments();
2883
2431
  const keyArg = args[0];
2884
- const inputKey = keyArg && Node6.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2432
+ const inputKey = keyArg && Node5.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2885
2433
  const optsArg = args[1];
2886
- if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
2434
+ if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
2887
2435
  const typeProp = optsArg.getProperty("type");
2888
- if (typeProp && Node6.isPropertyAssignment(typeProp)) {
2436
+ if (typeProp && Node5.isPropertyAssignment(typeProp)) {
2889
2437
  const typeInit = typeProp.getInitializer();
2890
2438
  if (typeInit) {
2891
2439
  const classified = classifyFilterForHint(typeInit);
@@ -2908,14 +2456,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
2908
2456
  const args = filterDecorator.getArguments();
2909
2457
  if (args.length === 0) continue;
2910
2458
  const filterClassArg = args[0];
2911
- if (!filterClassArg || !Node6.isIdentifier(filterClassArg)) continue;
2459
+ if (!filterClassArg || !Node5.isIdentifier(filterClassArg)) continue;
2912
2460
  let source = "query";
2913
2461
  const optionsArg = args[1];
2914
- if (optionsArg && Node6.isObjectLiteralExpression(optionsArg)) {
2462
+ if (optionsArg && Node5.isObjectLiteralExpression(optionsArg)) {
2915
2463
  const sourceProp = optionsArg.getProperty("source");
2916
- if (sourceProp && Node6.isPropertyAssignment(sourceProp)) {
2464
+ if (sourceProp && Node5.isPropertyAssignment(sourceProp)) {
2917
2465
  const init = sourceProp.getInitializer();
2918
- if (init && Node6.isStringLiteral(init) && init.getLiteralValue() === "body") {
2466
+ if (init && Node5.isStringLiteral(init) && init.getLiteralValue() === "body") {
2919
2467
  source = "body";
2920
2468
  }
2921
2469
  }
@@ -2978,22 +2526,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
2978
2526
  const args = dec.getArguments();
2979
2527
  if (args.length === 0) continue;
2980
2528
  const arg = args[0];
2981
- if (Node6.isObjectLiteralExpression(arg)) {
2529
+ if (Node5.isObjectLiteralExpression(arg)) {
2982
2530
  const entityProp = arg.getProperty("entity");
2983
- if (entityProp && Node6.isPropertyAssignment(entityProp)) {
2531
+ if (entityProp && Node5.isPropertyAssignment(entityProp)) {
2984
2532
  const init = entityProp.getInitializer();
2985
- if (init && Node6.isArrowFunction(init)) {
2533
+ if (init && Node5.isArrowFunction(init)) {
2986
2534
  const body = init.getBody();
2987
- if (Node6.isIdentifier(body)) {
2535
+ if (Node5.isIdentifier(body)) {
2988
2536
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
2989
2537
  if (resolved?.kind === "class") return resolved.decl;
2990
2538
  }
2991
2539
  }
2992
2540
  }
2993
2541
  }
2994
- if (Node6.isArrowFunction(arg)) {
2542
+ if (Node5.isArrowFunction(arg)) {
2995
2543
  const body = arg.getBody();
2996
- if (Node6.isIdentifier(body)) {
2544
+ if (Node5.isIdentifier(body)) {
2997
2545
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
2998
2546
  if (resolved?.kind === "class") return resolved.decl;
2999
2547
  }
@@ -3017,11 +2565,11 @@ function extractFilterableEntityFields(filterClass, project) {
3017
2565
  const args = filterableDecorator.getArguments();
3018
2566
  if (args.length === 0) return [];
3019
2567
  const optionsArg = args[0];
3020
- if (!Node6.isObjectLiteralExpression(optionsArg)) return [];
2568
+ if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
3021
2569
  const entityProp = optionsArg.getProperty("entity");
3022
- if (!entityProp || !Node6.isPropertyAssignment(entityProp)) return [];
2570
+ if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
3023
2571
  const entityInit = entityProp.getInitializer();
3024
- if (!entityInit || !Node6.isIdentifier(entityInit)) return [];
2572
+ if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
3025
2573
  const entityName = entityInit.getText();
3026
2574
  const filterSourceFile = filterClass.getSourceFile();
3027
2575
  const resolvedEntity = findType(entityName, filterSourceFile, project);
@@ -3037,17 +2585,17 @@ function extractFilterableEntityFields(filterClass, project) {
3037
2585
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
3038
2586
  if (relationsDecorator) {
3039
2587
  const relArgs = relationsDecorator.getArguments();
3040
- if (relArgs.length > 0 && Node6.isObjectLiteralExpression(relArgs[0])) {
2588
+ if (relArgs.length > 0 && Node5.isObjectLiteralExpression(relArgs[0])) {
3041
2589
  for (const relProp of relArgs[0].getProperties()) {
3042
- if (!Node6.isPropertyAssignment(relProp)) continue;
2590
+ if (!Node5.isPropertyAssignment(relProp)) continue;
3043
2591
  const relInit = relProp.getInitializer();
3044
- if (!relInit || !Node6.isObjectLiteralExpression(relInit)) continue;
2592
+ if (!relInit || !Node5.isObjectLiteralExpression(relInit)) continue;
3045
2593
  const keysProp = relInit.getProperty("keys");
3046
- if (!keysProp || !Node6.isPropertyAssignment(keysProp)) continue;
2594
+ if (!keysProp || !Node5.isPropertyAssignment(keysProp)) continue;
3047
2595
  const keysInit = keysProp.getInitializer();
3048
- if (!keysInit || !Node6.isArrayLiteralExpression(keysInit)) continue;
2596
+ if (!keysInit || !Node5.isArrayLiteralExpression(keysInit)) continue;
3049
2597
  for (const el of keysInit.getElements()) {
3050
- if (Node6.isStringLiteral(el)) {
2598
+ if (Node5.isStringLiteral(el)) {
3051
2599
  fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
3052
2600
  }
3053
2601
  }
@@ -3057,267 +2605,65 @@ function extractFilterableEntityFields(filterClass, project) {
3057
2605
  return fields;
3058
2606
  }
3059
2607
 
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
- });
2608
+ // src/discovery/dto-type-resolver.ts
2609
+ var WRAPPER_TYPES = {
2610
+ // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
2611
+ // wrappers around related entities; the wire shape is just the referenced
2612
+ // entity. Unwrap to the type argument.
2613
+ Ref: "unwrap",
2614
+ Reference: "unwrap",
2615
+ LoadedReference: "unwrap",
2616
+ IdentifiedReference: "unwrap",
2617
+ // MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
2618
+ Opt: "unwrap",
2619
+ Loaded: "unwrap",
2620
+ // Promise<T> — unwrap
2621
+ Promise: "unwrap",
2622
+ // MikroORM Collection<T> serializes as an array of T on the wire.
2623
+ Collection: "arrayOf",
2624
+ // Array<T> generic form
2625
+ Array: "arrayOf"
2626
+ };
2627
+ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2628
+ "Record",
2629
+ "Omit",
2630
+ "Pick",
2631
+ "Partial",
2632
+ "Required",
2633
+ "Readonly",
2634
+ "Map",
2635
+ "Set"
2636
+ ]);
2637
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2638
+ if (depth <= 0) return "unknown";
2639
+ if (Node6.isArrayTypeNode(typeNode)) {
2640
+ const elementType = typeNode.getElementTypeNode();
2641
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3083
2642
  }
3084
- const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3085
- for (const f of files) {
3086
- project.addSourceFileAtPath(f);
2643
+ if (Node6.isUnionTypeNode(typeNode)) {
2644
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3087
2645
  }
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);
2646
+ if (Node6.isIntersectionTypeNode(typeNode)) {
2647
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3099
2648
  }
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)) {
2649
+ if (Node6.isParenthesizedTypeNode(typeNode)) {
3268
2650
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3269
2651
  }
3270
- if (Node7.isTypeReference(typeNode)) {
2652
+ if (Node6.isTypeReference(typeNode)) {
3271
2653
  const typeName = typeNode.getTypeName();
3272
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2654
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3273
2655
  if (name === "string" || name === "number" || name === "boolean") return name;
3274
2656
  if (name === "Date") return "string";
3275
2657
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
3276
2658
  if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
3277
2659
  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";
3285
- }
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";
2660
+ const wrapperMode = WRAPPER_TYPES[name];
2661
+ if (wrapperMode) {
2662
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3301
2663
  }
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)) {
2664
+ if (PASSTHROUGH_UTILITY.has(name)) {
3311
2665
  return typeNode.getText();
3312
2666
  }
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
2667
  const resolved = findType(name, sourceFile, project);
3322
2668
  if (resolved) {
3323
2669
  return expandTypeDecl(resolved, project, depth - 1);
@@ -3333,6 +2679,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3333
2679
  if (kind === SyntaxKind3.AnyKeyword) return "unknown";
3334
2680
  return typeNode.getText();
3335
2681
  }
2682
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
2683
+ const typeArgs = typeNode.getTypeArguments();
2684
+ const firstTypeArg = typeArgs[0];
2685
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2686
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
2687
+ return mode === "arrayOf" ? `Array<${inner}>` : inner;
2688
+ }
2689
+ return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2690
+ }
3336
2691
  function expandTypeDecl(result, project, depth) {
3337
2692
  if (depth < 0) return "unknown";
3338
2693
  switch (result.kind) {
@@ -3398,7 +2753,7 @@ function extractParamsType(method, sourceFile, project) {
3398
2753
  const paramArgs = paramDecorator.getArguments();
3399
2754
  if (paramArgs.length === 0) continue;
3400
2755
  const nameArg = paramArgs[0];
3401
- if (!Node7.isStringLiteral(nameArg)) continue;
2756
+ if (!Node6.isStringLiteral(nameArg)) continue;
3402
2757
  const paramName = nameArg.getLiteralValue();
3403
2758
  const typeNode = param.getTypeNode();
3404
2759
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -3411,13 +2766,13 @@ function extractResponseType(method, sourceFile, project) {
3411
2766
  if (apiResponseDecorator) {
3412
2767
  const args = apiResponseDecorator.getArguments();
3413
2768
  const optsArg = args[0];
3414
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
2769
+ if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
3415
2770
  for (const prop of optsArg.getProperties()) {
3416
- if (!Node7.isPropertyAssignment(prop)) continue;
2771
+ if (!Node6.isPropertyAssignment(prop)) continue;
3417
2772
  if (prop.getName() !== "type") continue;
3418
2773
  const val = prop.getInitializer();
3419
2774
  if (!val) continue;
3420
- if (Node7.isArrayLiteralExpression(val)) {
2775
+ if (Node6.isArrayLiteralExpression(val)) {
3421
2776
  const elements = val.getElements();
3422
2777
  const firstEl = elements[0];
3423
2778
  if (elements.length > 0 && firstEl !== void 0) {
@@ -3437,7 +2792,7 @@ function extractResponseType(method, sourceFile, project) {
3437
2792
  return "unknown";
3438
2793
  }
3439
2794
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
3440
- if (!Node7.isIdentifier(node)) return "unknown";
2795
+ if (!Node6.isIdentifier(node)) return "unknown";
3441
2796
  const name = node.getText();
3442
2797
  const resolved = findType(name, sourceFile, project);
3443
2798
  if (resolved) {
@@ -3484,11 +2839,11 @@ function extractDtoContract(method, sourceFile, project) {
3484
2839
  if (apiResp) {
3485
2840
  const args = apiResp.getArguments();
3486
2841
  const optsArg = args[0];
3487
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
2842
+ if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
3488
2843
  for (const prop of optsArg.getProperties()) {
3489
- if (Node7.isPropertyAssignment(prop) && prop.getName() === "type") {
2844
+ if (Node6.isPropertyAssignment(prop) && prop.getName() === "type") {
3490
2845
  const val = prop.getInitializer();
3491
- if (val && Node7.isIdentifier(val)) {
2846
+ if (val && Node6.isIdentifier(val)) {
3492
2847
  const name = val.getText();
3493
2848
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3494
2849
  if (localDecl?.isExported()) {
@@ -3505,60 +2860,243 @@ function extractDtoContract(method, sourceFile, project) {
3505
2860
  }
3506
2861
  }
3507
2862
  }
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);
2863
+ let bodySchema = null;
2864
+ let querySchema = null;
2865
+ const formWarnings = [];
2866
+ const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
2867
+ if (bodyClass) {
2868
+ bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2869
+ formWarnings.push(...bodySchema.warnings);
2870
+ }
2871
+ const queryClass = resolveParamClass(method, "Query", sourceFile, project);
2872
+ if (queryClass) {
2873
+ querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
2874
+ formWarnings.push(...querySchema.warnings);
2875
+ }
2876
+ return {
2877
+ query,
2878
+ body,
2879
+ response,
2880
+ params: paramsType,
2881
+ queryRef,
2882
+ bodyRef,
2883
+ responseRef,
2884
+ filterFields: filterInfo?.fieldNames ?? null,
2885
+ filterFieldTypes: filterInfo?.fieldTypes ?? null,
2886
+ filterSource: filterInfo?.source ?? null,
2887
+ formWarnings,
2888
+ bodySchema,
2889
+ querySchema
2890
+ };
2891
+ }
2892
+ function resolveParamClass(method, decoratorName, sourceFile, project) {
2893
+ for (const param of method.getParameters()) {
2894
+ if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
2895
+ const typeNode = param.getTypeNode();
2896
+ if (!typeNode) continue;
2897
+ const text = typeNode.getText().replace(/\[\]$/, "");
2898
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
2899
+ const resolved = findType(text, sourceFile, project);
2900
+ if (resolved && resolved.kind === "class") {
2901
+ return { decl: resolved.decl, file: resolved.file };
2902
+ }
2903
+ }
2904
+ return null;
2905
+ }
2906
+
2907
+ // src/discovery/zod-ast-to-ts.ts
2908
+ import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
2909
+ function zodAstToTs(node) {
2910
+ if (!Node7.isCallExpression(node)) return "unknown";
2911
+ const expr = node.getExpression();
2912
+ if (Node7.isPropertyAccessExpression(expr)) {
2913
+ const methodName = expr.getName();
2914
+ const receiver = expr.getExpression();
2915
+ if (methodName === "optional") {
2916
+ return `${zodAstToTs(receiver)} | undefined`;
2917
+ }
2918
+ if (methodName === "nullable") {
2919
+ return `${zodAstToTs(receiver)} | null`;
2920
+ }
2921
+ const args = node.getArguments();
2922
+ switch (methodName) {
2923
+ case "string":
2924
+ return "string";
2925
+ case "number":
2926
+ return "number";
2927
+ case "boolean":
2928
+ return "boolean";
2929
+ case "unknown":
2930
+ return "unknown";
2931
+ case "any":
2932
+ return "unknown";
2933
+ case "literal": {
2934
+ const lit = args[0];
2935
+ if (!lit) return "unknown";
2936
+ if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2937
+ if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2938
+ if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
2939
+ if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
2940
+ return "unknown";
2941
+ }
2942
+ case "enum": {
2943
+ const arrArg = args[0];
2944
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
2945
+ const members = arrArg.getElements().map(
2946
+ (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2947
+ );
2948
+ return members.join(" | ");
2949
+ }
2950
+ case "array": {
2951
+ const inner = args[0];
2952
+ if (!inner) return "unknown";
2953
+ return `Array<${zodAstToTs(inner)}>`;
2954
+ }
2955
+ case "object": {
2956
+ const objArg = args[0];
2957
+ if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
2958
+ const lines = [];
2959
+ for (const prop of objArg.getProperties()) {
2960
+ if (!Node7.isPropertyAssignment(prop)) continue;
2961
+ const key = prop.getName();
2962
+ const valNode = prop.getInitializer();
2963
+ if (!valNode) continue;
2964
+ const tsType = zodAstToTs(valNode);
2965
+ const isOpt = isOptionalChain(valNode);
2966
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
2967
+ }
2968
+ return `{ ${lines.join("; ")} }`;
2969
+ }
2970
+ case "union": {
2971
+ const arrArg = args[0];
2972
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
2973
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
2974
+ }
2975
+ case "record": {
2976
+ const valArg = args.length === 1 ? args[0] : args[1];
2977
+ if (!valArg) return "unknown";
2978
+ return `Record<string, ${zodAstToTs(valArg)}>`;
2979
+ }
2980
+ case "tuple": {
2981
+ const arrArg = args[0];
2982
+ if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
2983
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
2984
+ }
2985
+ default:
2986
+ return "unknown";
2987
+ }
2988
+ }
2989
+ return "unknown";
2990
+ }
2991
+ function isOptionalChain(node) {
2992
+ if (!Node7.isCallExpression(node)) return false;
2993
+ const expr = node.getExpression();
2994
+ return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
2995
+ }
2996
+ function parseDefineContractCall(callExpr) {
2997
+ if (!Node7.isCallExpression(callExpr)) return null;
2998
+ const callee = callExpr.getExpression();
2999
+ const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
3000
+ if (calleeName !== "defineContract") return null;
3001
+ const args = callExpr.getArguments();
3002
+ const optsArg = args[0];
3003
+ if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3004
+ let query = null;
3005
+ let body = null;
3006
+ let response = "unknown";
3007
+ let bodyZodText = null;
3008
+ let queryZodText = null;
3009
+ for (const prop of optsArg.getProperties()) {
3010
+ if (!Node7.isPropertyAssignment(prop)) continue;
3011
+ const propName = prop.getName();
3012
+ const val = prop.getInitializer();
3013
+ if (!val) continue;
3014
+ if (propName === "query") {
3015
+ query = zodAstToTs(val);
3016
+ queryZodText = val.getText();
3017
+ } else if (propName === "body") {
3018
+ body = zodAstToTs(val);
3019
+ bodyZodText = val.getText();
3020
+ } else if (propName === "response") {
3021
+ response = zodAstToTs(val);
3022
+ }
3023
+ }
3024
+ return { query, body, response, bodyZodText, queryZodText };
3025
+ }
3026
+
3027
+ // src/discovery/contracts-fast.ts
3028
+ async function discoverContractsFast(opts) {
3029
+ const { cwd, glob, tsconfig } = opts;
3030
+ const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
3031
+ let project;
3032
+ try {
3033
+ project = new Project3({
3034
+ tsConfigFilePath: tsconfigPath,
3035
+ skipAddingFilesFromTsConfig: true,
3036
+ skipLoadingLibFiles: true,
3037
+ skipFileDependencyResolution: true
3038
+ });
3039
+ } catch {
3040
+ project = new Project3({
3041
+ skipAddingFilesFromTsConfig: true,
3042
+ skipLoadingLibFiles: true,
3043
+ skipFileDependencyResolution: true,
3044
+ compilerOptions: {
3045
+ allowJs: true,
3046
+ resolveJsonModule: false,
3047
+ strict: false
3048
+ }
3049
+ });
3050
+ }
3051
+ const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
3052
+ for (const f of files) {
3053
+ project.addSourceFileAtPath(f);
3054
+ }
3055
+ const routes = [];
3056
+ setDiscoveryContext(project, {
3057
+ projectRoot: cwd,
3058
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3059
+ });
3060
+ for (const sourceFile of project.getSourceFiles()) {
3061
+ routes.push(...extractFromSourceFile(sourceFile, project));
3521
3062
  }
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);
3063
+ return routes;
3064
+ }
3065
+ function decoratorStringArg(decoratorExpr) {
3066
+ if (!decoratorExpr) return void 0;
3067
+ if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3068
+ if (Node8.isArrayLiteralExpression(decoratorExpr)) {
3069
+ const first = decoratorExpr.getElements()[0];
3070
+ if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
3529
3071
  }
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
- };
3072
+ return void 0;
3548
3073
  }
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
- }
3074
+ function deriveClassSegment(className) {
3075
+ const noSuffix = className.replace(/Controller$/, "");
3076
+ if (!noSuffix) {
3077
+ throw new Error(
3078
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3079
+ );
3560
3080
  }
3561
- return null;
3081
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3082
+ }
3083
+ function resolveRouteName(className, methodName, classAs, methodAs) {
3084
+ const classPortion = classAs ?? deriveClassSegment(className);
3085
+ const methodPortion = methodAs ?? methodName;
3086
+ return `${classPortion}.${methodPortion}`;
3087
+ }
3088
+ function joinPaths(prefix, suffix) {
3089
+ if (!prefix && !suffix) return "/";
3090
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3091
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3092
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3093
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3094
+ const combined = p + s;
3095
+ return combined === "" ? "/" : combined;
3096
+ }
3097
+ function extractParams(path) {
3098
+ const matches = path.matchAll(/:(\w+)/g);
3099
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3562
3100
  }
3563
3101
  var HTTP_METHOD_DECORATORS = {
3564
3102
  Get: "GET",
@@ -3570,176 +3108,186 @@ var HTTP_METHOD_DECORATORS = {
3570
3108
  Head: "HEAD",
3571
3109
  All: "ALL"
3572
3110
  };
3111
+ function resolveVerb(method) {
3112
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3113
+ const httpDecorator = method.getDecorator(decoratorName);
3114
+ if (httpDecorator) {
3115
+ const httpArgs = httpDecorator.getArguments();
3116
+ const pathArg = httpArgs[0];
3117
+ return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3118
+ }
3119
+ }
3120
+ return null;
3121
+ }
3122
+ function readAsDecorator(node, label) {
3123
+ const asDecorator = node.getDecorator("As");
3124
+ if (!asDecorator) return void 0;
3125
+ const asName = decoratorStringArg(asDecorator.getArguments()[0]);
3126
+ if (!asName) {
3127
+ throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
3128
+ }
3129
+ return asName;
3130
+ }
3131
+ function buildRoute(args) {
3132
+ const {
3133
+ className,
3134
+ methodName,
3135
+ resolvedMethod,
3136
+ combinedPath,
3137
+ classAs,
3138
+ methodAs,
3139
+ sourceFile,
3140
+ seenNames,
3141
+ contractSource
3142
+ } = args;
3143
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3144
+ const qualifiedRef = `${className}.${methodName}`;
3145
+ const existing = seenNames.get(routeName);
3146
+ if (existing !== void 0) {
3147
+ throw new Error(
3148
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3149
+ );
3150
+ }
3151
+ seenNames.set(routeName, qualifiedRef);
3152
+ return {
3153
+ method: resolvedMethod,
3154
+ path: combinedPath,
3155
+ name: routeName,
3156
+ params: extractParams(combinedPath),
3157
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3158
+ contract: { contractSource }
3159
+ };
3160
+ }
3161
+ function extractContractRoute(args) {
3162
+ const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
3163
+ const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3164
+ if (!firstDecoratorArg) return null;
3165
+ let contractDef = null;
3166
+ let bodyZodRef = null;
3167
+ let queryZodRef = null;
3168
+ if (Node8.isCallExpression(firstDecoratorArg)) {
3169
+ contractDef = parseDefineContractCall(firstDecoratorArg);
3170
+ } else if (Node8.isIdentifier(firstDecoratorArg)) {
3171
+ const identName = firstDecoratorArg.getText();
3172
+ const varDecl = sourceFile.getVariableDeclaration(identName);
3173
+ if (!varDecl) {
3174
+ console.warn(
3175
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3176
+ );
3177
+ return null;
3178
+ }
3179
+ const initializer = varDecl.getInitializer();
3180
+ if (!initializer) return null;
3181
+ contractDef = parseDefineContractCall(initializer);
3182
+ if (contractDef && varDecl.isExported()) {
3183
+ const filePath = sourceFile.getFilePath();
3184
+ if (contractDef.body !== null) {
3185
+ bodyZodRef = { name: `${identName}.body`, filePath };
3186
+ }
3187
+ if (contractDef.query !== null) {
3188
+ queryZodRef = { name: `${identName}.query`, filePath };
3189
+ }
3190
+ }
3191
+ } else {
3192
+ console.warn(
3193
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3194
+ );
3195
+ return null;
3196
+ }
3197
+ if (!contractDef) return null;
3198
+ if (!verb) return null;
3199
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
3200
+ const methodName = method.getName();
3201
+ const classAs = readAsDecorator(cls, `class ${className}`);
3202
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3203
+ return buildRoute({
3204
+ className,
3205
+ methodName,
3206
+ resolvedMethod: verb.httpMethod,
3207
+ combinedPath: resolvedPath,
3208
+ classAs,
3209
+ methodAs,
3210
+ sourceFile,
3211
+ seenNames,
3212
+ contractSource: {
3213
+ query: contractDef.query,
3214
+ body: contractDef.body,
3215
+ response: contractDef.response,
3216
+ // Path A: capture both the importable ref and the raw text. The emitter
3217
+ // prefers inlining the text (client-safe — re-exporting from a controller
3218
+ // would drag server-only deps into the client bundle).
3219
+ bodyZodRef,
3220
+ bodyZodText: contractDef.bodyZodText,
3221
+ queryZodRef,
3222
+ queryZodText: contractDef.queryZodText
3223
+ }
3224
+ });
3225
+ }
3226
+ function extractDtoRoute(args) {
3227
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
3228
+ if (!verb) return null;
3229
+ const combined = joinPaths(prefix, verb.handlerPath);
3230
+ const methodName = method.getName();
3231
+ const classAs = readAsDecorator(cls, `class ${className}`);
3232
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3233
+ const dtoContract = extractDtoContract(method, sourceFile, project);
3234
+ return buildRoute({
3235
+ className,
3236
+ methodName,
3237
+ resolvedMethod: verb.httpMethod,
3238
+ combinedPath: combined,
3239
+ classAs,
3240
+ methodAs,
3241
+ sourceFile,
3242
+ seenNames,
3243
+ contractSource: {
3244
+ query: dtoContract?.query ?? null,
3245
+ body: dtoContract?.body ?? null,
3246
+ response: dtoContract?.response ?? "unknown",
3247
+ queryRef: dtoContract?.queryRef ?? null,
3248
+ bodyRef: dtoContract?.bodyRef ?? null,
3249
+ responseRef: dtoContract?.responseRef ?? null,
3250
+ filterFields: dtoContract?.filterFields ?? null,
3251
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3252
+ filterSource: dtoContract?.filterSource ?? null,
3253
+ formWarnings: dtoContract?.formWarnings ?? [],
3254
+ bodySchema: dtoContract?.bodySchema ?? null,
3255
+ querySchema: dtoContract?.querySchema ?? null
3256
+ }
3257
+ });
3258
+ }
3573
3259
  function extractFromSourceFile(sourceFile, project) {
3574
3260
  const routes = [];
3575
3261
  const seenNames = /* @__PURE__ */ new Map();
3576
- const classes = sourceFile.getClasses();
3577
- for (const cls of classes) {
3262
+ for (const cls of sourceFile.getClasses()) {
3578
3263
  const controllerDecorator = cls.getDecorator("Controller");
3579
3264
  if (!controllerDecorator) continue;
3580
- const controllerArgs = controllerDecorator.getArguments();
3581
- const firstArg3 = controllerArgs[0];
3582
- const prefix = decoratorStringArg(firstArg3) ?? "";
3265
+ const firstArg2 = controllerDecorator.getArguments()[0];
3266
+ const prefix = decoratorStringArg(firstArg2) ?? "";
3583
3267
  const className = cls.getName() ?? "Unknown";
3584
3268
  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
- }
3269
+ const verb = resolveVerb(method);
3597
3270
  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
- }
3271
+ const route = applyContractDecorator ? extractContractRoute({
3272
+ cls,
3273
+ method,
3274
+ applyContractDecorator,
3275
+ verb,
3276
+ prefix,
3277
+ className,
3278
+ sourceFile,
3279
+ seenNames
3280
+ }) : extractDtoRoute({
3281
+ cls,
3282
+ method,
3283
+ verb,
3284
+ prefix,
3285
+ className,
3286
+ sourceFile,
3287
+ project,
3288
+ seenNames
3289
+ });
3290
+ if (route) routes.push(route);
3743
3291
  }
3744
3292
  }
3745
3293
  return routes;
@@ -3748,7 +3296,7 @@ function extractFromSourceFile(sourceFile, project) {
3748
3296
  // src/watch/lock-file.ts
3749
3297
  import { open } from "fs/promises";
3750
3298
  import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3751
- import { join as join11 } from "path";
3299
+ import { join as join12 } from "path";
3752
3300
  var LOCK_FILE = ".watcher.lock";
3753
3301
  function isProcessAlive(pid) {
3754
3302
  try {
@@ -3760,7 +3308,7 @@ function isProcessAlive(pid) {
3760
3308
  }
3761
3309
  async function acquireLock(outDir) {
3762
3310
  await mkdir8(outDir, { recursive: true });
3763
- const lockPath = join11(outDir, LOCK_FILE);
3311
+ const lockPath = join12(outDir, LOCK_FILE);
3764
3312
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3765
3313
  try {
3766
3314
  const fd = await open(lockPath, "wx");
@@ -3800,7 +3348,7 @@ async function watch(config, onChange) {
3800
3348
  if (lock === null) {
3801
3349
  let holderPid = "unknown";
3802
3350
  try {
3803
- const raw = await readFile3(join12(config.codegen.outDir, ".watcher.lock"), "utf8");
3351
+ const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
3804
3352
  const data = JSON.parse(raw);
3805
3353
  if (data.pid !== void 0) holderPid = String(data.pid);
3806
3354
  } catch {
@@ -3828,7 +3376,7 @@ async function watch(config, onChange) {
3828
3376
  }
3829
3377
  let pagesDebounceTimer;
3830
3378
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3831
- const pagesWatcher = chokidar.watch(join12(config.codegen.cwd, pagesGlob), {
3379
+ const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
3832
3380
  ignoreInitial: true,
3833
3381
  persistent: true,
3834
3382
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3854,7 +3402,7 @@ async function watch(config, onChange) {
3854
3402
  pagesWatcher.on("change", schedulePagesRegenerate);
3855
3403
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3856
3404
  let contractsDebounceTimer;
3857
- const contractsWatcher = chokidar.watch(join12(config.codegen.cwd, config.contracts.glob), {
3405
+ const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
3858
3406
  ignoreInitial: true,
3859
3407
  persistent: true,
3860
3408
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3884,7 +3432,7 @@ async function watch(config, onChange) {
3884
3432
  contractsWatcher.on("add", scheduleContractsRegenerate);
3885
3433
  contractsWatcher.on("change", scheduleContractsRegenerate);
3886
3434
  contractsWatcher.on("unlink", scheduleContractsRegenerate);
3887
- const formsWatcher = chokidar.watch(join12(config.codegen.cwd, config.forms.watch), {
3435
+ const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
3888
3436
  ignoreInitial: true,
3889
3437
  persistent: true,
3890
3438
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3911,7 +3459,7 @@ async function watch(config, onChange) {
3911
3459
  }
3912
3460
 
3913
3461
  // src/index.ts
3914
- var VERSION = "0.2.0";
3462
+ var VERSION = "0.3.0";
3915
3463
 
3916
3464
  // src/cli/codegen.ts
3917
3465
  async function runCodegen(opts = {}) {
@@ -3939,15 +3487,51 @@ async function runCodegen(opts = {}) {
3939
3487
 
3940
3488
  // src/cli/doctor.ts
3941
3489
  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";
3490
+ import { appendFileSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
3491
+ import { join as join15 } from "path";
3944
3492
 
3945
3493
  // src/cli/init.ts
3946
3494
  import { execFileSync } from "child_process";
3947
- import { readFileSync as readFileSync2, writeFileSync } from "fs";
3495
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3948
3496
  import { access as access2, mkdir as mkdir9, readFile as readFile4, writeFile as writeFile8 } from "fs/promises";
3949
- import { join as join13 } from "path";
3497
+ import { join as join14 } from "path";
3950
3498
  import { createInterface } from "readline";
3499
+
3500
+ // src/cli/patch-utils.ts
3501
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
3502
+ function patchJsonFile(filePath, mutator, parse = (raw) => raw) {
3503
+ let raw;
3504
+ try {
3505
+ raw = readFileSync2(filePath, "utf8");
3506
+ } catch {
3507
+ return "skipped";
3508
+ }
3509
+ const json = JSON.parse(parse(raw));
3510
+ if (!mutator(json)) return "already";
3511
+ writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
3512
+ `, "utf8");
3513
+ return "patched";
3514
+ }
3515
+ function findAfterLastImport(content) {
3516
+ const lastImportIndex = content.lastIndexOf("\nimport ");
3517
+ if (lastImportIndex !== -1) {
3518
+ const endOfLine = content.indexOf("\n", lastImportIndex + 1);
3519
+ return endOfLine !== -1 ? endOfLine + 1 : content.length;
3520
+ }
3521
+ if (content.startsWith("import ")) {
3522
+ const endOfLine = content.indexOf("\n");
3523
+ return endOfLine !== -1 ? endOfLine + 1 : content.length;
3524
+ }
3525
+ return 0;
3526
+ }
3527
+ function insertImport(content, stmt) {
3528
+ const insertAt = findAfterLastImport(content);
3529
+ if (insertAt <= 0) return content;
3530
+ return `${content.slice(0, insertAt)}${stmt}
3531
+ ${content.slice(insertAt)}`;
3532
+ }
3533
+
3534
+ // src/cli/init.ts
3951
3535
  var GITIGNORE_ENTRY = ".nestjs-inertia/";
3952
3536
  var green = (s) => `\x1B[32m${s}\x1B[0m`;
3953
3537
  var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
@@ -3972,7 +3556,7 @@ ${bold(title)}`);
3972
3556
  }
3973
3557
  async function readPackageJson(cwd) {
3974
3558
  try {
3975
- const raw = await readFile4(join13(cwd, "package.json"), "utf8");
3559
+ const raw = await readFile4(join14(cwd, "package.json"), "utf8");
3976
3560
  return JSON.parse(raw);
3977
3561
  } catch {
3978
3562
  return {};
@@ -4003,7 +3587,7 @@ async function detectTemplateEngine(cwd) {
4003
3587
  async function detectPackageManager(cwd) {
4004
3588
  async function exists(file) {
4005
3589
  try {
4006
- await access2(join13(cwd, file));
3590
+ await access2(join14(cwd, file));
4007
3591
  return true;
4008
3592
  } catch {
4009
3593
  return false;
@@ -4050,7 +3634,7 @@ async function writeIfNotExists(filePath, content, label) {
4050
3634
  logCreated(label);
4051
3635
  }
4052
3636
  async function handleViteConfig(cwd, framework) {
4053
- const filePath = join13(cwd, "vite.config.ts");
3637
+ const filePath = join14(cwd, "vite.config.ts");
4054
3638
  if (await fileExists2(filePath)) {
4055
3639
  const existing = await readFile4(filePath, "utf8");
4056
3640
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
@@ -4108,7 +3692,7 @@ function installDeps(pkgManager, deps, dev) {
4108
3692
  }
4109
3693
  }
4110
3694
  async function patchPackageJsonScripts(cwd, scripts) {
4111
- const pkgPath = join13(cwd, "package.json");
3695
+ const pkgPath = join14(cwd, "package.json");
4112
3696
  let pkg = {};
4113
3697
  try {
4114
3698
  pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
@@ -4133,32 +3717,16 @@ async function patchPackageJsonScripts(cwd, scripts) {
4133
3717
  await writeFile8(pkgPath, `${JSON.stringify(pkg, null, 2)}
4134
3718
  `, "utf8");
4135
3719
  }
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
3720
  function patchAppModule(filePath, rootView) {
4149
3721
  let content;
4150
3722
  try {
4151
- content = readFileSync2(filePath, "utf8");
3723
+ content = readFileSync3(filePath, "utf8");
4152
3724
  } catch {
4153
3725
  return "skipped";
4154
3726
  }
4155
3727
  let changed = false;
4156
3728
  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
- }
3729
+ content = insertImport(content, "import { InertiaModule } from '@dudousxd/nestjs-inertia';");
4162
3730
  if (!content.includes("from 'node:path'") && !content.includes('from "node:path"')) {
4163
3731
  const insertAt2 = findAfterLastImport(content);
4164
3732
  content = `${content.slice(0, insertAt2)}import { resolve } from 'node:path';
@@ -4176,11 +3744,7 @@ ${indent}}),${content.slice(bracketPos)}`;
4176
3744
  }
4177
3745
  }
4178
3746
  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
- }
3747
+ content = insertImport(content, "import { HomeController } from './home.controller';");
4184
3748
  const controllersMatch = content.match(/controllers\s*:\s*\[/);
4185
3749
  if (controllersMatch?.index !== void 0) {
4186
3750
  const bracketPos = content.indexOf("[", controllersMatch.index) + 1;
@@ -4191,22 +3755,21 @@ ${indent}HomeController,${content.slice(bracketPos)}`;
4191
3755
  }
4192
3756
  }
4193
3757
  if (!changed) return "already";
4194
- writeFileSync(filePath, content, "utf8");
3758
+ writeFileSync2(filePath, content, "utf8");
4195
3759
  return "patched";
4196
3760
  }
4197
3761
  function patchMainTs(filePath) {
4198
3762
  let content;
4199
3763
  try {
4200
- content = readFileSync2(filePath, "utf8");
3764
+ content = readFileSync3(filePath, "utf8");
4201
3765
  } catch {
4202
3766
  return "skipped";
4203
3767
  }
4204
3768
  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
- }
3769
+ content = insertImport(
3770
+ content,
3771
+ "import { setupInertiaVite } from '@dudousxd/nestjs-inertia-vite';"
3772
+ );
4210
3773
  const createMatch = content.match(
4211
3774
  /(?:const|let)\s+(\w+)\s*=\s*await\s+NestFactory\.create[^;]+;/
4212
3775
  );
@@ -4223,7 +3786,7 @@ ${content.slice(insertAt)}`;
4223
3786
  });`;
4224
3787
  content = `${content.slice(0, insertAfterPos)}
4225
3788
  ${viteSetup}${content.slice(insertAfterPos)}`;
4226
- writeFileSync(filePath, content, "utf8");
3789
+ writeFileSync2(filePath, content, "utf8");
4227
3790
  return "patched";
4228
3791
  }
4229
3792
  function configTemplate(framework) {
@@ -4446,107 +4009,84 @@ export class HomeController {
4446
4009
  }
4447
4010
  `;
4448
4011
  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";
4012
+ const filePath = join14(cwd, filename);
4013
+ return patchJsonFile(
4014
+ filePath,
4015
+ (json) => {
4016
+ const exclude = json.exclude ?? [];
4017
+ if (exclude.includes(dir)) return false;
4018
+ exclude.push(dir);
4019
+ json.exclude = exclude;
4020
+ return true;
4021
+ },
4022
+ // Strip single-line comments before JSON.parse
4023
+ (raw) => raw.replace(/\/\/.*$/gm, "")
4024
+ );
4465
4025
  }
4466
4026
  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
4027
+ const filePath = join14(cwd, "nest-cli.json");
4028
+ return patchJsonFile(filePath, (json) => {
4029
+ const compiler = json.compilerOptions ?? {};
4030
+ const assets = compiler.assets ?? [];
4031
+ const alreadyHas = assets.some((a) => {
4032
+ if (typeof a === "string") return a.includes(shellDir);
4033
+ return String(a.include ?? "").includes(shellDir);
4034
+ });
4035
+ if (alreadyHas) return false;
4036
+ assets.push({
4037
+ include: `../${shellDir}/**/*`,
4038
+ outDir: `dist/${shellDir}`,
4039
+ watchAssets: true
4040
+ });
4041
+ compiler.assets = assets;
4042
+ json.compilerOptions = compiler;
4043
+ return true;
4486
4044
  });
4487
- compiler.assets = assets;
4488
- json.compilerOptions = compiler;
4489
- writeFileSync(filePath, `${JSON.stringify(json, null, 2)}
4490
- `, "utf8");
4491
- return "patched";
4492
4045
  }
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";
4046
+ async function scaffoldFiles(ctx) {
4047
+ const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
4509
4048
  logSection("Scaffold files");
4510
4049
  await writeIfNotExists(
4511
- join13(cwd, "nestjs-inertia.config.ts"),
4050
+ join14(cwd, "nestjs-inertia.config.ts"),
4512
4051
  configTemplate(framework),
4513
4052
  "nestjs-inertia.config.ts"
4514
4053
  );
4515
- await writeIfNotExists(join13(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4054
+ await writeIfNotExists(join14(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4516
4055
  await writeIfNotExists(
4517
- join13(cwd, "tsconfig.inertia.json"),
4056
+ join14(cwd, "tsconfig.inertia.json"),
4518
4057
  TSCONFIG_INERTIA_TEMPLATE,
4519
4058
  "tsconfig.inertia.json"
4520
4059
  );
4521
4060
  await writeIfNotExists(
4522
- join13(cwd, "inertia", "tsconfig.json"),
4061
+ join14(cwd, "inertia", "tsconfig.json"),
4523
4062
  INERTIA_TSCONFIG_TEMPLATE,
4524
4063
  "inertia/tsconfig.json"
4525
4064
  );
4526
4065
  await writeIfNotExists(
4527
- join13(cwd, "inertia", shellFileName),
4066
+ join14(cwd, "inertia", shellFileName),
4528
4067
  htmlShellTemplate(framework, engine),
4529
4068
  `inertia/${shellFileName}`
4530
4069
  );
4531
4070
  await handleViteConfig(cwd, framework);
4532
4071
  await writeIfNotExists(
4533
- join13(cwd, "inertia", "app", `client.${entryExt}`),
4072
+ join14(cwd, "inertia", "app", `client.${entryExt}`),
4534
4073
  entryPointTemplate(framework),
4535
4074
  `inertia/app/client.${entryExt}`
4536
4075
  );
4537
4076
  await writeIfNotExists(
4538
- join13(cwd, "inertia", "pages", `Home.${pageExt}`),
4077
+ join14(cwd, "inertia", "pages", `Home.${pageExt}`),
4539
4078
  samplePageTemplate(framework),
4540
4079
  `inertia/pages/Home.${pageExt}`
4541
4080
  );
4542
4081
  await writeIfNotExists(
4543
- join13(cwd, "src", "home.controller.ts"),
4082
+ join14(cwd, "src", "home.controller.ts"),
4544
4083
  SAMPLE_CONTROLLER,
4545
4084
  "src/home.controller.ts"
4546
4085
  );
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");
4086
+ }
4087
+ function patchServerAppModule(ctx) {
4088
+ const { cwd, rootView } = ctx;
4089
+ const appModulePath = join14(cwd, "src", "app.module.ts");
4550
4090
  const appModuleResult = patchAppModule(appModulePath, rootView);
4551
4091
  if (appModuleResult === "patched") {
4552
4092
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -4558,7 +4098,9 @@ ${bold("nestjs-inertia init")}`);
4558
4098
  } else {
4559
4099
  logWarning("src/app.module.ts not found \u2014 add InertiaModule.forRoot() manually");
4560
4100
  }
4561
- const mainTsPath = join13(cwd, "src", "main.ts");
4101
+ }
4102
+ function patchServerMainTs(ctx) {
4103
+ const mainTsPath = join14(ctx.cwd, "src", "main.ts");
4562
4104
  const mainTsResult = patchMainTs(mainTsPath);
4563
4105
  if (mainTsResult === "patched") {
4564
4106
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -4567,7 +4109,9 @@ ${bold("nestjs-inertia init")}`);
4567
4109
  } else {
4568
4110
  logWarning("src/main.ts not found \u2014 add setupInertiaVite() manually");
4569
4111
  }
4570
- const shellDir = rootView.split("/")[0];
4112
+ }
4113
+ function patchBuildConfigs(ctx) {
4114
+ const { cwd, shellDir } = ctx;
4571
4115
  const nestCliResult = patchNestCliJson(cwd, shellDir);
4572
4116
  if (nestCliResult === "patched") {
4573
4117
  logPatched("nest-cli.json", `added asset copy for ${shellDir}/ \u2192 dist/${shellDir}/`);
@@ -4588,18 +4132,26 @@ ${bold("nestjs-inertia init")}`);
4588
4132
  );
4589
4133
  }
4590
4134
  }
4591
- await patchGitignore(join13(cwd, ".gitignore"));
4135
+ }
4136
+ async function patchGitignoreAndDist(ctx) {
4137
+ const { cwd } = ctx;
4138
+ await patchGitignore(join14(cwd, ".gitignore"));
4592
4139
  const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
4593
4140
  if (tsconfigDistResult === "patched") {
4594
4141
  logPatched("tsconfig.json", "excluded dist/ from server compilation");
4595
4142
  } else if (tsconfigDistResult === "already") {
4596
4143
  console.log(` ${cyan("\u2192")} tsconfig.json ${dim("(dist/ already excluded, skipped)")}`);
4597
4144
  }
4598
- await patchPackageJsonScripts(cwd, {
4145
+ }
4146
+ async function scaffoldPackageScripts(ctx) {
4147
+ await patchPackageJsonScripts(ctx.cwd, {
4599
4148
  "build:client": "vite build",
4600
4149
  "build:ssr": "VITE_SSR=1 vite build --ssr",
4601
4150
  "typecheck:inertia": "tsc --noEmit -p tsconfig.inertia.json"
4602
4151
  });
4152
+ }
4153
+ async function installMissingDeps(ctx) {
4154
+ const { cwd, framework, opts } = ctx;
4603
4155
  logSection("Install dependencies");
4604
4156
  const pkg = await readPackageJson(cwd);
4605
4157
  const installedDeps = allDeps(pkg);
@@ -4633,6 +4185,55 @@ ${bold("nestjs-inertia init")}`);
4633
4185
  installDeps(pkgManager, depsToInstall, false);
4634
4186
  installDeps(pkgManager, devDepsToInstall, true);
4635
4187
  }
4188
+ }
4189
+ var INIT_STEPS = [
4190
+ { label: "scaffold files", run: scaffoldFiles },
4191
+ {
4192
+ label: "patch app.module.ts",
4193
+ run: (ctx) => {
4194
+ logSection("Patch existing files");
4195
+ patchServerAppModule(ctx);
4196
+ }
4197
+ },
4198
+ { label: "patch main.ts", run: patchServerMainTs },
4199
+ { label: "patch build configs", run: patchBuildConfigs },
4200
+ { label: "patch .gitignore + dist exclude", run: patchGitignoreAndDist },
4201
+ { label: "add package.json scripts", run: scaffoldPackageScripts },
4202
+ { label: "install dependencies", run: installMissingDeps }
4203
+ ];
4204
+ async function runInit(opts = {}) {
4205
+ const cwd = opts.cwd ?? process.cwd();
4206
+ console.log(`
4207
+ ${bold("nestjs-inertia init")}`);
4208
+ let framework = await detectFramework(cwd);
4209
+ if (!framework) {
4210
+ framework = await promptFramework();
4211
+ }
4212
+ const engine = await detectTemplateEngine(cwd);
4213
+ const engineLabel = engine === "html" ? "plain HTML" : engine;
4214
+ const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
4215
+ console.log(`
4216
+ Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
4217
+ const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
4218
+ const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
4219
+ const ctx = {
4220
+ opts,
4221
+ cwd,
4222
+ framework,
4223
+ engine,
4224
+ shellFileName,
4225
+ entryExt: framework === "react" ? "tsx" : "ts",
4226
+ pageExt: framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte",
4227
+ rootView,
4228
+ shellDir: rootView.split("/")[0]
4229
+ // e.g. "inertia" from "inertia/index.html"
4230
+ };
4231
+ for (const step of INIT_STEPS) {
4232
+ if (process.env.NESTJS_CODEGEN_DEBUG) {
4233
+ console.log(dim(` \xB7 ${step.label}`));
4234
+ }
4235
+ await step.run(ctx);
4236
+ }
4636
4237
  console.log(`
4637
4238
  ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4638
4239
  `);
@@ -4640,18 +4241,18 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4640
4241
 
4641
4242
  // src/cli/doctor.ts
4642
4243
  function checkFileExists(cwd, file) {
4643
- return existsSync(join14(cwd, file));
4244
+ return existsSync(join15(cwd, file));
4644
4245
  }
4645
4246
  function readJson(path) {
4646
4247
  try {
4647
- const raw = readFileSync3(path, "utf8").replace(/\/\/.*$/gm, "");
4248
+ const raw = readFileSync4(path, "utf8").replace(/\/\/.*$/gm, "");
4648
4249
  return JSON.parse(raw);
4649
4250
  } catch {
4650
4251
  return null;
4651
4252
  }
4652
4253
  }
4653
4254
  function writeJsonField(filePath, dotPath, value) {
4654
- const raw = readFileSync3(filePath, "utf8");
4255
+ const raw = readFileSync4(filePath, "utf8");
4655
4256
  const stripped = raw.replace(/\/\/.*$/gm, "");
4656
4257
  const obj = JSON.parse(stripped);
4657
4258
  let target = obj;
@@ -4671,20 +4272,20 @@ function writeJsonField(filePath, dotPath, value) {
4671
4272
  } else {
4672
4273
  target[lastKey] = value;
4673
4274
  }
4674
- writeFileSync2(filePath, `${JSON.stringify(obj, null, 2)}
4275
+ writeFileSync3(filePath, `${JSON.stringify(obj, null, 2)}
4675
4276
  `, "utf8");
4676
4277
  }
4677
4278
  function getPackageVersion(cwd, pkg) {
4678
4279
  try {
4679
- const pkgJson = readJson(join14(cwd, "node_modules", pkg, "package.json"));
4280
+ const pkgJson = readJson(join15(cwd, "node_modules", pkg, "package.json"));
4680
4281
  return pkgJson?.version ?? null;
4681
4282
  } catch {
4682
4283
  return null;
4683
4284
  }
4684
4285
  }
4685
4286
  function detectPkgManager(cwd) {
4686
- if (existsSync(join14(cwd, "pnpm-lock.yaml"))) return "pnpm";
4687
- if (existsSync(join14(cwd, "yarn.lock"))) return "yarn";
4287
+ if (existsSync(join15(cwd, "pnpm-lock.yaml"))) return "pnpm";
4288
+ if (existsSync(join15(cwd, "yarn.lock"))) return "yarn";
4688
4289
  return "npm";
4689
4290
  }
4690
4291
  async function runDoctor(opts) {
@@ -4722,7 +4323,7 @@ async function runDoctor(opts) {
4722
4323
  autoFix: () => runInit({ cwd })
4723
4324
  });
4724
4325
  if (foundShellDir) {
4725
- const nestCliPath = join14(cwd, "nest-cli.json");
4326
+ const nestCliPath = join15(cwd, "nest-cli.json");
4726
4327
  const nestCli = readJson(nestCliPath);
4727
4328
  const compiler = nestCli?.compilerOptions ?? {};
4728
4329
  const assets = compiler.assets ?? [];
@@ -4761,7 +4362,7 @@ async function runDoctor(opts) {
4761
4362
  fix: "Run: nestjs-codegen codegen",
4762
4363
  autoFix: () => runCodegen({ cwd })
4763
4364
  });
4764
- const tsconfigPath = join14(cwd, "tsconfig.json");
4365
+ const tsconfigPath = join15(cwd, "tsconfig.json");
4765
4366
  const tsconfig = readJson(tsconfigPath);
4766
4367
  const paths = tsconfig?.compilerOptions?.paths;
4767
4368
  checks.push({
@@ -4772,7 +4373,7 @@ async function runDoctor(opts) {
4772
4373
  });
4773
4374
  const inertiaDir = foundShellDir ?? "inertia";
4774
4375
  for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
4775
- const tsc = readJson(join14(cwd, tsconfigFile));
4376
+ const tsc = readJson(join15(cwd, tsconfigFile));
4776
4377
  if (!tsc) continue;
4777
4378
  const excl = tsc.exclude ?? [];
4778
4379
  const excludesIt = excl.includes(inertiaDir);
@@ -4798,14 +4399,14 @@ async function runDoctor(opts) {
4798
4399
  }
4799
4400
  });
4800
4401
  }
4801
- const inertiaTsconfigPath = join14(cwd, "tsconfig.inertia.json");
4402
+ const inertiaTsconfigPath = join15(cwd, "tsconfig.inertia.json");
4802
4403
  const inertiaTsconfig = readJson(inertiaTsconfigPath);
4803
4404
  checks.push({
4804
4405
  name: "tsconfig.inertia.json exists",
4805
4406
  pass: !!inertiaTsconfig,
4806
4407
  fix: "Create tsconfig.inertia.json (typechecks inertia/ + .nestjs-inertia/)",
4807
4408
  autoFix: () => {
4808
- writeFileSync2(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
4409
+ writeFileSync3(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
4809
4410
  }
4810
4411
  });
4811
4412
  if (inertiaTsconfig) {
@@ -4850,17 +4451,17 @@ async function runDoctor(opts) {
4850
4451
  fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
4851
4452
  });
4852
4453
  }
4853
- const innerTsconfigPath = join14(cwd, "inertia", "tsconfig.json");
4454
+ const innerTsconfigPath = join15(cwd, "inertia", "tsconfig.json");
4854
4455
  checks.push({
4855
4456
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
4856
4457
  pass: existsSync(innerTsconfigPath),
4857
4458
  fix: "Create inertia/tsconfig.json that extends ../tsconfig.inertia.json",
4858
4459
  autoFix: () => {
4859
- writeFileSync2(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
4460
+ writeFileSync3(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
4860
4461
  }
4861
4462
  });
4862
4463
  if (checkFileExists(cwd, "vite.config.ts")) {
4863
- const viteContent = readFileSync3(join14(cwd, "vite.config.ts"), "utf8");
4464
+ const viteContent = readFileSync4(join15(cwd, "vite.config.ts"), "utf8");
4864
4465
  checks.push({
4865
4466
  name: "vite.config.ts has resolve.alias",
4866
4467
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -4925,8 +4526,8 @@ async function runDoctor(opts) {
4925
4526
  });
4926
4527
  }
4927
4528
  if (checkFileExists(cwd, ".gitignore")) {
4928
- const gitignorePath = join14(cwd, ".gitignore");
4929
- const gitignore = readFileSync3(gitignorePath, "utf8");
4529
+ const gitignorePath = join15(cwd, ".gitignore");
4530
+ const gitignore = readFileSync4(gitignorePath, "utf8");
4930
4531
  checks.push({
4931
4532
  name: ".gitignore includes .nestjs-inertia/",
4932
4533
  pass: gitignore.includes(".nestjs-inertia"),
@@ -4934,7 +4535,7 @@ async function runDoctor(opts) {
4934
4535
  autoFix: () => appendFileSync(gitignorePath, "\n.nestjs-inertia/\n")
4935
4536
  });
4936
4537
  }
4937
- const pkgJsonPath = join14(cwd, "package.json");
4538
+ const pkgJsonPath = join15(cwd, "package.json");
4938
4539
  const pkgJson = readJson(pkgJsonPath);
4939
4540
  const scripts = pkgJson?.scripts ?? {};
4940
4541
  checks.push({