@dudousxd/nestjs-codegen 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.cjs CHANGED
@@ -54,112 +54,9 @@ var CodegenError = class extends Error {
54
54
  }
55
55
  };
56
56
 
57
- // src/adapters/zod.ts
58
- function toObjectKey(name) {
59
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
60
- }
61
- function messageSuffix(messageRaw2) {
62
- return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
63
- }
64
- function renderStringChecks(checks) {
65
- let out = "";
66
- for (const c of checks) {
67
- switch (c.check) {
68
- case "email":
69
- out += `.email(${messageSuffix(c.messageRaw)})`;
70
- break;
71
- case "url":
72
- out += `.url(${messageSuffix(c.messageRaw)})`;
73
- break;
74
- case "uuid":
75
- out += `.uuid(${messageSuffix(c.messageRaw)})`;
76
- break;
77
- case "regex":
78
- out += `.regex(${c.pattern})`;
79
- break;
80
- case "min":
81
- out += `.min(${c.value})`;
82
- break;
83
- case "max":
84
- out += `.max(${c.value})`;
85
- break;
86
- }
87
- }
88
- return out;
89
- }
90
- function render(node, ctx) {
91
- switch (node.kind) {
92
- case "string":
93
- return `z.string()${renderStringChecks(node.checks)}`;
94
- case "number": {
95
- let out = "z.number()";
96
- for (const c of node.checks) {
97
- if (c.check === "int") out += ".int()";
98
- else if (c.check === "positive") out += ".positive()";
99
- else if (c.check === "negative") out += ".negative()";
100
- else if (c.check === "min") out += `.min(${c.value})`;
101
- else if (c.check === "max") out += `.max(${c.value})`;
102
- }
103
- return out;
104
- }
105
- case "boolean":
106
- return "z.boolean()";
107
- case "date":
108
- return "z.coerce.date()";
109
- case "unknown":
110
- return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
111
- case "instanceof":
112
- return `z.instanceof(${node.ctor})`;
113
- case "enum":
114
- return `z.enum([${node.literals.join(", ")}])`;
115
- case "literal":
116
- return `z.literal(${node.raw})`;
117
- case "union":
118
- return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
119
- case "object": {
120
- if (node.fields.length === 0) {
121
- return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
122
- }
123
- const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
124
- return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
125
- }
126
- case "array":
127
- return `z.array(${render(node.element, ctx)})`;
128
- case "optional":
129
- return `${render(node.inner, ctx)}.optional()`;
130
- case "ref":
131
- return node.name;
132
- case "lazyRef":
133
- return `z.lazy(() => ${node.name})`;
134
- case "annotated": {
135
- const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
136
- return `${render(node.inner, ctx)} ${comments}`;
137
- }
138
- }
139
- }
140
- var zodAdapter = {
141
- name: "zod",
142
- importStatements(usage) {
143
- return usage.used ? ["import { z } from 'zod';"] : [];
144
- },
145
- render,
146
- inferType(schemaConst) {
147
- return `z.infer<typeof ${schemaConst}>`;
148
- },
149
- renderModule(mod) {
150
- const ctx = { named: mod.named };
151
- const namedNestedSchemas = /* @__PURE__ */ new Map();
152
- for (const [name, node] of mod.named) {
153
- namedNestedSchemas.set(name, render(node, ctx));
154
- }
155
- return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
156
- }
157
- };
158
-
159
57
  // src/adapters/registry.ts
160
58
  function resolveAdapter(option) {
161
59
  if (typeof option !== "string") return option;
162
- if (option === "zod") return zodAdapter;
163
60
  const pkg = `@dudousxd/nestjs-codegen-${option}`;
164
61
  const named = `${option}Adapter`;
165
62
  throw new ConfigError(
@@ -209,8 +106,21 @@ If this is intentional, move the file inside your project directory.`
209
106
  );
210
107
  }
211
108
  }
109
+ function validateUserConfig(userConfig) {
110
+ if (userConfig.validation == null) {
111
+ throw new ConfigError(
112
+ "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
113
+ );
114
+ }
115
+ if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
116
+ throw new ConfigError(
117
+ "Config validation failed: `pages.glob` must be a string when `pages` is set"
118
+ );
119
+ }
120
+ }
212
121
  function applyDefaults(userConfig, cwd) {
213
- const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-inertia");
122
+ validateUserConfig(userConfig);
123
+ const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-codegen");
214
124
  const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
215
125
  let app = null;
216
126
  if (userConfig.app) {
@@ -228,7 +138,8 @@ function applyDefaults(userConfig, cwd) {
228
138
  }
229
139
  return {
230
140
  extensions: userConfig.extensions ?? [],
231
- validation: resolveAdapter(userConfig.validation ?? "zod"),
141
+ // Non-null: validateUserConfig() above throws when `validation` is absent.
142
+ validation: resolveAdapter(userConfig.validation),
232
143
  pages: userConfig.pages ? {
233
144
  glob: userConfig.pages.glob,
234
145
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
@@ -282,18 +193,12 @@ Run \`nestjs-codegen init\` to create a starter config.`
282
193
  `Config file must have a default export. Did you forget \`export default defineConfig({...})\`? (${configPath})`
283
194
  );
284
195
  }
285
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
286
- throw new ConfigError(
287
- `Config validation failed: \`pages.glob\` must be a string when \`pages\` is set (${configPath})`
288
- );
289
- }
290
196
  return applyDefaults(userConfig, resolvedCwd);
291
197
  }
292
198
 
293
199
  // src/generate.ts
294
200
  var import_promises9 = require("fs/promises");
295
- var import_node_path9 = require("path");
296
- var import_ts_morph3 = require("ts-morph");
201
+ var import_node_path10 = require("path");
297
202
 
298
203
  // src/discovery/pages.ts
299
204
  var import_promises2 = require("fs/promises");
@@ -348,6 +253,7 @@ function extractPropsSource(source, exportName) {
348
253
  }
349
254
 
350
255
  // src/discovery/shared-props.ts
256
+ var import_node_path3 = require("path");
351
257
  var import_ts_morph = require("ts-morph");
352
258
  function discoverSharedProps(project, moduleEntry) {
353
259
  try {
@@ -368,6 +274,31 @@ function discoverSharedProps(project, moduleEntry) {
368
274
  return null;
369
275
  }
370
276
  }
277
+ function discoverSharedPropsFromConfig(config) {
278
+ if (!config.app?.moduleEntry) return null;
279
+ try {
280
+ const tsconfigPath = config.app.tsconfig ?? (0, import_node_path3.join)(config.codegen.cwd, "tsconfig.json");
281
+ let project;
282
+ try {
283
+ project = new import_ts_morph.Project({
284
+ tsConfigFilePath: tsconfigPath,
285
+ skipAddingFilesFromTsConfig: true,
286
+ skipLoadingLibFiles: true,
287
+ skipFileDependencyResolution: true
288
+ });
289
+ } catch {
290
+ project = new import_ts_morph.Project({
291
+ skipAddingFilesFromTsConfig: true,
292
+ skipLoadingLibFiles: true,
293
+ skipFileDependencyResolution: true,
294
+ compilerOptions: { allowJs: true, strict: false }
295
+ });
296
+ }
297
+ return discoverSharedProps(project, config.app.moduleEntry);
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
371
302
  function findForRootCall(sourceFile) {
372
303
  const callExpressions = sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression);
373
304
  for (const call of callExpressions) {
@@ -387,9 +318,9 @@ function findForRootCall(sourceFile) {
387
318
  function findShareInitializer(forRootCall) {
388
319
  if (!import_ts_morph.Node.isCallExpression(forRootCall)) return null;
389
320
  const args = forRootCall.getArguments();
390
- const firstArg3 = args[0];
391
- if (!firstArg3 || !import_ts_morph.Node.isObjectLiteralExpression(firstArg3)) return null;
392
- for (const prop of firstArg3.getProperties()) {
321
+ const firstArg2 = args[0];
322
+ if (!firstArg2 || !import_ts_morph.Node.isObjectLiteralExpression(firstArg2)) return null;
323
+ for (const prop of firstArg2.getProperties()) {
393
324
  if (import_ts_morph.Node.isPropertyAssignment(prop) && prop.getName() === "share") {
394
325
  return prop.getInitializer() ?? null;
395
326
  }
@@ -488,9 +419,9 @@ function extractFromReturnTypeAnnotation(typeNode) {
488
419
  const typeName = typeNode.getTypeName();
489
420
  if (import_ts_morph.Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
490
421
  const typeArgs = typeNode.getTypeArguments();
491
- const firstArg3 = typeArgs[0];
492
- if (firstArg3) {
493
- return extractFromReturnTypeAnnotation(firstArg3);
422
+ const firstArg2 = typeArgs[0];
423
+ if (firstArg2) {
424
+ return extractFromReturnTypeAnnotation(firstArg2);
494
425
  }
495
426
  return null;
496
427
  }
@@ -596,25 +527,14 @@ function inferExpressionType(node) {
596
527
 
597
528
  // src/emit/emit-api.ts
598
529
  var import_promises3 = require("fs/promises");
599
- var import_node_path3 = require("path");
530
+ var import_node_path4 = require("path");
600
531
 
601
532
  // src/extension/registry.ts
602
533
  var import_ts_morph2 = require("ts-morph");
603
534
  function resolveApiSlots(extensions) {
604
- let transport;
605
- let transportOwner;
606
535
  let layer;
607
536
  let layerOwner;
608
537
  for (const ext of extensions) {
609
- if (ext.apiTransport) {
610
- if (transport) {
611
- throw new CodegenError(
612
- `api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
613
- );
614
- }
615
- transport = ext.apiTransport;
616
- transportOwner = ext.name;
617
- }
618
538
  if (ext.apiClientLayer) {
619
539
  if (layer) {
620
540
  throw new CodegenError(
@@ -626,11 +546,22 @@ function resolveApiSlots(extensions) {
626
546
  }
627
547
  }
628
548
  return {
629
- ...transport ? { transport } : {},
630
549
  ...layer ? { layer } : {}
631
550
  };
632
551
  }
633
552
  var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
553
+ function mergeExclusive(target, incoming, {
554
+ owner,
555
+ describe
556
+ }) {
557
+ for (const [key, value] of incoming) {
558
+ const prev = target.get(key);
559
+ if (prev !== void 0) {
560
+ throw new CodegenError(describe(key, prev.owner, owner));
561
+ }
562
+ target.set(key, { value, owner });
563
+ }
564
+ }
634
565
  function createExtensionContext(config, getRoutes) {
635
566
  let project;
636
567
  return {
@@ -675,29 +606,36 @@ async function collectEmittedFiles(extensions, ctx) {
675
606
  `Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
676
607
  );
677
608
  }
678
- const prev = owners.get(key);
679
- if (prev !== void 0) {
680
- throw new CodegenError(
681
- `Output file "${file.path}" is emitted by both "${prev}" and "${ext.name}". Two extensions cannot write the same file.`
682
- );
683
- }
684
- owners.set(key, ext.name);
609
+ mergeExclusive(owners, [[key, file]], {
610
+ owner: ext.name,
611
+ describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
612
+ });
685
613
  files.push(file);
686
614
  }
687
615
  }
688
616
  return files;
689
617
  }
690
618
 
619
+ // src/extension/types.ts
620
+ function requestShape(route) {
621
+ const cs = route.contract?.contractSource;
622
+ const isGet = route.method.toUpperCase() === "GET";
623
+ const isQuery = isGet || !!cs?.filterFields?.length;
624
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
625
+ const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
626
+ return { isGet, isQuery, hasBody, hasQuery };
627
+ }
628
+
691
629
  // src/emit/emit-api.ts
692
630
  async function emitApi(routes, outDir, opts = {}) {
693
631
  await (0, import_promises3.mkdir)(outDir, { recursive: true });
694
632
  const content = buildApiFile(routes, outDir, opts);
695
- await (0, import_promises3.writeFile)((0, import_node_path3.join)(outDir, "api.ts"), content, "utf8");
633
+ await (0, import_promises3.writeFile)((0, import_node_path4.join)(outDir, "api.ts"), content, "utf8");
696
634
  }
697
635
  function splitName(name) {
698
636
  return name.split(".");
699
637
  }
700
- function toObjectKey2(segment) {
638
+ function toObjectKey(segment) {
701
639
  if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
702
640
  return segment;
703
641
  }
@@ -790,7 +728,7 @@ function emitFilterQueryType(c) {
790
728
  }
791
729
  function buildResponseType(c, outDir) {
792
730
  if (c.controllerRef) {
793
- let relPath = (0, import_node_path3.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
731
+ let relPath = (0, import_node_path4.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
794
732
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
795
733
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
796
734
  }
@@ -804,7 +742,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
804
742
  const pad = " ".repeat(indent);
805
743
  const lines = [];
806
744
  for (const [key, node] of tree) {
807
- const objKey = toObjectKey2(key);
745
+ const objKey = toObjectKey(key);
808
746
  if (node.kind === "leaf") {
809
747
  const c = node;
810
748
  const method = c.method.toUpperCase();
@@ -830,15 +768,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
830
768
  return lines;
831
769
  }
832
770
  function buildRequestModel(c) {
833
- const isGet = c.method.toUpperCase() === "GET";
834
771
  const m = c.method.toLowerCase();
835
772
  const flat = JSON.stringify(c.name);
836
773
  const path = JSON.stringify(c.path);
837
774
  const TA = buildRouterTypeAccess(c.name);
838
775
  const withParams = hasPathParams(c.params);
839
- const hasBody = !!c.contractSource.bodyRef || c.contractSource.body != null && c.contractSource.body !== "never";
840
- const isQuery = isGet || !!c.contractSource.filterFields?.length;
841
- const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
776
+ const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
842
777
  const fields = [];
843
778
  if (withParams) fields.push(`params: ${TA}['params']`);
844
779
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -860,7 +795,6 @@ function buildRequestModel(c) {
860
795
  urlExpr,
861
796
  optsExpr,
862
797
  responseType: `${TA}['response']`,
863
- bodyType: `${TA}['body']`,
864
798
  queryKeyExpr: `[${flat}, input] as const`
865
799
  };
866
800
  }
@@ -908,7 +842,7 @@ function emitApiObjectBlock(tree, indent, p) {
908
842
  const pad = " ".repeat(indent);
909
843
  const lines = [];
910
844
  for (const [key, node] of tree) {
911
- const objKey = toObjectKey2(key);
845
+ const objKey = toObjectKey(key);
912
846
  if (node.kind === "branch") {
913
847
  lines.push(`${pad}${objKey}: {`);
914
848
  lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
@@ -916,30 +850,28 @@ function emitApiObjectBlock(tree, indent, p) {
916
850
  continue;
917
851
  }
918
852
  const req = buildRequestModel(node);
919
- const route = {
920
- method: node.method,
921
- path: node.path,
922
- name: node.name,
923
- params: node.params,
924
- contract: { contractSource: node.contractSource },
925
- ...node.controllerRef ? { controllerRef: node.controllerRef } : {}
853
+ const leaf = {
854
+ route: node.route,
855
+ request: req,
856
+ requestExpr: renderFetcherRequest(req)
926
857
  };
927
- const leaf = { route, request: req, requestExpr: "" };
928
- leaf.requestExpr = p.transport ? p.transport.renderRequest(leaf, p.ctx) : renderFetcherRequest(req);
929
- const members = {};
930
- if (p.layer) Object.assign(members, p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx));
858
+ const owned = /* @__PURE__ */ new Map();
859
+ if (p.layer) {
860
+ mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
861
+ owner: p.layer.name,
862
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
863
+ });
864
+ }
931
865
  for (const ext of p.memberExts) {
932
866
  const extra = ext.apiMembers?.(leaf, p.ctx);
933
867
  if (!extra) continue;
934
- for (const [name, value] of Object.entries(extra)) {
935
- if (name in members) {
936
- throw new Error(
937
- `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict at "${ext.name}").`
938
- );
939
- }
940
- members[name] = value;
941
- }
868
+ mergeExclusive(owned, Object.entries(extra), {
869
+ owner: ext.name,
870
+ describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
871
+ });
942
872
  }
873
+ const members = {};
874
+ for (const [name, { value }] of owned) members[name] = value;
943
875
  lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
944
876
  }
945
877
  return lines;
@@ -948,10 +880,82 @@ function buildRouterTypeAccess(name) {
948
880
  const segments = splitName(name);
949
881
  return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
950
882
  }
883
+ var RESOLVER_HELPERS = [
884
+ // --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
885
+ "type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
886
+ " ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
887
+ " : P extends keyof R ? R[P] : never;",
888
+ "",
889
+ // --- ResolveByName: resolve a field from a dot-path name ---
890
+ "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
891
+ "",
892
+ // --- ResolveByPath: scan all leaves for matching method + url ---
893
+ // Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
894
+ "type _LeafValues<T> = T extends { method: string; url: string }",
895
+ " ? T",
896
+ " : T extends object ? _LeafValues<T[keyof T]> : never;",
897
+ "",
898
+ "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
899
+ " ? L extends { method: M; url: U }",
900
+ " ? Field extends keyof L ? L[Field] : never",
901
+ " : never",
902
+ " : never;",
903
+ ""
904
+ ];
905
+ var ROUTE_NAMESPACE = [
906
+ "export namespace Route {",
907
+ ' export type Response<K extends string> = ResolveByName<K, "response">;',
908
+ ' export type Body<K extends string> = ResolveByName<K, "body">;',
909
+ ' export type Query<K extends string> = ResolveByName<K, "query">;',
910
+ ' export type Params<K extends string> = ResolveByName<K, "params">;',
911
+ ' export type Error<K extends string> = ResolveByName<K, "error">;',
912
+ ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
913
+ " export type Request<K extends string> = {",
914
+ " body: Body<K>;",
915
+ " query: Query<K>;",
916
+ " params: Params<K>;",
917
+ " };",
918
+ "}",
919
+ ""
920
+ ];
921
+ var PATH_NAMESPACE = [
922
+ "export namespace Path {",
923
+ ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
924
+ ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
925
+ ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
926
+ ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
927
+ ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
928
+ ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
929
+ "}",
930
+ ""
931
+ ];
932
+ var EMPTY_ROUTE_NAMESPACE = [
933
+ "export namespace Route {",
934
+ " export type Response<K extends string> = never;",
935
+ " export type Body<K extends string> = never;",
936
+ " export type Query<K extends string> = never;",
937
+ " export type Params<K extends string> = never;",
938
+ " export type Error<K extends string> = never;",
939
+ " export type FilterFields<K extends string> = never;",
940
+ " export type Request<K extends string> = { body: never; query: never; params: never };",
941
+ "}",
942
+ ""
943
+ ];
944
+ var EMPTY_PATH_NAMESPACE = [
945
+ "export namespace Path {",
946
+ " export type Response<M extends string, U extends string> = never;",
947
+ " export type Body<M extends string, U extends string> = never;",
948
+ " export type Query<M extends string, U extends string> = never;",
949
+ " export type Params<M extends string, U extends string> = never;",
950
+ " export type Error<M extends string, U extends string> = never;",
951
+ " export type FilterFields<M extends string, U extends string> = never;",
952
+ "}",
953
+ ""
954
+ ];
951
955
  function buildApiFile(routes, outDir, opts = {}) {
952
956
  const fetcherImportPath = opts.fetcherImportPath;
953
957
  const extensions = opts.extensions ?? [];
954
- const { transport, layer } = resolveApiSlots(extensions);
958
+ const { layer } = resolveApiSlots(extensions);
955
959
  const memberExts = extensions.filter((e) => e.apiMembers);
956
960
  const headerExts = extensions.filter((e) => e.apiHeader);
957
961
  const contracted = routes.filter((r) => r.contract);
@@ -996,7 +1000,6 @@ function buildApiFile(routes, outDir, opts = {}) {
996
1000
  seenImports.add(imp);
997
1001
  extImports.push(imp);
998
1002
  };
999
- for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
1000
1003
  for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
1001
1004
  for (const ext of headerExts) {
1002
1005
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
@@ -1012,8 +1015,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1012
1015
  const emittedNames = /* @__PURE__ */ new Set();
1013
1016
  for (const [filePath, names] of importsByFile) {
1014
1017
  let relPath;
1015
- if ((0, import_node_path3.isAbsolute)(filePath)) {
1016
- relPath = (0, import_node_path3.relative)(outDir, filePath).replace(/\.ts$/, "");
1018
+ if ((0, import_node_path4.isAbsolute)(filePath)) {
1019
+ relPath = (0, import_node_path4.relative)(outDir, filePath).replace(/\.ts$/, "");
1017
1020
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1018
1021
  } else {
1019
1022
  relPath = filePath;
@@ -1041,27 +1044,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1041
1044
  lines.push("}");
1042
1045
  lines.push("export type Api = ReturnType<typeof createApi>;");
1043
1046
  lines.push("");
1044
- lines.push("export namespace Route {");
1045
- lines.push(" export type Response<K extends string> = never;");
1046
- lines.push(" export type Body<K extends string> = never;");
1047
- lines.push(" export type Query<K extends string> = never;");
1048
- lines.push(" export type Params<K extends string> = never;");
1049
- lines.push(" export type Error<K extends string> = never;");
1050
- lines.push(" export type FilterFields<K extends string> = never;");
1051
- lines.push(
1052
- " export type Request<K extends string> = { body: never; query: never; params: never };"
1053
- );
1054
- lines.push("}");
1055
- lines.push("");
1056
- lines.push("export namespace Path {");
1057
- lines.push(" export type Response<M extends string, U extends string> = never;");
1058
- lines.push(" export type Body<M extends string, U extends string> = never;");
1059
- lines.push(" export type Query<M extends string, U extends string> = never;");
1060
- lines.push(" export type Params<M extends string, U extends string> = never;");
1061
- lines.push(" export type Error<M extends string, U extends string> = never;");
1062
- lines.push(" export type FilterFields<M extends string, U extends string> = never;");
1063
- lines.push("}");
1064
- lines.push("");
1047
+ lines.push(...EMPTY_ROUTE_NAMESPACE);
1048
+ lines.push(...EMPTY_PATH_NAMESPACE);
1065
1049
  return lines.join("\n");
1066
1050
  }
1067
1051
  const tree = /* @__PURE__ */ new Map();
@@ -1079,7 +1063,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1079
1063
  path: r.path,
1080
1064
  params: r.params,
1081
1065
  controllerRef: r.controllerRef,
1082
- contractSource: c.contractSource
1066
+ contractSource: c.contractSource,
1067
+ route: r
1083
1068
  };
1084
1069
  insertIntoTree(tree, segments, leaf, name);
1085
1070
  }
@@ -1092,7 +1077,6 @@ function buildApiFile(routes, outDir, opts = {}) {
1092
1077
  lines.push(" return {");
1093
1078
  lines.push(
1094
1079
  ...emitApiObjectBlock(tree, 4, {
1095
- ...transport ? { transport } : {},
1096
1080
  ...layer ? { layer } : {},
1097
1081
  memberExts,
1098
1082
  ctx
@@ -1103,61 +1087,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1103
1087
  lines.push("");
1104
1088
  lines.push("export type Api = ReturnType<typeof createApi>;");
1105
1089
  lines.push("");
1106
- lines.push("type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`");
1107
- lines.push(" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never");
1108
- lines.push(" : P extends keyof R ? R[P] : never;");
1109
- lines.push("");
1110
- lines.push(
1111
- "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
1112
- );
1113
- lines.push("");
1114
- lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
1115
- lines.push(" ? T");
1116
- lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
1117
- lines.push("");
1118
- lines.push(
1119
- "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
1120
- );
1121
- lines.push(" ? L extends { method: M; url: U }");
1122
- lines.push(" ? Field extends keyof L ? L[Field] : never");
1123
- lines.push(" : never");
1124
- lines.push(" : never;");
1125
- lines.push("");
1126
- lines.push("export namespace Route {");
1127
- lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
1128
- lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
1129
- lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
1130
- lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
1131
- lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
1132
- lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
1133
- lines.push(" export type Request<K extends string> = {");
1134
- lines.push(" body: Body<K>;");
1135
- lines.push(" query: Query<K>;");
1136
- lines.push(" params: Params<K>;");
1137
- lines.push(" };");
1138
- lines.push("}");
1139
- lines.push("");
1140
- lines.push("export namespace Path {");
1141
- lines.push(
1142
- ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
1143
- );
1144
- lines.push(
1145
- ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
1146
- );
1147
- lines.push(
1148
- ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
1149
- );
1150
- lines.push(
1151
- ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
1152
- );
1153
- lines.push(
1154
- ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
1155
- );
1156
- lines.push(
1157
- ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
1158
- );
1159
- lines.push("}");
1160
- lines.push("");
1090
+ lines.push(...RESOLVER_HELPERS);
1091
+ lines.push(...ROUTE_NAMESPACE);
1092
+ lines.push(...PATH_NAMESPACE);
1161
1093
  for (const ext of headerExts) {
1162
1094
  const statements = ext.apiHeader?.(ctx)?.statements;
1163
1095
  if (statements?.length) {
@@ -1169,7 +1101,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1169
1101
 
1170
1102
  // src/emit/emit-cache.ts
1171
1103
  var import_promises4 = require("fs/promises");
1172
- var import_node_path4 = require("path");
1104
+ var import_node_path5 = require("path");
1173
1105
  async function emitCache(pages, outDir) {
1174
1106
  await (0, import_promises4.mkdir)(outDir, { recursive: true });
1175
1107
  const entries = await Promise.all(
@@ -1183,95 +1115,21 @@ async function emitCache(pages, outDir) {
1183
1115
  })
1184
1116
  );
1185
1117
  const cache = { pages: entries };
1186
- await (0, import_promises4.writeFile)((0, import_node_path4.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1118
+ await (0, import_promises4.writeFile)((0, import_node_path5.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1187
1119
  `, "utf8");
1188
1120
  }
1189
1121
 
1190
1122
  // src/emit/emit-forms.ts
1191
1123
  var import_promises5 = require("fs/promises");
1192
- var import_node_path5 = require("path");
1124
+ var import_node_path6 = require("path");
1193
1125
  async function emitForms(routes, outDir, config, adapter) {
1194
1126
  if (config && config.enabled === false) return false;
1195
- if (adapter && adapter.name !== "zod") {
1196
- const content2 = buildFormsFileWithAdapter(routes, adapter);
1197
- if (content2 === null) return false;
1198
- await (0, import_promises5.mkdir)(outDir, { recursive: true });
1199
- await (0, import_promises5.writeFile)((0, import_node_path5.join)(outDir, "forms.ts"), content2, "utf8");
1200
- return true;
1201
- }
1202
- const entries = collectFormEntries(routes);
1203
- if (entries.length === 0) return false;
1127
+ const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
1128
+ if (content === null) return false;
1204
1129
  await (0, import_promises5.mkdir)(outDir, { recursive: true });
1205
- const content = buildFormsFile(entries, outDir, config);
1206
- await (0, import_promises5.writeFile)((0, import_node_path5.join)(outDir, "forms.ts"), content, "utf8");
1130
+ await (0, import_promises5.writeFile)((0, import_node_path6.join)(outDir, "forms.ts"), content, "utf8");
1207
1131
  return true;
1208
1132
  }
1209
- function buildFormsFileWithAdapter(routes, adapter) {
1210
- const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1211
- const methodNameCounts = /* @__PURE__ */ new Map();
1212
- for (const route of sorted) {
1213
- const cs = route.contract.contractSource;
1214
- if (!cs.bodySchema && !cs.querySchema) continue;
1215
- methodNameCounts.set(
1216
- deriveBaseName(route.name).method,
1217
- (methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
1218
- );
1219
- }
1220
- const named = /* @__PURE__ */ new Map();
1221
- const decls = [];
1222
- const mapEntries = [];
1223
- let used = false;
1224
- for (const route of sorted) {
1225
- const cs = route.contract.contractSource;
1226
- const { method, full } = deriveBaseName(route.name);
1227
- const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
1228
- const block = [];
1229
- if (cs.bodyZodText && !cs.bodySchema) {
1230
- block.push(
1231
- `// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1232
- );
1233
- }
1234
- let bodyConst;
1235
- if (cs.bodySchema) {
1236
- used = true;
1237
- const r = adapter.renderModule(cs.bodySchema);
1238
- for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
1239
- bodyConst = `${base}BodySchema`;
1240
- block.push(`export const ${bodyConst} = ${r.schemaText};`);
1241
- block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
1242
- }
1243
- if (cs.querySchema) {
1244
- used = true;
1245
- const r = adapter.renderModule(cs.querySchema);
1246
- for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
1247
- const queryConst = `${base}QuerySchema`;
1248
- block.push(`export const ${queryConst} = ${r.schemaText};`);
1249
- block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
1250
- }
1251
- if (block.length === 0) continue;
1252
- decls.push(`// ${route.name}`, ...block, "");
1253
- if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
1254
- }
1255
- if (!used) return null;
1256
- const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1257
- for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1258
- lines.push("");
1259
- if (named.size > 0) {
1260
- lines.push("// Hoisted nested schemas (shared across endpoints).");
1261
- for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
1262
- lines.push("");
1263
- }
1264
- lines.push(...decls);
1265
- lines.push("/** Route name \u2192 body schema map. */");
1266
- lines.push("export const formSchemas = {");
1267
- lines.push(...mapEntries);
1268
- lines.push("} as const;");
1269
- lines.push("");
1270
- return lines.join("\n");
1271
- }
1272
- function hasSchema(src) {
1273
- return !!src && (src.ref !== null || src.text !== null);
1274
- }
1275
1133
  function pascal(segment) {
1276
1134
  return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1277
1135
  }
@@ -1281,124 +1139,16 @@ function deriveBaseName(routeName) {
1281
1139
  const full = segments.map(pascal).join("");
1282
1140
  return { method, full };
1283
1141
  }
1284
- function collectFormEntries(routes) {
1285
- const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1286
- const methodNameCounts = /* @__PURE__ */ new Map();
1287
- const candidates = [];
1288
- for (const route of sorted) {
1289
- const cs = route.contract.contractSource;
1290
- const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
1291
- const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
1292
- if (!hasSchema(body) && !hasSchema(query)) continue;
1293
- const { method, full } = deriveBaseName(route.name);
1294
- methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1295
- candidates.push({ route, method, full });
1296
- }
1297
- const entries = [];
1298
- for (const { route, method, full } of candidates) {
1299
- const cs = route.contract.contractSource;
1300
- const collision = (methodNameCounts.get(method) ?? 0) > 1;
1301
- const baseName = collision ? full : method;
1302
- const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
1303
- const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
1304
- entries.push({
1305
- routeName: route.name,
1306
- baseName,
1307
- body: hasSchema(body) ? body : void 0,
1308
- query: hasSchema(query) ? query : void 0,
1309
- nestedSchemas: cs.formNestedSchemas ?? null,
1310
- warnings: cs.formWarnings ?? []
1311
- });
1312
- }
1313
- return entries;
1314
- }
1315
1142
  function relImport(outDir, filePath) {
1316
- let relPath = (0, import_node_path5.relative)(outDir, filePath).replace(/\.ts$/, "");
1143
+ let relPath = (0, import_node_path6.relative)(outDir, filePath).replace(/\.ts$/, "");
1317
1144
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1318
1145
  return relPath;
1319
1146
  }
1320
1147
  function refRootIdentifier(refName) {
1321
1148
  return refName.split(".")[0] ?? refName;
1322
1149
  }
1323
- function buildFormsFile(entries, outDir, config) {
1324
- const zodImport = config?.zodImport ?? "zod";
1325
- const lines = [
1326
- "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
1327
- `import { z } from '${zodImport}';`
1328
- ];
1329
- const importsByFile = /* @__PURE__ */ new Map();
1330
- const refAlias = /* @__PURE__ */ new Map();
1331
- for (const entry of entries) {
1332
- for (const src of [entry.body, entry.query]) {
1333
- if (src?.ref && !src.text) {
1334
- const root = refRootIdentifier(src.ref.name);
1335
- const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
1336
- set.add(root);
1337
- importsByFile.set(src.ref.filePath, set);
1338
- }
1339
- }
1340
- }
1341
- if (importsByFile.size > 0) {
1342
- const emitted = /* @__PURE__ */ new Set();
1343
- for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1344
- const relPath = relImport(outDir, filePath);
1345
- const specifiers = [];
1346
- for (const root of [...roots].sort()) {
1347
- if (emitted.has(root)) {
1348
- const alias = `${root}_${emitted.size}`;
1349
- specifiers.push(`${root} as ${alias}`);
1350
- emitted.add(alias);
1351
- refAlias.set(`${filePath}\0${root}`, alias);
1352
- } else {
1353
- specifiers.push(root);
1354
- emitted.add(root);
1355
- refAlias.set(`${filePath}\0${root}`, root);
1356
- }
1357
- }
1358
- lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1359
- }
1360
- }
1361
- lines.push("");
1362
- const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1363
- if (globalSchemas.size > 0) {
1364
- lines.push("// Hoisted nested schemas (shared across endpoints).");
1365
- for (const [name, text] of globalSchemas) {
1366
- lines.push(`const ${name} = ${text};`);
1367
- }
1368
- lines.push("");
1369
- }
1370
- const mapEntries = [];
1371
- for (const entry of entries) {
1372
- lines.push(`// ${entry.routeName}`);
1373
- if (entry.warnings && entry.warnings.length > 0) {
1374
- for (const w of entry.warnings) {
1375
- lines.push(`// warning: ${w}`);
1376
- }
1377
- }
1378
- const rename = renamesByEntry.get(entry) ?? null;
1379
- if (entry.body) {
1380
- const schemaName = `${entry.baseName}BodySchema`;
1381
- const typeName = `${entry.baseName}Body`;
1382
- const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
1383
- lines.push(`export const ${schemaName} = ${text};`);
1384
- lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
1385
- mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
1386
- }
1387
- if (entry.query) {
1388
- const schemaName = `${entry.baseName}QuerySchema`;
1389
- const typeName = `${entry.baseName}Query`;
1390
- const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
1391
- lines.push(`export const ${schemaName} = ${text};`);
1392
- lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
1393
- }
1394
- lines.push("");
1395
- }
1396
- lines.push("/** Route name \u2192 body schema map. */");
1397
- lines.push("export const formSchemas = {");
1398
- lines.push(...mapEntries);
1399
- lines.push("} as const;");
1400
- lines.push("");
1401
- return lines.join("\n");
1150
+ function hasSource(src) {
1151
+ return !!(src.schema || src.zodText || src.zodRef);
1402
1152
  }
1403
1153
  function applyRenames(text, renames) {
1404
1154
  if (!renames || renames.size === 0) return text;
@@ -1464,44 +1214,206 @@ function planNestedSchemas(entries) {
1464
1214
  }
1465
1215
  return { globalSchemas, renamesByEntry };
1466
1216
  }
1467
- function renderSchema(src, outDir, refAlias) {
1468
- if (src.text) return src.text;
1469
- if (src.ref) {
1470
- const root = refRootIdentifier(src.ref.name);
1471
- const alias = refAlias.get(`${src.ref.filePath}\0${root}`) ?? root;
1472
- const member = src.ref.name.slice(root.length);
1473
- return `${alias}${member}`;
1474
- }
1475
- return "z.unknown()";
1476
- }
1477
-
1478
- // src/emit/emit-index.ts
1479
- var import_promises6 = require("fs/promises");
1480
- var import_node_path6 = require("path");
1481
- async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1482
- await (0, import_promises6.mkdir)(outDir, { recursive: true });
1483
- const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
1484
- if (hasContracts) {
1485
- exports2.push("export * from './api.js';");
1217
+ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1218
+ const acceptsRawZod = adapter.acceptsRawZodSource === true;
1219
+ const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
1220
+ const methodNameCounts = /* @__PURE__ */ new Map();
1221
+ const candidates = [];
1222
+ for (const route of sorted) {
1223
+ const cs = route.contract.contractSource;
1224
+ const body = {
1225
+ schema: cs.bodySchema ?? null,
1226
+ zodText: cs.bodyZodText ?? null,
1227
+ zodRef: cs.bodyZodRef ?? null
1228
+ };
1229
+ const query = {
1230
+ schema: cs.querySchema ?? null,
1231
+ zodText: cs.queryZodText ?? null,
1232
+ zodRef: cs.queryZodRef ?? null
1233
+ };
1234
+ if (!hasSource(body) && !hasSource(query)) continue;
1235
+ const { method, full } = deriveBaseName(route.name);
1236
+ methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
1237
+ candidates.push({
1238
+ routeName: route.name,
1239
+ baseName: full,
1240
+ // resolved below
1241
+ body: hasSource(body) ? body : void 0,
1242
+ query: hasSource(query) ? query : void 0,
1243
+ nestedSchemas: cs.formNestedSchemas ?? null,
1244
+ warnings: cs.formWarnings ?? []
1245
+ });
1486
1246
  }
1487
- if (hasForms) {
1488
- exports2.push("export * from './forms.js';");
1247
+ const entries = candidates.map((c) => {
1248
+ const { method, full } = deriveBaseName(c.routeName);
1249
+ const collision = (methodNameCounts.get(method) ?? 0) > 1;
1250
+ return { ...c, baseName: collision ? full : method };
1251
+ });
1252
+ if (entries.length === 0) return null;
1253
+ const importsByFile = /* @__PURE__ */ new Map();
1254
+ const refAlias = /* @__PURE__ */ new Map();
1255
+ for (const entry of entries) {
1256
+ for (const src of [entry.body, entry.query]) {
1257
+ if (src?.zodRef && !src.zodText && !src.schema) {
1258
+ const root = refRootIdentifier(src.zodRef.name);
1259
+ const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
1260
+ set.add(root);
1261
+ importsByFile.set(src.zodRef.filePath, set);
1262
+ }
1263
+ }
1489
1264
  }
1490
- const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
1491
- "\n"
1492
- );
1493
- await (0, import_promises6.writeFile)((0, import_node_path6.join)(outDir, "index.d.ts"), content, "utf8");
1494
- }
1495
-
1496
- // src/emit/emit-pages.ts
1497
- var import_promises7 = require("fs/promises");
1498
- var import_node_path7 = require("path");
1499
- async function emitPages(pages, outDir, _options = {}) {
1500
- await (0, import_promises7.mkdir)(outDir, { recursive: true });
1501
- const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1502
- const augBody = pages.map((p) => {
1503
- const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
1504
- const valueType = buildAugmentationType(p, outDir);
1265
+ const importLines = [];
1266
+ if (importsByFile.size > 0) {
1267
+ const emitted = /* @__PURE__ */ new Set();
1268
+ for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
1269
+ const relPath = relImport(outDir, filePath);
1270
+ const specifiers = [];
1271
+ for (const root of [...roots].sort()) {
1272
+ if (emitted.has(root)) {
1273
+ const alias = `${root}_${emitted.size}`;
1274
+ specifiers.push(`${root} as ${alias}`);
1275
+ emitted.add(alias);
1276
+ refAlias.set(`${filePath}\0${root}`, alias);
1277
+ } else {
1278
+ specifiers.push(root);
1279
+ emitted.add(root);
1280
+ refAlias.set(`${filePath}\0${root}`, root);
1281
+ }
1282
+ }
1283
+ importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1284
+ }
1285
+ }
1286
+ const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
1287
+ const irNamed = /* @__PURE__ */ new Map();
1288
+ const irTypeAliases = /* @__PURE__ */ new Map();
1289
+ const irAnnotations = /* @__PURE__ */ new Map();
1290
+ const decls = [];
1291
+ const mapEntries = [];
1292
+ let used = false;
1293
+ const renderSource = (src, rename) => {
1294
+ if (src.schema) {
1295
+ const r = adapter.renderModule(src.schema);
1296
+ for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1297
+ if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
1298
+ if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
1299
+ return { text: r.schemaText };
1300
+ }
1301
+ if (src.zodText) {
1302
+ if (!acceptsRawZod) {
1303
+ return {
1304
+ text: "",
1305
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1306
+ };
1307
+ }
1308
+ return { text: applyRenames(src.zodText, rename) };
1309
+ }
1310
+ if (src.zodRef) {
1311
+ if (!acceptsRawZod) {
1312
+ return {
1313
+ text: "",
1314
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1315
+ };
1316
+ }
1317
+ const root = refRootIdentifier(src.zodRef.name);
1318
+ const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
1319
+ const member = src.zodRef.name.slice(root.length);
1320
+ return { text: `${alias}${member}` };
1321
+ }
1322
+ return null;
1323
+ };
1324
+ for (const entry of entries) {
1325
+ const block = [];
1326
+ const rename = renamesByEntry.get(entry) ?? null;
1327
+ let bodyConst;
1328
+ if (entry.warnings && entry.warnings.length > 0) {
1329
+ for (const w of entry.warnings) block.push(`// warning: ${w}`);
1330
+ }
1331
+ if (entry.body) {
1332
+ const rendered = renderSource(entry.body, rename);
1333
+ if (rendered?.warn) {
1334
+ block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
1335
+ } else if (rendered) {
1336
+ used = true;
1337
+ bodyConst = `${entry.baseName}BodySchema`;
1338
+ block.push(`export const ${bodyConst} = ${rendered.text};`);
1339
+ block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
1340
+ }
1341
+ }
1342
+ if (entry.query) {
1343
+ const rendered = renderSource(entry.query, rename);
1344
+ if (rendered?.warn) {
1345
+ block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
1346
+ } else if (rendered) {
1347
+ used = true;
1348
+ const queryConst = `${entry.baseName}QuerySchema`;
1349
+ block.push(`export const ${queryConst} = ${rendered.text};`);
1350
+ block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
1351
+ }
1352
+ }
1353
+ if (block.length === 0) continue;
1354
+ decls.push(`// ${entry.routeName}`, ...block, "");
1355
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
1356
+ }
1357
+ if (!used) return null;
1358
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1359
+ if (acceptsRawZod) {
1360
+ const zodImport = config?.zodImport ?? "zod";
1361
+ lines.push(`import { z } from '${zodImport}';`);
1362
+ } else {
1363
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1364
+ }
1365
+ lines.push(...importLines);
1366
+ lines.push("");
1367
+ const allNested = /* @__PURE__ */ new Map();
1368
+ for (const [n, t] of globalSchemas) allNested.set(n, t);
1369
+ for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1370
+ if (allNested.size > 0) {
1371
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
1372
+ for (const [n, alias] of irTypeAliases) {
1373
+ if (allNested.has(n)) lines.push(`${alias};`);
1374
+ }
1375
+ for (const [n, t] of allNested) {
1376
+ const annotation = irAnnotations.get(n);
1377
+ lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
1378
+ }
1379
+ lines.push("");
1380
+ }
1381
+ lines.push(...decls);
1382
+ lines.push("/** Route name \u2192 body schema map. */");
1383
+ lines.push("export const formSchemas = {");
1384
+ lines.push(...mapEntries);
1385
+ lines.push("} as const;");
1386
+ lines.push("");
1387
+ return lines.join("\n");
1388
+ }
1389
+
1390
+ // src/emit/emit-index.ts
1391
+ var import_promises6 = require("fs/promises");
1392
+ var import_node_path7 = require("path");
1393
+ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1394
+ await (0, import_promises6.mkdir)(outDir, { recursive: true });
1395
+ const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
1396
+ if (hasContracts) {
1397
+ exports2.push("export * from './api.js';");
1398
+ }
1399
+ if (hasForms) {
1400
+ exports2.push("export * from './forms.js';");
1401
+ }
1402
+ const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
1403
+ "\n"
1404
+ );
1405
+ await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
1406
+ }
1407
+
1408
+ // src/emit/emit-pages.ts
1409
+ var import_promises7 = require("fs/promises");
1410
+ var import_node_path8 = require("path");
1411
+ async function emitPages(pages, outDir, _options = {}) {
1412
+ await (0, import_promises7.mkdir)(outDir, { recursive: true });
1413
+ const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1414
+ const augBody = pages.map((p) => {
1415
+ const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
1416
+ const valueType = buildAugmentationType(p, outDir);
1505
1417
  return ` ${key}: ${valueType};`;
1506
1418
  }).join("\n");
1507
1419
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
@@ -1516,7 +1428,7 @@ ${augBody}
1516
1428
  }
1517
1429
  ${sharedPropsBlock}}
1518
1430
  `;
1519
- await (0, import_promises7.writeFile)((0, import_node_path7.join)(outDir, "pages.d.ts"), content, "utf8");
1431
+ await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, "pages.d.ts"), content, "utf8");
1520
1432
  }
1521
1433
  function buildSharedPropsBlock(sharedProps) {
1522
1434
  if (!sharedProps) return "";
@@ -1535,7 +1447,7 @@ ${propsBody}
1535
1447
  `;
1536
1448
  }
1537
1449
  function buildAugmentationType(page, outDir) {
1538
- let importPath = (0, import_node_path7.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1450
+ let importPath = (0, import_node_path8.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1539
1451
  if (!importPath.startsWith(".")) {
1540
1452
  importPath = `./${importPath}`;
1541
1453
  }
@@ -1547,11 +1459,11 @@ function needsQuotes(name) {
1547
1459
 
1548
1460
  // src/emit/emit-routes.ts
1549
1461
  var import_promises8 = require("fs/promises");
1550
- var import_node_path8 = require("path");
1462
+ var import_node_path9 = require("path");
1551
1463
  async function emitRoutes(routes, outDir) {
1552
1464
  await (0, import_promises8.mkdir)(outDir, { recursive: true });
1553
1465
  const content = buildRoutesFile(routes);
1554
- await (0, import_promises8.writeFile)((0, import_node_path8.join)(outDir, "routes.ts"), content, "utf8");
1466
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, "routes.ts"), content, "utf8");
1555
1467
  }
1556
1468
  function buildRoutesFile(routes) {
1557
1469
  if (routes.length === 0) {
@@ -1679,30 +1591,7 @@ async function generate(config, inputRoutes = []) {
1679
1591
  propsExport: pagesConfig.propsExport,
1680
1592
  componentNameStrategy: pagesConfig.componentNameStrategy
1681
1593
  });
1682
- let sharedProps = null;
1683
- if (config.app?.moduleEntry) {
1684
- try {
1685
- const tsconfigPath = config.app.tsconfig ?? (0, import_node_path9.join)(config.codegen.cwd, "tsconfig.json");
1686
- let project;
1687
- try {
1688
- project = new import_ts_morph3.Project({
1689
- tsConfigFilePath: tsconfigPath,
1690
- skipAddingFilesFromTsConfig: true,
1691
- skipLoadingLibFiles: true,
1692
- skipFileDependencyResolution: true
1693
- });
1694
- } catch {
1695
- project = new import_ts_morph3.Project({
1696
- skipAddingFilesFromTsConfig: true,
1697
- skipLoadingLibFiles: true,
1698
- skipFileDependencyResolution: true,
1699
- compilerOptions: { allowJs: true, strict: false }
1700
- });
1701
- }
1702
- sharedProps = discoverSharedProps(project, config.app.moduleEntry);
1703
- } catch {
1704
- }
1705
- }
1594
+ const sharedProps = discoverSharedPropsFromConfig(config);
1706
1595
  await emitPages(pages, config.codegen.outDir, {
1707
1596
  propsExport: pagesConfig.propsExport,
1708
1597
  sharedProps
@@ -1726,8 +1615,8 @@ async function generate(config, inputRoutes = []) {
1726
1615
  if (extensions.length > 0) {
1727
1616
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1728
1617
  for (const file of extraFiles) {
1729
- const dest = (0, import_node_path9.join)(config.codegen.outDir, file.path);
1730
- await (0, import_promises9.mkdir)((0, import_node_path9.dirname)(dest), { recursive: true });
1618
+ const dest = (0, import_node_path10.join)(config.codegen.outDir, file.path);
1619
+ await (0, import_promises9.mkdir)((0, import_node_path10.dirname)(dest), { recursive: true });
1731
1620
  await (0, import_promises9.writeFile)(dest, file.contents, "utf8");
1732
1621
  }
1733
1622
  }
@@ -1735,35 +1624,31 @@ async function generate(config, inputRoutes = []) {
1735
1624
 
1736
1625
  // src/watch/watcher.ts
1737
1626
  var import_promises12 = require("fs/promises");
1738
- var import_node_path13 = require("path");
1627
+ var import_node_path14 = require("path");
1739
1628
  var import_chokidar = __toESM(require("chokidar"), 1);
1740
1629
 
1741
1630
  // src/discovery/contracts-fast.ts
1742
- var import_node_path11 = require("path");
1631
+ var import_node_path12 = require("path");
1743
1632
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
1744
1633
  var import_ts_morph9 = require("ts-morph");
1745
1634
 
1635
+ // src/discovery/dto-type-resolver.ts
1636
+ var import_ts_morph7 = require("ts-morph");
1637
+
1746
1638
  // src/discovery/dto-to-ir.ts
1747
- var import_ts_morph5 = require("ts-morph");
1639
+ var import_ts_morph4 = require("ts-morph");
1748
1640
 
1749
1641
  // src/discovery/type-ref-resolution.ts
1750
1642
  var import_node_fs = require("fs");
1751
- var import_node_path10 = require("path");
1752
- var import_ts_morph4 = require("ts-morph");
1753
- var _ctx = { projectRoot: "", tsconfigPaths: null };
1754
- function setDiscoveryContext(ctx) {
1755
- const prev = _ctx;
1756
- _ctx = ctx;
1757
- return prev;
1758
- }
1759
- function restoreDiscoveryContext(ctx) {
1760
- _ctx = ctx;
1761
- }
1762
- function _projectRoot() {
1763
- return _ctx.projectRoot;
1643
+ var import_node_path11 = require("path");
1644
+ var import_ts_morph3 = require("ts-morph");
1645
+ var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1646
+ var _ctxByProject = /* @__PURE__ */ new WeakMap();
1647
+ function setDiscoveryContext(project, ctx) {
1648
+ _ctxByProject.set(project, ctx);
1764
1649
  }
1765
- function _tsconfigPaths() {
1766
- return _ctx.tsconfigPaths;
1650
+ function _ctxFor(project) {
1651
+ return _ctxByProject.get(project) ?? _EMPTY_CTX;
1767
1652
  }
1768
1653
  var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
1769
1654
  function dbg(...args) {
@@ -1805,18 +1690,19 @@ function findTypeInFile(name, file) {
1805
1690
  }
1806
1691
  return null;
1807
1692
  }
1808
- function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1693
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1809
1694
  if (moduleSpecifier.startsWith(".")) {
1810
- const dir = (0, import_node_path10.dirname)(sourceFile.getFilePath());
1695
+ const dir = (0, import_node_path11.dirname)(sourceFile.getFilePath());
1811
1696
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
1812
1697
  return [
1813
- (0, import_node_path10.resolve)(dir, `${noExt}.ts`),
1814
- (0, import_node_path10.resolve)(dir, `${moduleSpecifier}.ts`),
1815
- (0, import_node_path10.resolve)(dir, moduleSpecifier, "index.ts")
1698
+ (0, import_node_path11.resolve)(dir, `${noExt}.ts`),
1699
+ (0, import_node_path11.resolve)(dir, `${moduleSpecifier}.ts`),
1700
+ (0, import_node_path11.resolve)(dir, moduleSpecifier, "index.ts")
1816
1701
  ];
1817
1702
  }
1818
- const baseUrl = _projectRoot();
1819
- const tsconfigPaths = _tsconfigPaths();
1703
+ const ctx = _ctxFor(project);
1704
+ const baseUrl = ctx.projectRoot;
1705
+ const tsconfigPaths = ctx.tsconfigPaths;
1820
1706
  dbg(
1821
1707
  "resolveModuleSpecifier",
1822
1708
  moduleSpecifier,
@@ -1832,8 +1718,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1832
1718
  const rest = moduleSpecifier.slice(prefix.length);
1833
1719
  const candidates = [];
1834
1720
  for (const mapping of mappings) {
1835
- const resolved = (0, import_node_path10.resolve)(baseUrl, mapping.replace("*", rest));
1836
- candidates.push(`${resolved}.ts`, (0, import_node_path10.resolve)(resolved, "index.ts"));
1721
+ const resolved = (0, import_node_path11.resolve)(baseUrl, mapping.replace("*", rest));
1722
+ candidates.push(`${resolved}.ts`, (0, import_node_path11.resolve)(resolved, "index.ts"));
1837
1723
  }
1838
1724
  dbg(" resolved candidates:", candidates);
1839
1725
  return candidates;
@@ -1933,13 +1819,13 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1933
1819
  name = nodeOrName;
1934
1820
  } else {
1935
1821
  const typeNode = nodeOrName;
1936
- if (opts.unwrapContainers && import_ts_morph4.Node.isArrayTypeNode(typeNode)) {
1822
+ if (opts.unwrapContainers && import_ts_morph3.Node.isArrayTypeNode(typeNode)) {
1937
1823
  const inner = resolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project, opts);
1938
1824
  return inner ? { ...inner, isArray: true } : null;
1939
1825
  }
1940
- if (!import_ts_morph4.Node.isTypeReference(typeNode)) return null;
1826
+ if (!import_ts_morph3.Node.isTypeReference(typeNode)) return null;
1941
1827
  const typeName = typeNode.getTypeName();
1942
- const refName = import_ts_morph4.Node.isIdentifier(typeName) ? typeName.getText() : null;
1828
+ const refName = import_ts_morph3.Node.isIdentifier(typeName) ? typeName.getText() : null;
1943
1829
  if (!refName) return null;
1944
1830
  if (opts.unwrapContainers && refName === "Promise") {
1945
1831
  const first = typeNode.getTypeArguments()[0];
@@ -1962,7 +1848,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1962
1848
  if (!namedImport) continue;
1963
1849
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
1964
1850
  if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
1965
- const tsconfigPaths = _tsconfigPaths();
1851
+ const tsconfigPaths = _ctxFor(project).tsconfigPaths;
1966
1852
  const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
1967
1853
  const prefix = p.replace("*", "");
1968
1854
  return moduleSpecifier.startsWith(prefix);
@@ -2031,10 +1917,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
2031
1917
  depth: 0
2032
1918
  };
2033
1919
  const root = buildObject(classDecl, sourceFile, ctx);
2034
- for (const schemaName of ctx.recursiveSchemas) {
2035
- ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
2036
- }
2037
- return { root, named: ctx.named, warnings: ctx.warnings };
1920
+ return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
2038
1921
  }
2039
1922
  function buildObject(classDecl, classFile, ctx) {
2040
1923
  const props = classDecl.getProperties();
@@ -2054,7 +1937,7 @@ function buildProperty(prop, classFile, ctx) {
2054
1937
  const dec = (n) => decorators.get(n);
2055
1938
  const typeNode = prop.getTypeNode();
2056
1939
  const typeText = typeNode?.getText() ?? "unknown";
2057
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
1940
+ const isArrayType = !!typeNode && import_ts_morph4.Node.isArrayTypeNode(typeNode);
2058
1941
  const typeRefName = resolveTypeFactoryName(dec("Type"));
2059
1942
  if (has("ValidateNested") || typeRefName) {
2060
1943
  const childName = typeRefName ?? singularClassName(typeText);
@@ -2185,18 +2068,27 @@ function baseFromType(typeText, isArrayType) {
2185
2068
  }
2186
2069
  }
2187
2070
  function buildNestedReference(className, fromFile, ctx) {
2188
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2071
+ if (ctx.visiting.has(className)) {
2189
2072
  const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
2190
2073
  ctx.emittedClasses.set(className, reserved);
2191
2074
  ctx.recursiveSchemas.add(reserved);
2192
2075
  if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2193
2076
  ctx.warnedDecorators.add(`recursive:${reserved}`);
2194
- const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
2077
+ const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
2195
2078
  ctx.warnings.push(msg);
2196
2079
  console.warn(`[nestjs-codegen] ${msg}`);
2197
2080
  }
2198
2081
  return { kind: "lazyRef", name: reserved };
2199
2082
  }
2083
+ if (ctx.depth >= 8) {
2084
+ if (!ctx.warnedDecorators.has(`deep:${className}`)) {
2085
+ ctx.warnedDecorators.add(`deep:${className}`);
2086
+ const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
2087
+ ctx.warnings.push(msg);
2088
+ console.warn(`[nestjs-codegen] ${msg}`);
2089
+ }
2090
+ return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
2091
+ }
2200
2092
  const existing = ctx.emittedClasses.get(className);
2201
2093
  if (existing) return { kind: "ref", name: existing };
2202
2094
  const schemaName = aliasFor(className, ctx);
@@ -2234,22 +2126,22 @@ function firstArgText(decorator) {
2234
2126
  }
2235
2127
  function numericArg(decorator) {
2236
2128
  const arg = firstArg(decorator);
2237
- if (arg && import_ts_morph5.Node.isNumericLiteral(arg)) return arg.getText();
2129
+ if (arg && import_ts_morph4.Node.isNumericLiteral(arg)) return arg.getText();
2238
2130
  return null;
2239
2131
  }
2240
2132
  function numericArgs(decorator) {
2241
2133
  const args = decorator?.getArguments() ?? [];
2242
- const num = (n) => n && import_ts_morph5.Node.isNumericLiteral(n) ? n.getText() : null;
2134
+ const num = (n) => n && import_ts_morph4.Node.isNumericLiteral(n) ? n.getText() : null;
2243
2135
  return [num(args[0]), num(args[1])];
2244
2136
  }
2245
2137
  function messageRaw(decorator) {
2246
2138
  const args = decorator?.getArguments() ?? [];
2247
2139
  for (const arg of args) {
2248
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2140
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
2249
2141
  for (const prop of arg.getProperties()) {
2250
- if (import_ts_morph5.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2142
+ if (import_ts_morph4.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2251
2143
  const init = prop.getInitializer();
2252
- if (init && import_ts_morph5.Node.isStringLiteral(init)) return init.getText();
2144
+ if (init && import_ts_morph4.Node.isStringLiteral(init)) return init.getText();
2253
2145
  }
2254
2146
  }
2255
2147
  }
@@ -2259,9 +2151,9 @@ function messageRaw(decorator) {
2259
2151
  function resolveTypeFactoryName(decorator) {
2260
2152
  const arg = firstArg(decorator);
2261
2153
  if (!arg) return null;
2262
- if (import_ts_morph5.Node.isArrowFunction(arg)) {
2154
+ if (import_ts_morph4.Node.isArrowFunction(arg)) {
2263
2155
  const body = arg.getBody();
2264
- if (import_ts_morph5.Node.isIdentifier(body)) return body.getText();
2156
+ if (import_ts_morph4.Node.isIdentifier(body)) return body.getText();
2265
2157
  }
2266
2158
  return null;
2267
2159
  }
@@ -2272,7 +2164,7 @@ function singularClassName(typeText) {
2272
2164
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2273
2165
  const arg = firstArg(decorator);
2274
2166
  if (!arg) return null;
2275
- if (import_ts_morph5.Node.isIdentifier(arg)) {
2167
+ if (import_ts_morph4.Node.isIdentifier(arg)) {
2276
2168
  const name = arg.getText();
2277
2169
  const resolved = findType(name, classFile, ctx.project);
2278
2170
  if (resolved && resolved.kind === "enum") {
@@ -2286,12 +2178,12 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
2286
2178
  }
2287
2179
  return { kind: "unknown", note: `@IsEnum(${name}): enum not resolvable to literals` };
2288
2180
  }
2289
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2181
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
2290
2182
  const values = [];
2291
2183
  for (const p of arg.getProperties()) {
2292
- if (!import_ts_morph5.Node.isPropertyAssignment(p)) continue;
2184
+ if (!import_ts_morph4.Node.isPropertyAssignment(p)) continue;
2293
2185
  const init = p.getInitializer();
2294
- if (init && import_ts_morph5.Node.isStringLiteral(init)) values.push(init.getText());
2186
+ if (init && import_ts_morph4.Node.isStringLiteral(init)) values.push(init.getText());
2295
2187
  }
2296
2188
  if (values.length > 0) return { kind: "enum", literals: values };
2297
2189
  }
@@ -2299,9 +2191,9 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
2299
2191
  }
2300
2192
  function inSchemaFromDecorator(decorator) {
2301
2193
  const arg = firstArg(decorator);
2302
- if (arg && import_ts_morph5.Node.isArrayLiteralExpression(arg)) {
2194
+ if (arg && import_ts_morph4.Node.isArrayLiteralExpression(arg)) {
2303
2195
  const elements = arg.getElements();
2304
- const allStrings = elements.every((e) => import_ts_morph5.Node.isStringLiteral(e));
2196
+ const allStrings = elements.every((e) => import_ts_morph4.Node.isStringLiteral(e));
2305
2197
  if (allStrings && elements.length > 0) {
2306
2198
  return { kind: "enum", literals: elements.map((e) => e.getText()) };
2307
2199
  }
@@ -2315,427 +2207,99 @@ function inSchemaFromDecorator(decorator) {
2315
2207
  return null;
2316
2208
  }
2317
2209
 
2318
- // src/discovery/dto-to-zod.ts
2210
+ // src/discovery/filter-for.ts
2319
2211
  var import_ts_morph6 = require("ts-morph");
2320
- var KNOWN_DECORATORS2 = /* @__PURE__ */ new Set([
2321
- "IsString",
2322
- "IsNumber",
2323
- "IsInt",
2324
- "IsBoolean",
2325
- "IsDate",
2326
- "IsEmail",
2327
- "IsUrl",
2328
- "IsUUID",
2329
- "MinLength",
2330
- "MaxLength",
2331
- "Length",
2332
- "Min",
2333
- "Max",
2334
- "IsPositive",
2335
- "IsNegative",
2336
- "Matches",
2337
- "IsEnum",
2338
- "IsIn",
2339
- "IsOptional",
2340
- "IsNotEmpty",
2341
- "IsArray",
2342
- "ValidateNested",
2343
- "Type",
2344
- "IsObject",
2345
- "Allow",
2346
- "IsDefined"
2347
- ]);
2348
- function extractZodFromDto(classDecl, sourceFile, project) {
2349
- const ctx = {
2350
- sourceFile,
2351
- project,
2352
- namedNestedSchemas: /* @__PURE__ */ new Map(),
2353
- warnings: [],
2354
- warnedDecorators: /* @__PURE__ */ new Set(),
2355
- emittedClasses: /* @__PURE__ */ new Map(),
2356
- visiting: /* @__PURE__ */ new Set(),
2357
- recursiveSchemas: /* @__PURE__ */ new Set(),
2358
- depth: 0
2359
- };
2360
- const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
2361
- for (const schemaName of ctx.recursiveSchemas) {
2362
- ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
2363
- }
2364
- return {
2365
- schemaText,
2366
- namedNestedSchemas: ctx.namedNestedSchemas,
2367
- warnings: ctx.warnings
2368
- };
2212
+
2213
+ // src/discovery/filter-field-types.ts
2214
+ var import_ts_morph5 = require("ts-morph");
2215
+
2216
+ // src/discovery/enum-resolution.ts
2217
+ function resolveEnumValues(name, sourceFile, project) {
2218
+ const resolved = findType(name, sourceFile, project);
2219
+ if (!resolved || resolved.kind !== "enum") return null;
2220
+ let numeric = true;
2221
+ const values = resolved.members.map((m) => {
2222
+ const parsed = JSON.parse(m);
2223
+ if (typeof parsed === "string") numeric = false;
2224
+ return String(parsed);
2225
+ });
2226
+ if (values.length === 0) return null;
2227
+ return { values, numeric };
2369
2228
  }
2370
- function buildObjectSchema(classDecl, classFile, ctx) {
2371
- const props = classDecl.getProperties();
2372
- if (props.length === 0) {
2373
- return "z.object({}).passthrough()";
2374
- }
2375
- const fields = [];
2376
- for (const prop of props) {
2377
- const name = prop.getName();
2378
- const expr = buildPropertySchema(prop, classFile, ctx);
2379
- fields.push(`${toObjectKey3(name)}: ${expr}`);
2380
- }
2381
- return `z.object({ ${fields.join(", ")} })`;
2229
+
2230
+ // src/discovery/filter-field-types.ts
2231
+ var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2232
+ var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2233
+ var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2234
+ var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2235
+ var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2236
+ function classifyTypeKeyword(raw) {
2237
+ const t = raw.toLowerCase();
2238
+ if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2239
+ if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2240
+ if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2241
+ if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2242
+ if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2243
+ return null;
2382
2244
  }
2383
- function toObjectKey3(name) {
2384
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2245
+ function markNullable(r, nullable) {
2246
+ return nullable ? { ...r, nullable: true } : r;
2385
2247
  }
2386
- function buildPropertySchema(prop, classFile, ctx) {
2387
- const decorators = /* @__PURE__ */ new Map();
2388
- for (const d of prop.getDecorators()) decorators.set(d.getName(), d);
2389
- const has = (n) => decorators.has(n);
2390
- const dec = (n) => decorators.get(n);
2391
- const typeNode = prop.getTypeNode();
2392
- const typeText = typeNode?.getText() ?? "unknown";
2393
- const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
2394
- const comments = [];
2395
- const typeRefName = resolveTypeFactoryName2(dec("Type"));
2396
- if (has("ValidateNested") || typeRefName) {
2397
- const childName = typeRefName ?? singularClassName2(typeText);
2398
- if (childName) {
2399
- const childExpr = buildNestedReference2(childName, classFile, ctx);
2400
- const wrapArray = has("IsArray") || isArrayType;
2401
- let expr2 = wrapArray ? `z.array(${childExpr})` : childExpr;
2402
- expr2 = applyPresence2(expr2, decorators);
2403
- return expr2;
2404
- }
2405
- }
2406
- let base = baseFromType2(typeText, isArrayType, ctx, classFile);
2407
- const refinements = [];
2408
- if (has("IsString")) base = "z.string()";
2409
- if (has("IsBoolean")) base = "z.boolean()";
2410
- if (has("IsDate")) base = "z.coerce.date()";
2411
- if (has("IsNumber")) base = "z.number()";
2412
- if (has("IsInt")) base = "z.number().int()";
2413
- if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
2414
- if (has("Allow")) base = "z.unknown()";
2415
- if (has("IsEmail")) {
2416
- base = ensureStringBase(base);
2417
- refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
2418
- }
2419
- if (has("IsUrl")) {
2420
- base = ensureStringBase(base);
2421
- refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
2422
- }
2423
- if (has("IsUUID")) {
2424
- base = ensureStringBase(base);
2425
- refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
2426
- }
2427
- if (has("Matches")) {
2428
- const re = firstArgText2(dec("Matches"));
2429
- if (re) {
2430
- base = ensureStringBase(base);
2431
- refinements.push(`.regex(${re})`);
2248
+ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2249
+ if (import_ts_morph5.Node.isUnionTypeNode(typeNode)) {
2250
+ let nullable = false;
2251
+ const stringLits = [];
2252
+ const numberLits = [];
2253
+ const others = [];
2254
+ for (const member of typeNode.getTypeNodes()) {
2255
+ const kind = member.getKind();
2256
+ if (kind === import_ts_morph5.SyntaxKind.NullKeyword || kind === import_ts_morph5.SyntaxKind.UndefinedKeyword) {
2257
+ nullable = true;
2258
+ continue;
2259
+ }
2260
+ if (import_ts_morph5.Node.isLiteralTypeNode(member)) {
2261
+ const lit = member.getLiteral();
2262
+ if (import_ts_morph5.Node.isStringLiteral(lit)) {
2263
+ stringLits.push(lit.getLiteralValue());
2264
+ continue;
2265
+ }
2266
+ if (import_ts_morph5.Node.isNumericLiteral(lit)) {
2267
+ numberLits.push(lit.getText());
2268
+ continue;
2269
+ }
2270
+ if (lit.getKind() === import_ts_morph5.SyntaxKind.NullKeyword) {
2271
+ nullable = true;
2272
+ continue;
2273
+ }
2274
+ }
2275
+ others.push(member);
2276
+ }
2277
+ if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2278
+ return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2279
+ }
2280
+ if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2281
+ return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2282
+ }
2283
+ if (others.length === 1) {
2284
+ const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2285
+ return markNullable(inner, nullable || inner.nullable === true);
2432
2286
  }
2287
+ return markNullable({ kind: "unknown" }, nullable);
2433
2288
  }
2434
- if (has("MinLength")) {
2435
- const n = numericArg2(dec("MinLength"));
2436
- if (n !== null) refinements.push(`.min(${n})`);
2289
+ switch (typeNode.getKind()) {
2290
+ case import_ts_morph5.SyntaxKind.StringKeyword:
2291
+ return { kind: "string" };
2292
+ case import_ts_morph5.SyntaxKind.NumberKeyword:
2293
+ return { kind: "number" };
2294
+ case import_ts_morph5.SyntaxKind.BooleanKeyword:
2295
+ return { kind: "boolean" };
2296
+ case import_ts_morph5.SyntaxKind.AnyKeyword:
2297
+ case import_ts_morph5.SyntaxKind.UnknownKeyword:
2298
+ return { kind: "unknown" };
2299
+ default:
2300
+ break;
2437
2301
  }
2438
- if (has("MaxLength")) {
2439
- const n = numericArg2(dec("MaxLength"));
2440
- if (n !== null) refinements.push(`.max(${n})`);
2441
- }
2442
- if (has("Length")) {
2443
- const [min, max] = numericArgs2(dec("Length"));
2444
- if (min !== null) refinements.push(`.min(${min})`);
2445
- if (max !== null) refinements.push(`.max(${max})`);
2446
- }
2447
- if (has("Min")) {
2448
- const n = numericArg2(dec("Min"));
2449
- if (n !== null) refinements.push(`.min(${n})`);
2450
- }
2451
- if (has("Max")) {
2452
- const n = numericArg2(dec("Max"));
2453
- if (n !== null) refinements.push(`.max(${n})`);
2454
- }
2455
- if (has("IsPositive")) refinements.push(".positive()");
2456
- if (has("IsNegative")) refinements.push(".negative()");
2457
- if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
2458
- if (has("IsEnum")) {
2459
- const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
2460
- if (enumExpr) base = enumExpr;
2461
- }
2462
- if (has("IsIn")) {
2463
- const inExpr = inSchemaFromDecorator2(dec("IsIn"));
2464
- if (inExpr) base = inExpr;
2465
- }
2466
- for (const name of decorators.keys()) {
2467
- if (!KNOWN_DECORATORS2.has(name)) {
2468
- comments.push(`/* @${name}: not translatable to zod (server-only) */`);
2469
- if (!ctx.warnedDecorators.has(name)) {
2470
- ctx.warnedDecorators.add(name);
2471
- const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
2472
- ctx.warnings.push(msg);
2473
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2474
- }
2475
- }
2476
- }
2477
- let expr = base + refinements.join("");
2478
- if (isArrayType && !expr.startsWith("z.array(")) {
2479
- expr = `z.array(${expr})`;
2480
- }
2481
- expr = applyPresence2(expr, decorators);
2482
- if (comments.length > 0) {
2483
- expr = `${expr} ${comments.join(" ")}`;
2484
- }
2485
- return expr;
2486
- }
2487
- function applyPresence2(expr, decorators) {
2488
- if (decorators.has("IsDefined")) return expr;
2489
- if (decorators.has("IsOptional")) return `${expr}.optional()`;
2490
- return expr;
2491
- }
2492
- function baseFromType2(typeText, isArrayType, ctx, classFile) {
2493
- const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
2494
- switch (inner) {
2495
- case "string":
2496
- return "z.string()";
2497
- case "number":
2498
- return "z.number()";
2499
- case "boolean":
2500
- return "z.boolean()";
2501
- case "Date":
2502
- return "z.coerce.date()";
2503
- case "File":
2504
- case "Express.Multer.File":
2505
- return "z.instanceof(File)";
2506
- default:
2507
- return "z.unknown()";
2508
- }
2509
- }
2510
- function ensureStringBase(base) {
2511
- return isStringBase(base) ? base : "z.string()";
2512
- }
2513
- function isStringBase(base) {
2514
- return base.startsWith("z.string(");
2515
- }
2516
- function buildNestedReference2(className, fromFile, ctx) {
2517
- if (ctx.visiting.has(className) || ctx.depth >= 8) {
2518
- const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
2519
- ctx.emittedClasses.set(className, reserved);
2520
- ctx.recursiveSchemas.add(reserved);
2521
- if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
2522
- ctx.warnedDecorators.add(`recursive:${reserved}`);
2523
- const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
2524
- ctx.warnings.push(msg);
2525
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2526
- }
2527
- return `z.lazy(() => ${reserved})`;
2528
- }
2529
- const existing = ctx.emittedClasses.get(className);
2530
- if (existing) return existing;
2531
- const schemaName = aliasFor2(className, ctx);
2532
- const resolved = findType(className, fromFile, ctx.project);
2533
- if (!resolved || resolved.kind !== "class") {
2534
- return "z.object({}).passthrough()";
2535
- }
2536
- ctx.emittedClasses.set(className, schemaName);
2537
- ctx.visiting.add(className);
2538
- ctx.depth += 1;
2539
- const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
2540
- ctx.depth -= 1;
2541
- ctx.visiting.delete(className);
2542
- ctx.namedNestedSchemas.set(schemaName, childText);
2543
- return schemaName;
2544
- }
2545
- function aliasFor2(className, ctx) {
2546
- const baseName = `${className}Schema`;
2547
- let candidate = baseName;
2548
- let i = 1;
2549
- const used = new Set(ctx.namedNestedSchemas.keys());
2550
- for (const v of ctx.emittedClasses.values()) used.add(v);
2551
- while (used.has(candidate)) {
2552
- candidate = `${baseName}_${i}`;
2553
- i += 1;
2554
- }
2555
- return candidate;
2556
- }
2557
- function firstArg2(decorator) {
2558
- return decorator?.getArguments()[0];
2559
- }
2560
- function firstArgText2(decorator) {
2561
- const arg = firstArg2(decorator);
2562
- return arg ? arg.getText() : null;
2563
- }
2564
- function numericArg2(decorator) {
2565
- const arg = firstArg2(decorator);
2566
- if (arg && import_ts_morph6.Node.isNumericLiteral(arg)) return arg.getText();
2567
- return null;
2568
- }
2569
- function numericArgs2(decorator) {
2570
- const args = decorator?.getArguments() ?? [];
2571
- const num = (n) => n && import_ts_morph6.Node.isNumericLiteral(n) ? n.getText() : null;
2572
- return [num(args[0]), num(args[1])];
2573
- }
2574
- function messageArg(decorator) {
2575
- const args = decorator?.getArguments() ?? [];
2576
- for (const arg of args) {
2577
- if (import_ts_morph6.Node.isObjectLiteralExpression(arg)) {
2578
- for (const prop of arg.getProperties()) {
2579
- if (import_ts_morph6.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2580
- const init = prop.getInitializer();
2581
- if (init && import_ts_morph6.Node.isStringLiteral(init)) {
2582
- return `{ message: ${init.getText()} }`;
2583
- }
2584
- }
2585
- }
2586
- }
2587
- }
2588
- return "";
2589
- }
2590
- function resolveTypeFactoryName2(decorator) {
2591
- const arg = firstArg2(decorator);
2592
- if (!arg) return null;
2593
- if (import_ts_morph6.Node.isArrowFunction(arg)) {
2594
- const body = arg.getBody();
2595
- if (import_ts_morph6.Node.isIdentifier(body)) return body.getText();
2596
- }
2597
- return null;
2598
- }
2599
- function singularClassName2(typeText) {
2600
- const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
2601
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
2602
- }
2603
- function enumSchemaFromDecorator2(decorator, classFile, ctx) {
2604
- const arg = firstArg2(decorator);
2605
- if (!arg) return null;
2606
- if (import_ts_morph6.Node.isIdentifier(arg)) {
2607
- const name = arg.getText();
2608
- const resolved = findType(name, classFile, ctx.project);
2609
- if (resolved && resolved.kind === "enum") {
2610
- return `z.enum([${resolved.members.join(", ")}])`;
2611
- }
2612
- 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().`;
2613
- if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
2614
- ctx.warnedDecorators.add(`IsEnum:${name}`);
2615
- ctx.warnings.push(msg);
2616
- console.warn(`[nestjs-codegen/forms] ${msg}`);
2617
- }
2618
- return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
2619
- }
2620
- if (import_ts_morph6.Node.isObjectLiteralExpression(arg)) {
2621
- const values = [];
2622
- for (const p of arg.getProperties()) {
2623
- if (!import_ts_morph6.Node.isPropertyAssignment(p)) continue;
2624
- const init = p.getInitializer();
2625
- if (init && import_ts_morph6.Node.isStringLiteral(init)) values.push(init.getText());
2626
- }
2627
- if (values.length > 0) return `z.enum([${values.join(", ")}])`;
2628
- }
2629
- return null;
2630
- }
2631
- function inSchemaFromDecorator2(decorator) {
2632
- const arg = firstArg2(decorator);
2633
- if (arg && import_ts_morph6.Node.isArrayLiteralExpression(arg)) {
2634
- const elements = arg.getElements();
2635
- const allStrings = elements.every((e) => import_ts_morph6.Node.isStringLiteral(e));
2636
- if (allStrings && elements.length > 0) {
2637
- return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
2638
- }
2639
- if (elements.length > 0) {
2640
- return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
2641
- }
2642
- }
2643
- return null;
2644
- }
2645
-
2646
- // src/discovery/filter-for.ts
2647
- var import_ts_morph8 = require("ts-morph");
2648
-
2649
- // src/discovery/filter-field-types.ts
2650
- var import_ts_morph7 = require("ts-morph");
2651
-
2652
- // src/discovery/enum-resolution.ts
2653
- function resolveEnumValues(name, sourceFile, project) {
2654
- const resolved = findType(name, sourceFile, project);
2655
- if (!resolved || resolved.kind !== "enum") return null;
2656
- let numeric = true;
2657
- const values = resolved.members.map((m) => {
2658
- const parsed = JSON.parse(m);
2659
- if (typeof parsed === "string") numeric = false;
2660
- return String(parsed);
2661
- });
2662
- if (values.length === 0) return null;
2663
- return { values, numeric };
2664
- }
2665
-
2666
- // src/discovery/filter-field-types.ts
2667
- var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2668
- var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2669
- var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2670
- var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2671
- var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2672
- function classifyTypeKeyword(raw) {
2673
- const t = raw.toLowerCase();
2674
- if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2675
- if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2676
- if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2677
- if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2678
- if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2679
- return null;
2680
- }
2681
- function markNullable(r, nullable) {
2682
- return nullable ? { ...r, nullable: true } : r;
2683
- }
2684
- function classifyTypeNode(typeNode, sourceFile, project, opts) {
2685
- if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
2686
- let nullable = false;
2687
- const stringLits = [];
2688
- const numberLits = [];
2689
- const others = [];
2690
- for (const member of typeNode.getTypeNodes()) {
2691
- const kind = member.getKind();
2692
- if (kind === import_ts_morph7.SyntaxKind.NullKeyword || kind === import_ts_morph7.SyntaxKind.UndefinedKeyword) {
2693
- nullable = true;
2694
- continue;
2695
- }
2696
- if (import_ts_morph7.Node.isLiteralTypeNode(member)) {
2697
- const lit = member.getLiteral();
2698
- if (import_ts_morph7.Node.isStringLiteral(lit)) {
2699
- stringLits.push(lit.getLiteralValue());
2700
- continue;
2701
- }
2702
- if (import_ts_morph7.Node.isNumericLiteral(lit)) {
2703
- numberLits.push(lit.getText());
2704
- continue;
2705
- }
2706
- if (lit.getKind() === import_ts_morph7.SyntaxKind.NullKeyword) {
2707
- nullable = true;
2708
- continue;
2709
- }
2710
- }
2711
- others.push(member);
2712
- }
2713
- if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2714
- return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2715
- }
2716
- if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2717
- return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2718
- }
2719
- if (others.length === 1) {
2720
- const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2721
- return markNullable(inner, nullable || inner.nullable === true);
2722
- }
2723
- return markNullable({ kind: "unknown" }, nullable);
2724
- }
2725
- switch (typeNode.getKind()) {
2726
- case import_ts_morph7.SyntaxKind.StringKeyword:
2727
- return { kind: "string" };
2728
- case import_ts_morph7.SyntaxKind.NumberKeyword:
2729
- return { kind: "number" };
2730
- case import_ts_morph7.SyntaxKind.BooleanKeyword:
2731
- return { kind: "boolean" };
2732
- case import_ts_morph7.SyntaxKind.AnyKeyword:
2733
- case import_ts_morph7.SyntaxKind.UnknownKeyword:
2734
- return { kind: "unknown" };
2735
- default:
2736
- break;
2737
- }
2738
- if (import_ts_morph7.Node.isTypeReference(typeNode)) {
2302
+ if (import_ts_morph5.Node.isTypeReference(typeNode)) {
2739
2303
  const refName = typeNode.getTypeName().getText();
2740
2304
  if (refName === "Date") return { kind: "date" };
2741
2305
  if (refName === "Record" || refName === "Object") return { kind: "json" };
@@ -2748,25 +2312,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2748
2312
  if (typeRef) return { kind: "unknown", typeRef };
2749
2313
  return { kind: "unknown" };
2750
2314
  }
2751
- if (import_ts_morph7.Node.isTypeLiteral(typeNode)) return { kind: "json" };
2315
+ if (import_ts_morph5.Node.isTypeLiteral(typeNode)) return { kind: "json" };
2752
2316
  return { kind: "unknown" };
2753
2317
  }
2754
2318
  function enumFromDecoratorArgs(args, sourceFile, project) {
2755
2319
  for (const arg of args) {
2756
- if (import_ts_morph7.Node.isArrowFunction(arg)) {
2320
+ if (import_ts_morph5.Node.isArrowFunction(arg)) {
2757
2321
  const body = arg.getBody();
2758
- if (import_ts_morph7.Node.isIdentifier(body)) {
2322
+ if (import_ts_morph5.Node.isIdentifier(body)) {
2759
2323
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2760
2324
  if (en) return en;
2761
2325
  }
2762
2326
  }
2763
- if (import_ts_morph7.Node.isObjectLiteralExpression(arg)) {
2327
+ if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2764
2328
  const itemsProp = arg.getProperty("items");
2765
- if (itemsProp && import_ts_morph7.Node.isPropertyAssignment(itemsProp)) {
2329
+ if (itemsProp && import_ts_morph5.Node.isPropertyAssignment(itemsProp)) {
2766
2330
  const init = itemsProp.getInitializer();
2767
- if (init && import_ts_morph7.Node.isArrowFunction(init)) {
2331
+ if (init && import_ts_morph5.Node.isArrowFunction(init)) {
2768
2332
  const body = init.getBody();
2769
- if (import_ts_morph7.Node.isIdentifier(body)) {
2333
+ if (import_ts_morph5.Node.isIdentifier(body)) {
2770
2334
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2771
2335
  if (en) return en;
2772
2336
  }
@@ -2789,7 +2353,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2789
2353
  return { kind: "string" };
2790
2354
  }
2791
2355
  for (const arg of args) {
2792
- if (import_ts_morph7.Node.isStringLiteral(arg)) {
2356
+ if (import_ts_morph5.Node.isStringLiteral(arg)) {
2793
2357
  const raw = arg.getLiteralValue();
2794
2358
  const kind = classifyTypeKeyword(raw);
2795
2359
  if (kind) {
@@ -2797,11 +2361,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2797
2361
  return { kind };
2798
2362
  }
2799
2363
  }
2800
- if (import_ts_morph7.Node.isObjectLiteralExpression(arg)) {
2364
+ if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2801
2365
  const enumProp = arg.getProperty("enum");
2802
- if (enumProp && import_ts_morph7.Node.isPropertyAssignment(enumProp)) {
2366
+ if (enumProp && import_ts_morph5.Node.isPropertyAssignment(enumProp)) {
2803
2367
  const init = enumProp.getInitializer();
2804
- if (init && import_ts_morph7.Node.isIdentifier(init)) {
2368
+ if (init && import_ts_morph5.Node.isIdentifier(init)) {
2805
2369
  const en = resolveEnumValues(init.getText(), sourceFile, project);
2806
2370
  if (en) {
2807
2371
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -2810,9 +2374,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2810
2374
  }
2811
2375
  }
2812
2376
  const typeProp = arg.getProperty("type");
2813
- if (typeProp && import_ts_morph7.Node.isPropertyAssignment(typeProp)) {
2377
+ if (typeProp && import_ts_morph5.Node.isPropertyAssignment(typeProp)) {
2814
2378
  const init = typeProp.getInitializer();
2815
- if (init && import_ts_morph7.Node.isStringLiteral(init)) {
2379
+ if (init && import_ts_morph5.Node.isStringLiteral(init)) {
2816
2380
  const kind = classifyTypeKeyword(init.getLiteralValue());
2817
2381
  if (kind) return { kind };
2818
2382
  }
@@ -2848,7 +2412,7 @@ function toFilterFieldType(name, r) {
2848
2412
 
2849
2413
  // src/discovery/filter-for.ts
2850
2414
  function classifyFilterForHint(typeInit) {
2851
- if (import_ts_morph8.Node.isStringLiteral(typeInit)) {
2415
+ if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
2852
2416
  switch (typeInit.getLiteralValue()) {
2853
2417
  case "string":
2854
2418
  return { kind: "string" };
@@ -2862,10 +2426,10 @@ function classifyFilterForHint(typeInit) {
2862
2426
  return null;
2863
2427
  }
2864
2428
  }
2865
- if (import_ts_morph8.Node.isArrayLiteralExpression(typeInit)) {
2429
+ if (import_ts_morph6.Node.isArrayLiteralExpression(typeInit)) {
2866
2430
  const values = [];
2867
2431
  for (const el of typeInit.getElements()) {
2868
- if (!import_ts_morph8.Node.isStringLiteral(el)) return null;
2432
+ if (!import_ts_morph6.Node.isStringLiteral(el)) return null;
2869
2433
  values.push(el.getLiteralValue());
2870
2434
  }
2871
2435
  if (values.length === 0) return null;
@@ -2900,11 +2464,11 @@ function extractFilterForHints(classDecl, project) {
2900
2464
  if (!filterForDec) continue;
2901
2465
  const args = filterForDec.getArguments();
2902
2466
  const keyArg = args[0];
2903
- const inputKey = keyArg && import_ts_morph8.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2467
+ const inputKey = keyArg && import_ts_morph6.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2904
2468
  const optsArg = args[1];
2905
- if (optsArg && import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) {
2469
+ if (optsArg && import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) {
2906
2470
  const typeProp = optsArg.getProperty("type");
2907
- if (typeProp && import_ts_morph8.Node.isPropertyAssignment(typeProp)) {
2471
+ if (typeProp && import_ts_morph6.Node.isPropertyAssignment(typeProp)) {
2908
2472
  const typeInit = typeProp.getInitializer();
2909
2473
  if (typeInit) {
2910
2474
  const classified = classifyFilterForHint(typeInit);
@@ -2927,14 +2491,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
2927
2491
  const args = filterDecorator.getArguments();
2928
2492
  if (args.length === 0) continue;
2929
2493
  const filterClassArg = args[0];
2930
- if (!filterClassArg || !import_ts_morph8.Node.isIdentifier(filterClassArg)) continue;
2494
+ if (!filterClassArg || !import_ts_morph6.Node.isIdentifier(filterClassArg)) continue;
2931
2495
  let source = "query";
2932
2496
  const optionsArg = args[1];
2933
- if (optionsArg && import_ts_morph8.Node.isObjectLiteralExpression(optionsArg)) {
2497
+ if (optionsArg && import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) {
2934
2498
  const sourceProp = optionsArg.getProperty("source");
2935
- if (sourceProp && import_ts_morph8.Node.isPropertyAssignment(sourceProp)) {
2499
+ if (sourceProp && import_ts_morph6.Node.isPropertyAssignment(sourceProp)) {
2936
2500
  const init = sourceProp.getInitializer();
2937
- if (init && import_ts_morph8.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
2501
+ if (init && import_ts_morph6.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
2938
2502
  source = "body";
2939
2503
  }
2940
2504
  }
@@ -2997,22 +2561,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
2997
2561
  const args = dec.getArguments();
2998
2562
  if (args.length === 0) continue;
2999
2563
  const arg = args[0];
3000
- if (import_ts_morph8.Node.isObjectLiteralExpression(arg)) {
2564
+ if (import_ts_morph6.Node.isObjectLiteralExpression(arg)) {
3001
2565
  const entityProp = arg.getProperty("entity");
3002
- if (entityProp && import_ts_morph8.Node.isPropertyAssignment(entityProp)) {
2566
+ if (entityProp && import_ts_morph6.Node.isPropertyAssignment(entityProp)) {
3003
2567
  const init = entityProp.getInitializer();
3004
- if (init && import_ts_morph8.Node.isArrowFunction(init)) {
2568
+ if (init && import_ts_morph6.Node.isArrowFunction(init)) {
3005
2569
  const body = init.getBody();
3006
- if (import_ts_morph8.Node.isIdentifier(body)) {
2570
+ if (import_ts_morph6.Node.isIdentifier(body)) {
3007
2571
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3008
2572
  if (resolved?.kind === "class") return resolved.decl;
3009
2573
  }
3010
2574
  }
3011
2575
  }
3012
2576
  }
3013
- if (import_ts_morph8.Node.isArrowFunction(arg)) {
2577
+ if (import_ts_morph6.Node.isArrowFunction(arg)) {
3014
2578
  const body = arg.getBody();
3015
- if (import_ts_morph8.Node.isIdentifier(body)) {
2579
+ if (import_ts_morph6.Node.isIdentifier(body)) {
3016
2580
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3017
2581
  if (resolved?.kind === "class") return resolved.decl;
3018
2582
  }
@@ -3036,11 +2600,11 @@ function extractFilterableEntityFields(filterClass, project) {
3036
2600
  const args = filterableDecorator.getArguments();
3037
2601
  if (args.length === 0) return [];
3038
2602
  const optionsArg = args[0];
3039
- if (!import_ts_morph8.Node.isObjectLiteralExpression(optionsArg)) return [];
2603
+ if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
3040
2604
  const entityProp = optionsArg.getProperty("entity");
3041
- if (!entityProp || !import_ts_morph8.Node.isPropertyAssignment(entityProp)) return [];
2605
+ if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return [];
3042
2606
  const entityInit = entityProp.getInitializer();
3043
- if (!entityInit || !import_ts_morph8.Node.isIdentifier(entityInit)) return [];
2607
+ if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
3044
2608
  const entityName = entityInit.getText();
3045
2609
  const filterSourceFile = filterClass.getSourceFile();
3046
2610
  const resolvedEntity = findType(entityName, filterSourceFile, project);
@@ -3056,17 +2620,17 @@ function extractFilterableEntityFields(filterClass, project) {
3056
2620
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
3057
2621
  if (relationsDecorator) {
3058
2622
  const relArgs = relationsDecorator.getArguments();
3059
- if (relArgs.length > 0 && import_ts_morph8.Node.isObjectLiteralExpression(relArgs[0])) {
2623
+ if (relArgs.length > 0 && import_ts_morph6.Node.isObjectLiteralExpression(relArgs[0])) {
3060
2624
  for (const relProp of relArgs[0].getProperties()) {
3061
- if (!import_ts_morph8.Node.isPropertyAssignment(relProp)) continue;
2625
+ if (!import_ts_morph6.Node.isPropertyAssignment(relProp)) continue;
3062
2626
  const relInit = relProp.getInitializer();
3063
- if (!relInit || !import_ts_morph8.Node.isObjectLiteralExpression(relInit)) continue;
2627
+ if (!relInit || !import_ts_morph6.Node.isObjectLiteralExpression(relInit)) continue;
3064
2628
  const keysProp = relInit.getProperty("keys");
3065
- if (!keysProp || !import_ts_morph8.Node.isPropertyAssignment(keysProp)) continue;
2629
+ if (!keysProp || !import_ts_morph6.Node.isPropertyAssignment(keysProp)) continue;
3066
2630
  const keysInit = keysProp.getInitializer();
3067
- if (!keysInit || !import_ts_morph8.Node.isArrayLiteralExpression(keysInit)) continue;
2631
+ if (!keysInit || !import_ts_morph6.Node.isArrayLiteralExpression(keysInit)) continue;
3068
2632
  for (const el of keysInit.getElements()) {
3069
- if (import_ts_morph8.Node.isStringLiteral(el)) {
2633
+ if (import_ts_morph6.Node.isStringLiteral(el)) {
3070
2634
  fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
3071
2635
  }
3072
2636
  }
@@ -3076,267 +2640,65 @@ function extractFilterableEntityFields(filterClass, project) {
3076
2640
  return fields;
3077
2641
  }
3078
2642
 
3079
- // src/discovery/contracts-fast.ts
3080
- async function discoverContractsFast(opts) {
3081
- const { cwd, glob, tsconfig } = opts;
3082
- const tsconfigPath = tsconfig ? (0, import_node_path11.resolve)(tsconfig) : (0, import_node_path11.join)(cwd, "tsconfig.json");
3083
- let project;
3084
- try {
3085
- project = new import_ts_morph9.Project({
3086
- tsConfigFilePath: tsconfigPath,
3087
- skipAddingFilesFromTsConfig: true,
3088
- skipLoadingLibFiles: true,
3089
- skipFileDependencyResolution: true
3090
- });
3091
- } catch {
3092
- project = new import_ts_morph9.Project({
3093
- skipAddingFilesFromTsConfig: true,
3094
- skipLoadingLibFiles: true,
3095
- skipFileDependencyResolution: true,
3096
- compilerOptions: {
3097
- allowJs: true,
3098
- resolveJsonModule: false,
3099
- strict: false
3100
- }
3101
- });
2643
+ // src/discovery/dto-type-resolver.ts
2644
+ var WRAPPER_TYPES = {
2645
+ // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
2646
+ // wrappers around related entities; the wire shape is just the referenced
2647
+ // entity. Unwrap to the type argument.
2648
+ Ref: "unwrap",
2649
+ Reference: "unwrap",
2650
+ LoadedReference: "unwrap",
2651
+ IdentifiedReference: "unwrap",
2652
+ // MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
2653
+ Opt: "unwrap",
2654
+ Loaded: "unwrap",
2655
+ // Promise<T> — unwrap
2656
+ Promise: "unwrap",
2657
+ // MikroORM Collection<T> serializes as an array of T on the wire.
2658
+ Collection: "arrayOf",
2659
+ // Array<T> generic form
2660
+ Array: "arrayOf"
2661
+ };
2662
+ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2663
+ "Record",
2664
+ "Omit",
2665
+ "Pick",
2666
+ "Partial",
2667
+ "Required",
2668
+ "Readonly",
2669
+ "Map",
2670
+ "Set"
2671
+ ]);
2672
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2673
+ if (depth <= 0) return "unknown";
2674
+ if (import_ts_morph7.Node.isArrayTypeNode(typeNode)) {
2675
+ const elementType = typeNode.getElementTypeNode();
2676
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3102
2677
  }
3103
- const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3104
- for (const f of files) {
3105
- project.addSourceFileAtPath(f);
2678
+ if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
2679
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3106
2680
  }
3107
- const routes = [];
3108
- const prevCtx = setDiscoveryContext({
3109
- projectRoot: cwd,
3110
- tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3111
- });
3112
- try {
3113
- for (const sourceFile of project.getSourceFiles()) {
3114
- routes.push(...extractFromSourceFile(sourceFile, project));
3115
- }
3116
- } finally {
3117
- restoreDiscoveryContext(prevCtx);
2681
+ if (import_ts_morph7.Node.isIntersectionTypeNode(typeNode)) {
2682
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3118
2683
  }
3119
- return routes;
3120
- }
3121
- function zodAstToTs(node) {
3122
- if (!import_ts_morph9.Node.isCallExpression(node)) return "unknown";
3123
- const expr = node.getExpression();
3124
- if (import_ts_morph9.Node.isPropertyAccessExpression(expr)) {
3125
- const methodName = expr.getName();
3126
- const receiver = expr.getExpression();
3127
- if (methodName === "optional") {
3128
- return `${zodAstToTs(receiver)} | undefined`;
3129
- }
3130
- if (methodName === "nullable") {
3131
- return `${zodAstToTs(receiver)} | null`;
3132
- }
3133
- const args = node.getArguments();
3134
- switch (methodName) {
3135
- case "string":
3136
- return "string";
3137
- case "number":
3138
- return "number";
3139
- case "boolean":
3140
- return "boolean";
3141
- case "unknown":
3142
- return "unknown";
3143
- case "any":
3144
- return "unknown";
3145
- case "literal": {
3146
- const lit = args[0];
3147
- if (!lit) return "unknown";
3148
- if (import_ts_morph9.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
3149
- if (import_ts_morph9.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
3150
- if (lit.getKind() === import_ts_morph9.SyntaxKind.TrueKeyword) return "true";
3151
- if (lit.getKind() === import_ts_morph9.SyntaxKind.FalseKeyword) return "false";
3152
- return "unknown";
3153
- }
3154
- case "enum": {
3155
- const arrArg = args[0];
3156
- if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3157
- const members = arrArg.getElements().map(
3158
- (el) => import_ts_morph9.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
3159
- );
3160
- return members.join(" | ");
3161
- }
3162
- case "array": {
3163
- const inner = args[0];
3164
- if (!inner) return "unknown";
3165
- return `Array<${zodAstToTs(inner)}>`;
3166
- }
3167
- case "object": {
3168
- const objArg = args[0];
3169
- if (!objArg || !import_ts_morph9.Node.isObjectLiteralExpression(objArg)) return "unknown";
3170
- const lines = [];
3171
- for (const prop of objArg.getProperties()) {
3172
- if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
3173
- const key = prop.getName();
3174
- const valNode = prop.getInitializer();
3175
- if (!valNode) continue;
3176
- const tsType = zodAstToTs(valNode);
3177
- const isOpt = isOptionalChain(valNode);
3178
- lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
3179
- }
3180
- return `{ ${lines.join("; ")} }`;
3181
- }
3182
- case "union": {
3183
- const arrArg = args[0];
3184
- if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3185
- return arrArg.getElements().map(zodAstToTs).join(" | ");
3186
- }
3187
- case "record": {
3188
- const valArg = args.length === 1 ? args[0] : args[1];
3189
- if (!valArg) return "unknown";
3190
- return `Record<string, ${zodAstToTs(valArg)}>`;
3191
- }
3192
- case "tuple": {
3193
- const arrArg = args[0];
3194
- if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3195
- return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3196
- }
3197
- default:
3198
- return "unknown";
3199
- }
3200
- }
3201
- return "unknown";
3202
- }
3203
- function isOptionalChain(node) {
3204
- if (!import_ts_morph9.Node.isCallExpression(node)) return false;
3205
- const expr = node.getExpression();
3206
- return import_ts_morph9.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3207
- }
3208
- function decoratorStringArg(decoratorExpr) {
3209
- if (!decoratorExpr) return void 0;
3210
- if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3211
- if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
3212
- const first = decoratorExpr.getElements()[0];
3213
- if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
3214
- }
3215
- return void 0;
3216
- }
3217
- function parseDefineContractCall(callExpr) {
3218
- if (!import_ts_morph9.Node.isCallExpression(callExpr)) return null;
3219
- const callee = callExpr.getExpression();
3220
- const calleeName = import_ts_morph9.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph9.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
3221
- if (calleeName !== "defineContract") return null;
3222
- const args = callExpr.getArguments();
3223
- const optsArg = args[0];
3224
- if (!optsArg || !import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) return null;
3225
- let query = null;
3226
- let body = null;
3227
- let response = "unknown";
3228
- let bodyZodText = null;
3229
- let queryZodText = null;
3230
- for (const prop of optsArg.getProperties()) {
3231
- if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
3232
- const propName = prop.getName();
3233
- const val = prop.getInitializer();
3234
- if (!val) continue;
3235
- if (propName === "query") {
3236
- query = zodAstToTs(val);
3237
- queryZodText = val.getText();
3238
- } else if (propName === "body") {
3239
- body = zodAstToTs(val);
3240
- bodyZodText = val.getText();
3241
- } else if (propName === "response") {
3242
- response = zodAstToTs(val);
3243
- }
3244
- }
3245
- return { query, body, response, bodyZodText, queryZodText };
3246
- }
3247
- function deriveClassSegment(className) {
3248
- const noSuffix = className.replace(/Controller$/, "");
3249
- if (!noSuffix) {
3250
- throw new Error(
3251
- `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3252
- );
3253
- }
3254
- return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3255
- }
3256
- function resolveRouteName(className, methodName, classAs, methodAs) {
3257
- const classPortion = classAs ?? deriveClassSegment(className);
3258
- const methodPortion = methodAs ?? methodName;
3259
- return `${classPortion}.${methodPortion}`;
3260
- }
3261
- function joinPaths(prefix, suffix) {
3262
- if (!prefix && !suffix) return "/";
3263
- if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3264
- if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3265
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3266
- const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3267
- const combined = p + s;
3268
- return combined === "" ? "/" : combined;
3269
- }
3270
- function extractParams(path) {
3271
- const matches = path.matchAll(/:(\w+)/g);
3272
- return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3273
- }
3274
- function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3275
- if (depth <= 0) return "unknown";
3276
- if (import_ts_morph9.Node.isArrayTypeNode(typeNode)) {
3277
- const elementType = typeNode.getElementTypeNode();
3278
- return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3279
- }
3280
- if (import_ts_morph9.Node.isUnionTypeNode(typeNode)) {
3281
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3282
- }
3283
- if (import_ts_morph9.Node.isIntersectionTypeNode(typeNode)) {
3284
- return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3285
- }
3286
- if (import_ts_morph9.Node.isParenthesizedTypeNode(typeNode)) {
2684
+ if (import_ts_morph7.Node.isParenthesizedTypeNode(typeNode)) {
3287
2685
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3288
2686
  }
3289
- if (import_ts_morph9.Node.isTypeReference(typeNode)) {
2687
+ if (import_ts_morph7.Node.isTypeReference(typeNode)) {
3290
2688
  const typeName = typeNode.getTypeName();
3291
- const name = import_ts_morph9.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2689
+ const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3292
2690
  if (name === "string" || name === "number" || name === "boolean") return name;
3293
2691
  if (name === "Date") return "string";
3294
2692
  if (name === "unknown" || name === "any" || name === "void") return "unknown";
3295
2693
  if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
3296
2694
  return "unknown";
3297
- if (name === "Ref" || name === "Reference" || name === "LoadedReference" || name === "IdentifiedReference") {
3298
- const typeArgs = typeNode.getTypeArguments();
3299
- const firstTypeArg = typeArgs[0];
3300
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3301
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3302
- }
3303
- return "unknown";
2695
+ const wrapperMode = WRAPPER_TYPES[name];
2696
+ if (wrapperMode) {
2697
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3304
2698
  }
3305
- if (name === "Collection") {
3306
- const typeArgs = typeNode.getTypeArguments();
3307
- const firstTypeArg = typeArgs[0];
3308
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3309
- return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
3310
- }
3311
- return "Array<unknown>";
3312
- }
3313
- if (name === "Opt" || name === "Loaded") {
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
- if (name === "Array") {
3322
- const typeArgs = typeNode.getTypeArguments();
3323
- const firstTypeArg = typeArgs[0];
3324
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3325
- return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
3326
- }
3327
- return "Array<unknown>";
3328
- }
3329
- if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
2699
+ if (PASSTHROUGH_UTILITY.has(name)) {
3330
2700
  return typeNode.getText();
3331
2701
  }
3332
- if (name === "Promise") {
3333
- const typeArgs = typeNode.getTypeArguments();
3334
- const firstTypeArg = typeArgs[0];
3335
- if (typeArgs.length > 0 && firstTypeArg !== void 0) {
3336
- return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
3337
- }
3338
- return "unknown";
3339
- }
3340
2702
  const resolved = findType(name, sourceFile, project);
3341
2703
  if (resolved) {
3342
2704
  return expandTypeDecl(resolved, project, depth - 1);
@@ -3345,13 +2707,22 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3345
2707
  return "unknown";
3346
2708
  }
3347
2709
  const kind = typeNode.getKind();
3348
- if (kind === import_ts_morph9.SyntaxKind.StringKeyword) return "string";
3349
- if (kind === import_ts_morph9.SyntaxKind.NumberKeyword) return "number";
3350
- if (kind === import_ts_morph9.SyntaxKind.BooleanKeyword) return "boolean";
3351
- if (kind === import_ts_morph9.SyntaxKind.UnknownKeyword) return "unknown";
3352
- if (kind === import_ts_morph9.SyntaxKind.AnyKeyword) return "unknown";
2710
+ if (kind === import_ts_morph7.SyntaxKind.StringKeyword) return "string";
2711
+ if (kind === import_ts_morph7.SyntaxKind.NumberKeyword) return "number";
2712
+ if (kind === import_ts_morph7.SyntaxKind.BooleanKeyword) return "boolean";
2713
+ if (kind === import_ts_morph7.SyntaxKind.UnknownKeyword) return "unknown";
2714
+ if (kind === import_ts_morph7.SyntaxKind.AnyKeyword) return "unknown";
3353
2715
  return typeNode.getText();
3354
2716
  }
2717
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
2718
+ const typeArgs = typeNode.getTypeArguments();
2719
+ const firstTypeArg = typeArgs[0];
2720
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2721
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
2722
+ return mode === "arrayOf" ? `Array<${inner}>` : inner;
2723
+ }
2724
+ return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2725
+ }
3355
2726
  function expandTypeDecl(result, project, depth) {
3356
2727
  if (depth < 0) return "unknown";
3357
2728
  switch (result.kind) {
@@ -3417,7 +2788,7 @@ function extractParamsType(method, sourceFile, project) {
3417
2788
  const paramArgs = paramDecorator.getArguments();
3418
2789
  if (paramArgs.length === 0) continue;
3419
2790
  const nameArg = paramArgs[0];
3420
- if (!import_ts_morph9.Node.isStringLiteral(nameArg)) continue;
2791
+ if (!import_ts_morph7.Node.isStringLiteral(nameArg)) continue;
3421
2792
  const paramName = nameArg.getLiteralValue();
3422
2793
  const typeNode = param.getTypeNode();
3423
2794
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -3430,13 +2801,13 @@ function extractResponseType(method, sourceFile, project) {
3430
2801
  if (apiResponseDecorator) {
3431
2802
  const args = apiResponseDecorator.getArguments();
3432
2803
  const optsArg = args[0];
3433
- if (optsArg && import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) {
2804
+ if (optsArg && import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) {
3434
2805
  for (const prop of optsArg.getProperties()) {
3435
- if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
2806
+ if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
3436
2807
  if (prop.getName() !== "type") continue;
3437
2808
  const val = prop.getInitializer();
3438
2809
  if (!val) continue;
3439
- if (import_ts_morph9.Node.isArrayLiteralExpression(val)) {
2810
+ if (import_ts_morph7.Node.isArrayLiteralExpression(val)) {
3440
2811
  const elements = val.getElements();
3441
2812
  const firstEl = elements[0];
3442
2813
  if (elements.length > 0 && firstEl !== void 0) {
@@ -3456,7 +2827,7 @@ function extractResponseType(method, sourceFile, project) {
3456
2827
  return "unknown";
3457
2828
  }
3458
2829
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
3459
- if (!import_ts_morph9.Node.isIdentifier(node)) return "unknown";
2830
+ if (!import_ts_morph7.Node.isIdentifier(node)) return "unknown";
3460
2831
  const name = node.getText();
3461
2832
  const resolved = findType(name, sourceFile, project);
3462
2833
  if (resolved) {
@@ -3503,11 +2874,11 @@ function extractDtoContract(method, sourceFile, project) {
3503
2874
  if (apiResp) {
3504
2875
  const args = apiResp.getArguments();
3505
2876
  const optsArg = args[0];
3506
- if (optsArg && import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) {
2877
+ if (optsArg && import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) {
3507
2878
  for (const prop of optsArg.getProperties()) {
3508
- if (import_ts_morph9.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
2879
+ if (import_ts_morph7.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
3509
2880
  const val = prop.getInitializer();
3510
- if (val && import_ts_morph9.Node.isIdentifier(val)) {
2881
+ if (val && import_ts_morph7.Node.isIdentifier(val)) {
3511
2882
  const name = val.getText();
3512
2883
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3513
2884
  if (localDecl?.isExported()) {
@@ -3524,60 +2895,243 @@ function extractDtoContract(method, sourceFile, project) {
3524
2895
  }
3525
2896
  }
3526
2897
  }
3527
- let bodyZodText = null;
3528
- let queryZodText = null;
3529
2898
  let bodySchema = null;
3530
2899
  let querySchema = null;
3531
- const formNested = {};
3532
2900
  const formWarnings = [];
3533
2901
  const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
3534
2902
  if (bodyClass) {
3535
- const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
3536
- bodyZodText = result.schemaText;
3537
- for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
3538
- formWarnings.push(...result.warnings);
3539
2903
  bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2904
+ formWarnings.push(...bodySchema.warnings);
2905
+ }
2906
+ const queryClass = resolveParamClass(method, "Query", sourceFile, project);
2907
+ if (queryClass) {
2908
+ querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
2909
+ formWarnings.push(...querySchema.warnings);
2910
+ }
2911
+ return {
2912
+ query,
2913
+ body,
2914
+ response,
2915
+ params: paramsType,
2916
+ queryRef,
2917
+ bodyRef,
2918
+ responseRef,
2919
+ filterFields: filterInfo?.fieldNames ?? null,
2920
+ filterFieldTypes: filterInfo?.fieldTypes ?? null,
2921
+ filterSource: filterInfo?.source ?? null,
2922
+ formWarnings,
2923
+ bodySchema,
2924
+ querySchema
2925
+ };
2926
+ }
2927
+ function resolveParamClass(method, decoratorName, sourceFile, project) {
2928
+ for (const param of method.getParameters()) {
2929
+ if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
2930
+ const typeNode = param.getTypeNode();
2931
+ if (!typeNode) continue;
2932
+ const text = typeNode.getText().replace(/\[\]$/, "");
2933
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
2934
+ const resolved = findType(text, sourceFile, project);
2935
+ if (resolved && resolved.kind === "class") {
2936
+ return { decl: resolved.decl, file: resolved.file };
2937
+ }
2938
+ }
2939
+ return null;
2940
+ }
2941
+
2942
+ // src/discovery/zod-ast-to-ts.ts
2943
+ var import_ts_morph8 = require("ts-morph");
2944
+ function zodAstToTs(node) {
2945
+ if (!import_ts_morph8.Node.isCallExpression(node)) return "unknown";
2946
+ const expr = node.getExpression();
2947
+ if (import_ts_morph8.Node.isPropertyAccessExpression(expr)) {
2948
+ const methodName = expr.getName();
2949
+ const receiver = expr.getExpression();
2950
+ if (methodName === "optional") {
2951
+ return `${zodAstToTs(receiver)} | undefined`;
2952
+ }
2953
+ if (methodName === "nullable") {
2954
+ return `${zodAstToTs(receiver)} | null`;
2955
+ }
2956
+ const args = node.getArguments();
2957
+ switch (methodName) {
2958
+ case "string":
2959
+ return "string";
2960
+ case "number":
2961
+ return "number";
2962
+ case "boolean":
2963
+ return "boolean";
2964
+ case "unknown":
2965
+ return "unknown";
2966
+ case "any":
2967
+ return "unknown";
2968
+ case "literal": {
2969
+ const lit = args[0];
2970
+ if (!lit) return "unknown";
2971
+ if (import_ts_morph8.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2972
+ if (import_ts_morph8.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2973
+ if (lit.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword) return "true";
2974
+ if (lit.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) return "false";
2975
+ return "unknown";
2976
+ }
2977
+ case "enum": {
2978
+ const arrArg = args[0];
2979
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
2980
+ const members = arrArg.getElements().map(
2981
+ (el) => import_ts_morph8.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2982
+ );
2983
+ return members.join(" | ");
2984
+ }
2985
+ case "array": {
2986
+ const inner = args[0];
2987
+ if (!inner) return "unknown";
2988
+ return `Array<${zodAstToTs(inner)}>`;
2989
+ }
2990
+ case "object": {
2991
+ const objArg = args[0];
2992
+ if (!objArg || !import_ts_morph8.Node.isObjectLiteralExpression(objArg)) return "unknown";
2993
+ const lines = [];
2994
+ for (const prop of objArg.getProperties()) {
2995
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
2996
+ const key = prop.getName();
2997
+ const valNode = prop.getInitializer();
2998
+ if (!valNode) continue;
2999
+ const tsType = zodAstToTs(valNode);
3000
+ const isOpt = isOptionalChain(valNode);
3001
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
3002
+ }
3003
+ return `{ ${lines.join("; ")} }`;
3004
+ }
3005
+ case "union": {
3006
+ const arrArg = args[0];
3007
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3008
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
3009
+ }
3010
+ case "record": {
3011
+ const valArg = args.length === 1 ? args[0] : args[1];
3012
+ if (!valArg) return "unknown";
3013
+ return `Record<string, ${zodAstToTs(valArg)}>`;
3014
+ }
3015
+ case "tuple": {
3016
+ const arrArg = args[0];
3017
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3018
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3019
+ }
3020
+ default:
3021
+ return "unknown";
3022
+ }
3023
+ }
3024
+ return "unknown";
3025
+ }
3026
+ function isOptionalChain(node) {
3027
+ if (!import_ts_morph8.Node.isCallExpression(node)) return false;
3028
+ const expr = node.getExpression();
3029
+ return import_ts_morph8.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3030
+ }
3031
+ function parseDefineContractCall(callExpr) {
3032
+ if (!import_ts_morph8.Node.isCallExpression(callExpr)) return null;
3033
+ const callee = callExpr.getExpression();
3034
+ const calleeName = import_ts_morph8.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph8.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
3035
+ if (calleeName !== "defineContract") return null;
3036
+ const args = callExpr.getArguments();
3037
+ const optsArg = args[0];
3038
+ if (!optsArg || !import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) return null;
3039
+ let query = null;
3040
+ let body = null;
3041
+ let response = "unknown";
3042
+ let bodyZodText = null;
3043
+ let queryZodText = null;
3044
+ for (const prop of optsArg.getProperties()) {
3045
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
3046
+ const propName = prop.getName();
3047
+ const val = prop.getInitializer();
3048
+ if (!val) continue;
3049
+ if (propName === "query") {
3050
+ query = zodAstToTs(val);
3051
+ queryZodText = val.getText();
3052
+ } else if (propName === "body") {
3053
+ body = zodAstToTs(val);
3054
+ bodyZodText = val.getText();
3055
+ } else if (propName === "response") {
3056
+ response = zodAstToTs(val);
3057
+ }
3058
+ }
3059
+ return { query, body, response, bodyZodText, queryZodText };
3060
+ }
3061
+
3062
+ // src/discovery/contracts-fast.ts
3063
+ async function discoverContractsFast(opts) {
3064
+ const { cwd, glob, tsconfig } = opts;
3065
+ const tsconfigPath = tsconfig ? (0, import_node_path12.resolve)(tsconfig) : (0, import_node_path12.join)(cwd, "tsconfig.json");
3066
+ let project;
3067
+ try {
3068
+ project = new import_ts_morph9.Project({
3069
+ tsConfigFilePath: tsconfigPath,
3070
+ skipAddingFilesFromTsConfig: true,
3071
+ skipLoadingLibFiles: true,
3072
+ skipFileDependencyResolution: true
3073
+ });
3074
+ } catch {
3075
+ project = new import_ts_morph9.Project({
3076
+ skipAddingFilesFromTsConfig: true,
3077
+ skipLoadingLibFiles: true,
3078
+ skipFileDependencyResolution: true,
3079
+ compilerOptions: {
3080
+ allowJs: true,
3081
+ resolveJsonModule: false,
3082
+ strict: false
3083
+ }
3084
+ });
3085
+ }
3086
+ const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3087
+ for (const f of files) {
3088
+ project.addSourceFileAtPath(f);
3540
3089
  }
3541
- const queryClass = resolveParamClass(method, "Query", sourceFile, project);
3542
- if (queryClass) {
3543
- const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
3544
- queryZodText = result.schemaText;
3545
- for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
3546
- formWarnings.push(...result.warnings);
3547
- querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
3090
+ const routes = [];
3091
+ setDiscoveryContext(project, {
3092
+ projectRoot: cwd,
3093
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3094
+ });
3095
+ for (const sourceFile of project.getSourceFiles()) {
3096
+ routes.push(...extractFromSourceFile(sourceFile, project));
3548
3097
  }
3549
- return {
3550
- query,
3551
- body,
3552
- response,
3553
- params: paramsType,
3554
- queryRef,
3555
- bodyRef,
3556
- responseRef,
3557
- filterFields: filterInfo?.fieldNames ?? null,
3558
- filterFieldTypes: filterInfo?.fieldTypes ?? null,
3559
- filterSource: filterInfo?.source ?? null,
3560
- bodyZodText,
3561
- queryZodText,
3562
- formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
3563
- formWarnings,
3564
- bodySchema,
3565
- querySchema
3566
- };
3098
+ return routes;
3567
3099
  }
3568
- function resolveParamClass(method, decoratorName, sourceFile, project) {
3569
- for (const param of method.getParameters()) {
3570
- if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
3571
- const typeNode = param.getTypeNode();
3572
- if (!typeNode) continue;
3573
- const text = typeNode.getText().replace(/\[\]$/, "");
3574
- if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
3575
- const resolved = findType(text, sourceFile, project);
3576
- if (resolved && resolved.kind === "class") {
3577
- return { decl: resolved.decl, file: resolved.file };
3578
- }
3100
+ function decoratorStringArg(decoratorExpr) {
3101
+ if (!decoratorExpr) return void 0;
3102
+ if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3103
+ if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
3104
+ const first = decoratorExpr.getElements()[0];
3105
+ if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
3579
3106
  }
3580
- return null;
3107
+ return void 0;
3108
+ }
3109
+ function deriveClassSegment(className) {
3110
+ const noSuffix = className.replace(/Controller$/, "");
3111
+ if (!noSuffix) {
3112
+ throw new Error(
3113
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3114
+ );
3115
+ }
3116
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3117
+ }
3118
+ function resolveRouteName(className, methodName, classAs, methodAs) {
3119
+ const classPortion = classAs ?? deriveClassSegment(className);
3120
+ const methodPortion = methodAs ?? methodName;
3121
+ return `${classPortion}.${methodPortion}`;
3122
+ }
3123
+ function joinPaths(prefix, suffix) {
3124
+ if (!prefix && !suffix) return "/";
3125
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3126
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3127
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3128
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3129
+ const combined = p + s;
3130
+ return combined === "" ? "/" : combined;
3131
+ }
3132
+ function extractParams(path) {
3133
+ const matches = path.matchAll(/:(\w+)/g);
3134
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3581
3135
  }
3582
3136
  var HTTP_METHOD_DECORATORS = {
3583
3137
  Get: "GET",
@@ -3589,176 +3143,186 @@ var HTTP_METHOD_DECORATORS = {
3589
3143
  Head: "HEAD",
3590
3144
  All: "ALL"
3591
3145
  };
3146
+ function resolveVerb(method) {
3147
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3148
+ const httpDecorator = method.getDecorator(decoratorName);
3149
+ if (httpDecorator) {
3150
+ const httpArgs = httpDecorator.getArguments();
3151
+ const pathArg = httpArgs[0];
3152
+ return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3153
+ }
3154
+ }
3155
+ return null;
3156
+ }
3157
+ function readAsDecorator(node, label) {
3158
+ const asDecorator = node.getDecorator("As");
3159
+ if (!asDecorator) return void 0;
3160
+ const asName = decoratorStringArg(asDecorator.getArguments()[0]);
3161
+ if (!asName) {
3162
+ throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
3163
+ }
3164
+ return asName;
3165
+ }
3166
+ function buildRoute(args) {
3167
+ const {
3168
+ className,
3169
+ methodName,
3170
+ resolvedMethod,
3171
+ combinedPath,
3172
+ classAs,
3173
+ methodAs,
3174
+ sourceFile,
3175
+ seenNames,
3176
+ contractSource
3177
+ } = args;
3178
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3179
+ const qualifiedRef = `${className}.${methodName}`;
3180
+ const existing = seenNames.get(routeName);
3181
+ if (existing !== void 0) {
3182
+ throw new Error(
3183
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3184
+ );
3185
+ }
3186
+ seenNames.set(routeName, qualifiedRef);
3187
+ return {
3188
+ method: resolvedMethod,
3189
+ path: combinedPath,
3190
+ name: routeName,
3191
+ params: extractParams(combinedPath),
3192
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3193
+ contract: { contractSource }
3194
+ };
3195
+ }
3196
+ function extractContractRoute(args) {
3197
+ const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
3198
+ const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3199
+ if (!firstDecoratorArg) return null;
3200
+ let contractDef = null;
3201
+ let bodyZodRef = null;
3202
+ let queryZodRef = null;
3203
+ if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
3204
+ contractDef = parseDefineContractCall(firstDecoratorArg);
3205
+ } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
3206
+ const identName = firstDecoratorArg.getText();
3207
+ const varDecl = sourceFile.getVariableDeclaration(identName);
3208
+ if (!varDecl) {
3209
+ console.warn(
3210
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3211
+ );
3212
+ return null;
3213
+ }
3214
+ const initializer = varDecl.getInitializer();
3215
+ if (!initializer) return null;
3216
+ contractDef = parseDefineContractCall(initializer);
3217
+ if (contractDef && varDecl.isExported()) {
3218
+ const filePath = sourceFile.getFilePath();
3219
+ if (contractDef.body !== null) {
3220
+ bodyZodRef = { name: `${identName}.body`, filePath };
3221
+ }
3222
+ if (contractDef.query !== null) {
3223
+ queryZodRef = { name: `${identName}.query`, filePath };
3224
+ }
3225
+ }
3226
+ } else {
3227
+ console.warn(
3228
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3229
+ );
3230
+ return null;
3231
+ }
3232
+ if (!contractDef) return null;
3233
+ if (!verb) return null;
3234
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
3235
+ const methodName = method.getName();
3236
+ const classAs = readAsDecorator(cls, `class ${className}`);
3237
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3238
+ return buildRoute({
3239
+ className,
3240
+ methodName,
3241
+ resolvedMethod: verb.httpMethod,
3242
+ combinedPath: resolvedPath,
3243
+ classAs,
3244
+ methodAs,
3245
+ sourceFile,
3246
+ seenNames,
3247
+ contractSource: {
3248
+ query: contractDef.query,
3249
+ body: contractDef.body,
3250
+ response: contractDef.response,
3251
+ // Path A: capture both the importable ref and the raw text. The emitter
3252
+ // prefers inlining the text (client-safe — re-exporting from a controller
3253
+ // would drag server-only deps into the client bundle).
3254
+ bodyZodRef,
3255
+ bodyZodText: contractDef.bodyZodText,
3256
+ queryZodRef,
3257
+ queryZodText: contractDef.queryZodText
3258
+ }
3259
+ });
3260
+ }
3261
+ function extractDtoRoute(args) {
3262
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
3263
+ if (!verb) return null;
3264
+ const combined = joinPaths(prefix, verb.handlerPath);
3265
+ const methodName = method.getName();
3266
+ const classAs = readAsDecorator(cls, `class ${className}`);
3267
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3268
+ const dtoContract = extractDtoContract(method, sourceFile, project);
3269
+ return buildRoute({
3270
+ className,
3271
+ methodName,
3272
+ resolvedMethod: verb.httpMethod,
3273
+ combinedPath: combined,
3274
+ classAs,
3275
+ methodAs,
3276
+ sourceFile,
3277
+ seenNames,
3278
+ contractSource: {
3279
+ query: dtoContract?.query ?? null,
3280
+ body: dtoContract?.body ?? null,
3281
+ response: dtoContract?.response ?? "unknown",
3282
+ queryRef: dtoContract?.queryRef ?? null,
3283
+ bodyRef: dtoContract?.bodyRef ?? null,
3284
+ responseRef: dtoContract?.responseRef ?? null,
3285
+ filterFields: dtoContract?.filterFields ?? null,
3286
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3287
+ filterSource: dtoContract?.filterSource ?? null,
3288
+ formWarnings: dtoContract?.formWarnings ?? [],
3289
+ bodySchema: dtoContract?.bodySchema ?? null,
3290
+ querySchema: dtoContract?.querySchema ?? null
3291
+ }
3292
+ });
3293
+ }
3592
3294
  function extractFromSourceFile(sourceFile, project) {
3593
3295
  const routes = [];
3594
3296
  const seenNames = /* @__PURE__ */ new Map();
3595
- const classes = sourceFile.getClasses();
3596
- for (const cls of classes) {
3297
+ for (const cls of sourceFile.getClasses()) {
3597
3298
  const controllerDecorator = cls.getDecorator("Controller");
3598
3299
  if (!controllerDecorator) continue;
3599
- const controllerArgs = controllerDecorator.getArguments();
3600
- const firstArg3 = controllerArgs[0];
3601
- const prefix = decoratorStringArg(firstArg3) ?? "";
3300
+ const firstArg2 = controllerDecorator.getArguments()[0];
3301
+ const prefix = decoratorStringArg(firstArg2) ?? "";
3602
3302
  const className = cls.getName() ?? "Unknown";
3603
3303
  for (const method of cls.getMethods()) {
3604
- let httpMethod;
3605
- let handlerPath = "";
3606
- for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3607
- const httpDecorator = method.getDecorator(decoratorName);
3608
- if (httpDecorator) {
3609
- httpMethod = verb;
3610
- const httpArgs = httpDecorator.getArguments();
3611
- const pathArg = httpArgs[0];
3612
- handlerPath = decoratorStringArg(pathArg) ?? "";
3613
- break;
3614
- }
3615
- }
3304
+ const verb = resolveVerb(method);
3616
3305
  const applyContractDecorator = method.getDecorator("ApplyContract");
3617
- if (applyContractDecorator) {
3618
- const decoratorArgs = applyContractDecorator.getArguments();
3619
- const firstDecoratorArg = decoratorArgs[0];
3620
- if (!firstDecoratorArg) continue;
3621
- let contractDef = null;
3622
- let bodyZodRef = null;
3623
- let queryZodRef = null;
3624
- if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
3625
- contractDef = parseDefineContractCall(firstDecoratorArg);
3626
- } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
3627
- const identName = firstDecoratorArg.getText();
3628
- const varDecl = sourceFile.getVariableDeclaration(identName);
3629
- if (!varDecl) {
3630
- console.warn(
3631
- `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3632
- );
3633
- continue;
3634
- }
3635
- const initializer = varDecl.getInitializer();
3636
- if (!initializer) continue;
3637
- contractDef = parseDefineContractCall(initializer);
3638
- if (contractDef && varDecl.isExported()) {
3639
- const filePath = sourceFile.getFilePath();
3640
- if (contractDef.body !== null) {
3641
- bodyZodRef = { name: `${identName}.body`, filePath };
3642
- }
3643
- if (contractDef.query !== null) {
3644
- queryZodRef = { name: `${identName}.query`, filePath };
3645
- }
3646
- }
3647
- } else {
3648
- console.warn(
3649
- `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3650
- );
3651
- continue;
3652
- }
3653
- if (!contractDef) continue;
3654
- if (!httpMethod) continue;
3655
- const resolvedMethod = httpMethod;
3656
- const resolvedPath = joinPaths(prefix, handlerPath);
3657
- const combined = resolvedPath;
3658
- const params = extractParams(combined);
3659
- const methodName = method.getName();
3660
- const classAsDecorator = cls.getDecorator("As");
3661
- let classAs;
3662
- if (classAsDecorator) {
3663
- const classAsArgs = classAsDecorator.getArguments();
3664
- const classAsName = decoratorStringArg(classAsArgs[0]);
3665
- if (!classAsName) {
3666
- throw new Error(
3667
- `@As decorator on class ${className} must have a non-empty string argument.`
3668
- );
3669
- }
3670
- classAs = classAsName;
3671
- }
3672
- const methodAsDecorator = method.getDecorator("As");
3673
- let methodAs;
3674
- if (methodAsDecorator) {
3675
- const methodAsArgs = methodAsDecorator.getArguments();
3676
- const methodAsName = decoratorStringArg(methodAsArgs[0]);
3677
- if (!methodAsName) {
3678
- throw new Error(
3679
- `@As decorator on ${className}.${methodName} must have a non-empty string argument.`
3680
- );
3681
- }
3682
- methodAs = methodAsName;
3683
- }
3684
- const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3685
- const qualifiedRef = `${className}.${methodName}`;
3686
- const existing = seenNames.get(routeName);
3687
- if (existing !== void 0) {
3688
- throw new Error(
3689
- `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3690
- );
3691
- }
3692
- seenNames.set(routeName, qualifiedRef);
3693
- routes.push({
3694
- method: resolvedMethod,
3695
- path: combined,
3696
- name: routeName,
3697
- params,
3698
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3699
- contract: {
3700
- contractSource: {
3701
- query: contractDef.query,
3702
- body: contractDef.body,
3703
- response: contractDef.response,
3704
- // Path A: capture both the importable ref and the raw text. The
3705
- // emitter prefers inlining the text (client-safe — re-exporting from
3706
- // a controller would drag server-only deps into the client bundle).
3707
- bodyZodRef,
3708
- bodyZodText: contractDef.bodyZodText,
3709
- queryZodRef,
3710
- queryZodText: contractDef.queryZodText
3711
- }
3712
- }
3713
- });
3714
- } else {
3715
- if (!httpMethod) continue;
3716
- const combined = joinPaths(prefix, handlerPath);
3717
- const params = extractParams(combined);
3718
- const methodName = method.getName();
3719
- const classAsDecorator = cls.getDecorator("As");
3720
- let classAs;
3721
- if (classAsDecorator) {
3722
- const classAsArgs = classAsDecorator.getArguments();
3723
- const classAsName = decoratorStringArg(classAsArgs[0]);
3724
- if (classAsName) classAs = classAsName;
3725
- }
3726
- const methodAsDecorator = method.getDecorator("As");
3727
- let methodAs;
3728
- if (methodAsDecorator) {
3729
- const methodAsArgs = methodAsDecorator.getArguments();
3730
- const methodAsName = decoratorStringArg(methodAsArgs[0]);
3731
- if (methodAsName) methodAs = methodAsName;
3732
- }
3733
- const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3734
- const dtoContract = extractDtoContract(method, sourceFile, project);
3735
- routes.push({
3736
- method: httpMethod,
3737
- path: combined,
3738
- name: routeName,
3739
- params,
3740
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3741
- contract: {
3742
- contractSource: {
3743
- query: dtoContract?.query ?? null,
3744
- body: dtoContract?.body ?? null,
3745
- response: dtoContract?.response ?? "unknown",
3746
- queryRef: dtoContract?.queryRef ?? null,
3747
- bodyRef: dtoContract?.bodyRef ?? null,
3748
- responseRef: dtoContract?.responseRef ?? null,
3749
- filterFields: dtoContract?.filterFields ?? null,
3750
- filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3751
- filterSource: dtoContract?.filterSource ?? null,
3752
- bodyZodText: dtoContract?.bodyZodText ?? null,
3753
- queryZodText: dtoContract?.queryZodText ?? null,
3754
- formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
3755
- formWarnings: dtoContract?.formWarnings ?? [],
3756
- bodySchema: dtoContract?.bodySchema ?? null,
3757
- querySchema: dtoContract?.querySchema ?? null
3758
- }
3759
- }
3760
- });
3761
- }
3306
+ const route = applyContractDecorator ? extractContractRoute({
3307
+ cls,
3308
+ method,
3309
+ applyContractDecorator,
3310
+ verb,
3311
+ prefix,
3312
+ className,
3313
+ sourceFile,
3314
+ seenNames
3315
+ }) : extractDtoRoute({
3316
+ cls,
3317
+ method,
3318
+ verb,
3319
+ prefix,
3320
+ className,
3321
+ sourceFile,
3322
+ project,
3323
+ seenNames
3324
+ });
3325
+ if (route) routes.push(route);
3762
3326
  }
3763
3327
  }
3764
3328
  return routes;
@@ -3767,7 +3331,7 @@ function extractFromSourceFile(sourceFile, project) {
3767
3331
  // src/watch/lock-file.ts
3768
3332
  var import_promises10 = require("fs/promises");
3769
3333
  var import_promises11 = require("fs/promises");
3770
- var import_node_path12 = require("path");
3334
+ var import_node_path13 = require("path");
3771
3335
  var LOCK_FILE = ".watcher.lock";
3772
3336
  function isProcessAlive(pid) {
3773
3337
  try {
@@ -3779,7 +3343,7 @@ function isProcessAlive(pid) {
3779
3343
  }
3780
3344
  async function acquireLock(outDir) {
3781
3345
  await (0, import_promises11.mkdir)(outDir, { recursive: true });
3782
- const lockPath = (0, import_node_path12.join)(outDir, LOCK_FILE);
3346
+ const lockPath = (0, import_node_path13.join)(outDir, LOCK_FILE);
3783
3347
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3784
3348
  try {
3785
3349
  const fd = await (0, import_promises10.open)(lockPath, "wx");
@@ -3819,7 +3383,7 @@ async function watch(config, onChange) {
3819
3383
  if (lock === null) {
3820
3384
  let holderPid = "unknown";
3821
3385
  try {
3822
- const raw = await (0, import_promises12.readFile)((0, import_node_path13.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3386
+ const raw = await (0, import_promises12.readFile)((0, import_node_path14.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3823
3387
  const data = JSON.parse(raw);
3824
3388
  if (data.pid !== void 0) holderPid = String(data.pid);
3825
3389
  } catch {
@@ -3847,7 +3411,7 @@ async function watch(config, onChange) {
3847
3411
  }
3848
3412
  let pagesDebounceTimer;
3849
3413
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3850
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, pagesGlob), {
3414
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, pagesGlob), {
3851
3415
  ignoreInitial: true,
3852
3416
  persistent: true,
3853
3417
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3873,7 +3437,7 @@ async function watch(config, onChange) {
3873
3437
  pagesWatcher.on("change", schedulePagesRegenerate);
3874
3438
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3875
3439
  let contractsDebounceTimer;
3876
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.contracts.glob), {
3440
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.contracts.glob), {
3877
3441
  ignoreInitial: true,
3878
3442
  persistent: true,
3879
3443
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3903,7 +3467,7 @@ async function watch(config, onChange) {
3903
3467
  contractsWatcher.on("add", scheduleContractsRegenerate);
3904
3468
  contractsWatcher.on("change", scheduleContractsRegenerate);
3905
3469
  contractsWatcher.on("unlink", scheduleContractsRegenerate);
3906
- const formsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.forms.watch), {
3470
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.forms.watch), {
3907
3471
  ignoreInitial: true,
3908
3472
  persistent: true,
3909
3473
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3930,7 +3494,7 @@ async function watch(config, onChange) {
3930
3494
  }
3931
3495
 
3932
3496
  // src/index.ts
3933
- var VERSION = "0.2.1";
3497
+ var VERSION = "0.4.0";
3934
3498
 
3935
3499
  // src/cli/codegen.ts
3936
3500
  async function runCodegen(opts = {}) {
@@ -3958,15 +3522,51 @@ async function runCodegen(opts = {}) {
3958
3522
 
3959
3523
  // src/cli/doctor.ts
3960
3524
  var import_node_child_process2 = require("child_process");
3961
- var import_node_fs3 = require("fs");
3962
- var import_node_path15 = require("path");
3525
+ var import_node_fs4 = require("fs");
3526
+ var import_node_path16 = require("path");
3963
3527
 
3964
3528
  // src/cli/init.ts
3965
3529
  var import_node_child_process = require("child_process");
3966
- var import_node_fs2 = require("fs");
3530
+ var import_node_fs3 = require("fs");
3967
3531
  var import_promises13 = require("fs/promises");
3968
- var import_node_path14 = require("path");
3532
+ var import_node_path15 = require("path");
3969
3533
  var import_node_readline = require("readline");
3534
+
3535
+ // src/cli/patch-utils.ts
3536
+ var import_node_fs2 = require("fs");
3537
+ function patchJsonFile(filePath, mutator, parse = (raw) => raw) {
3538
+ let raw;
3539
+ try {
3540
+ raw = (0, import_node_fs2.readFileSync)(filePath, "utf8");
3541
+ } catch {
3542
+ return "skipped";
3543
+ }
3544
+ const json = JSON.parse(parse(raw));
3545
+ if (!mutator(json)) return "already";
3546
+ (0, import_node_fs2.writeFileSync)(filePath, `${JSON.stringify(json, null, 2)}
3547
+ `, "utf8");
3548
+ return "patched";
3549
+ }
3550
+ function findAfterLastImport(content) {
3551
+ const lastImportIndex = content.lastIndexOf("\nimport ");
3552
+ if (lastImportIndex !== -1) {
3553
+ const endOfLine = content.indexOf("\n", lastImportIndex + 1);
3554
+ return endOfLine !== -1 ? endOfLine + 1 : content.length;
3555
+ }
3556
+ if (content.startsWith("import ")) {
3557
+ const endOfLine = content.indexOf("\n");
3558
+ return endOfLine !== -1 ? endOfLine + 1 : content.length;
3559
+ }
3560
+ return 0;
3561
+ }
3562
+ function insertImport(content, stmt) {
3563
+ const insertAt = findAfterLastImport(content);
3564
+ if (insertAt <= 0) return content;
3565
+ return `${content.slice(0, insertAt)}${stmt}
3566
+ ${content.slice(insertAt)}`;
3567
+ }
3568
+
3569
+ // src/cli/init.ts
3970
3570
  var GITIGNORE_ENTRY = ".nestjs-inertia/";
3971
3571
  var green = (s) => `\x1B[32m${s}\x1B[0m`;
3972
3572
  var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
@@ -3991,7 +3591,7 @@ ${bold(title)}`);
3991
3591
  }
3992
3592
  async function readPackageJson(cwd) {
3993
3593
  try {
3994
- const raw = await (0, import_promises13.readFile)((0, import_node_path14.join)(cwd, "package.json"), "utf8");
3594
+ const raw = await (0, import_promises13.readFile)((0, import_node_path15.join)(cwd, "package.json"), "utf8");
3995
3595
  return JSON.parse(raw);
3996
3596
  } catch {
3997
3597
  return {};
@@ -4022,7 +3622,7 @@ async function detectTemplateEngine(cwd) {
4022
3622
  async function detectPackageManager(cwd) {
4023
3623
  async function exists(file) {
4024
3624
  try {
4025
- await (0, import_promises13.access)((0, import_node_path14.join)(cwd, file));
3625
+ await (0, import_promises13.access)((0, import_node_path15.join)(cwd, file));
4026
3626
  return true;
4027
3627
  } catch {
4028
3628
  return false;
@@ -4069,7 +3669,7 @@ async function writeIfNotExists(filePath, content, label) {
4069
3669
  logCreated(label);
4070
3670
  }
4071
3671
  async function handleViteConfig(cwd, framework) {
4072
- const filePath = (0, import_node_path14.join)(cwd, "vite.config.ts");
3672
+ const filePath = (0, import_node_path15.join)(cwd, "vite.config.ts");
4073
3673
  if (await fileExists2(filePath)) {
4074
3674
  const existing = await (0, import_promises13.readFile)(filePath, "utf8");
4075
3675
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
@@ -4127,7 +3727,7 @@ function installDeps(pkgManager, deps, dev) {
4127
3727
  }
4128
3728
  }
4129
3729
  async function patchPackageJsonScripts(cwd, scripts) {
4130
- const pkgPath = (0, import_node_path14.join)(cwd, "package.json");
3730
+ const pkgPath = (0, import_node_path15.join)(cwd, "package.json");
4131
3731
  let pkg = {};
4132
3732
  try {
4133
3733
  pkg = JSON.parse(await (0, import_promises13.readFile)(pkgPath, "utf8"));
@@ -4152,32 +3752,16 @@ async function patchPackageJsonScripts(cwd, scripts) {
4152
3752
  await (0, import_promises13.writeFile)(pkgPath, `${JSON.stringify(pkg, null, 2)}
4153
3753
  `, "utf8");
4154
3754
  }
4155
- function findAfterLastImport(content) {
4156
- const lastImportIndex = content.lastIndexOf("\nimport ");
4157
- if (lastImportIndex !== -1) {
4158
- const endOfLine = content.indexOf("\n", lastImportIndex + 1);
4159
- return endOfLine !== -1 ? endOfLine + 1 : content.length;
4160
- }
4161
- if (content.startsWith("import ")) {
4162
- const endOfLine = content.indexOf("\n");
4163
- return endOfLine !== -1 ? endOfLine + 1 : content.length;
4164
- }
4165
- return 0;
4166
- }
4167
3755
  function patchAppModule(filePath, rootView) {
4168
3756
  let content;
4169
3757
  try {
4170
- content = (0, import_node_fs2.readFileSync)(filePath, "utf8");
3758
+ content = (0, import_node_fs3.readFileSync)(filePath, "utf8");
4171
3759
  } catch {
4172
3760
  return "skipped";
4173
3761
  }
4174
3762
  let changed = false;
4175
3763
  if (!content.includes("InertiaModule")) {
4176
- const insertAt = findAfterLastImport(content);
4177
- if (insertAt > 0) {
4178
- content = `${content.slice(0, insertAt)}import { InertiaModule } from '@dudousxd/nestjs-inertia';
4179
- ${content.slice(insertAt)}`;
4180
- }
3764
+ content = insertImport(content, "import { InertiaModule } from '@dudousxd/nestjs-inertia';");
4181
3765
  if (!content.includes("from 'node:path'") && !content.includes('from "node:path"')) {
4182
3766
  const insertAt2 = findAfterLastImport(content);
4183
3767
  content = `${content.slice(0, insertAt2)}import { resolve } from 'node:path';
@@ -4195,11 +3779,7 @@ ${indent}}),${content.slice(bracketPos)}`;
4195
3779
  }
4196
3780
  }
4197
3781
  if (!content.includes("HomeController")) {
4198
- const insertAt = findAfterLastImport(content);
4199
- if (insertAt > 0) {
4200
- content = `${content.slice(0, insertAt)}import { HomeController } from './home.controller';
4201
- ${content.slice(insertAt)}`;
4202
- }
3782
+ content = insertImport(content, "import { HomeController } from './home.controller';");
4203
3783
  const controllersMatch = content.match(/controllers\s*:\s*\[/);
4204
3784
  if (controllersMatch?.index !== void 0) {
4205
3785
  const bracketPos = content.indexOf("[", controllersMatch.index) + 1;
@@ -4210,22 +3790,21 @@ ${indent}HomeController,${content.slice(bracketPos)}`;
4210
3790
  }
4211
3791
  }
4212
3792
  if (!changed) return "already";
4213
- (0, import_node_fs2.writeFileSync)(filePath, content, "utf8");
3793
+ (0, import_node_fs3.writeFileSync)(filePath, content, "utf8");
4214
3794
  return "patched";
4215
3795
  }
4216
3796
  function patchMainTs(filePath) {
4217
3797
  let content;
4218
3798
  try {
4219
- content = (0, import_node_fs2.readFileSync)(filePath, "utf8");
3799
+ content = (0, import_node_fs3.readFileSync)(filePath, "utf8");
4220
3800
  } catch {
4221
3801
  return "skipped";
4222
3802
  }
4223
3803
  if (content.includes("setupInertiaVite")) return "already";
4224
- const insertAt = findAfterLastImport(content);
4225
- if (insertAt > 0) {
4226
- content = `${content.slice(0, insertAt)}import { setupInertiaVite } from '@dudousxd/nestjs-inertia-vite';
4227
- ${content.slice(insertAt)}`;
4228
- }
3804
+ content = insertImport(
3805
+ content,
3806
+ "import { setupInertiaVite } from '@dudousxd/nestjs-inertia-vite';"
3807
+ );
4229
3808
  const createMatch = content.match(
4230
3809
  /(?:const|let)\s+(\w+)\s*=\s*await\s+NestFactory\.create[^;]+;/
4231
3810
  );
@@ -4242,7 +3821,7 @@ ${content.slice(insertAt)}`;
4242
3821
  });`;
4243
3822
  content = `${content.slice(0, insertAfterPos)}
4244
3823
  ${viteSetup}${content.slice(insertAfterPos)}`;
4245
- (0, import_node_fs2.writeFileSync)(filePath, content, "utf8");
3824
+ (0, import_node_fs3.writeFileSync)(filePath, content, "utf8");
4246
3825
  return "patched";
4247
3826
  }
4248
3827
  function configTemplate(framework) {
@@ -4465,107 +4044,84 @@ export class HomeController {
4465
4044
  }
4466
4045
  `;
4467
4046
  function patchTsconfigExclude(cwd, dir, filename = "tsconfig.json") {
4468
- const filePath = (0, import_node_path14.join)(cwd, filename);
4469
- let raw;
4470
- try {
4471
- raw = (0, import_node_fs2.readFileSync)(filePath, "utf8");
4472
- } catch {
4473
- return "skipped";
4474
- }
4475
- const stripped = raw.replace(/\/\/.*$/gm, "");
4476
- const json = JSON.parse(stripped);
4477
- const exclude = json.exclude ?? [];
4478
- if (exclude.includes(dir)) return "already";
4479
- exclude.push(dir);
4480
- json.exclude = exclude;
4481
- (0, import_node_fs2.writeFileSync)(filePath, `${JSON.stringify(json, null, 2)}
4482
- `, "utf8");
4483
- return "patched";
4047
+ const filePath = (0, import_node_path15.join)(cwd, filename);
4048
+ return patchJsonFile(
4049
+ filePath,
4050
+ (json) => {
4051
+ const exclude = json.exclude ?? [];
4052
+ if (exclude.includes(dir)) return false;
4053
+ exclude.push(dir);
4054
+ json.exclude = exclude;
4055
+ return true;
4056
+ },
4057
+ // Strip single-line comments before JSON.parse
4058
+ (raw) => raw.replace(/\/\/.*$/gm, "")
4059
+ );
4484
4060
  }
4485
4061
  function patchNestCliJson(cwd, shellDir) {
4486
- const filePath = (0, import_node_path14.join)(cwd, "nest-cli.json");
4487
- let raw;
4488
- try {
4489
- raw = (0, import_node_fs2.readFileSync)(filePath, "utf8");
4490
- } catch {
4491
- return "skipped";
4492
- }
4493
- const json = JSON.parse(raw);
4494
- const compiler = json.compilerOptions ?? {};
4495
- const assets = compiler.assets ?? [];
4496
- const alreadyHas = assets.some((a) => {
4497
- if (typeof a === "string") return a.includes(shellDir);
4498
- return String(a.include ?? "").includes(shellDir);
4499
- });
4500
- if (alreadyHas) return "already";
4501
- assets.push({
4502
- include: `../${shellDir}/**/*`,
4503
- outDir: `dist/${shellDir}`,
4504
- watchAssets: true
4062
+ const filePath = (0, import_node_path15.join)(cwd, "nest-cli.json");
4063
+ return patchJsonFile(filePath, (json) => {
4064
+ const compiler = json.compilerOptions ?? {};
4065
+ const assets = compiler.assets ?? [];
4066
+ const alreadyHas = assets.some((a) => {
4067
+ if (typeof a === "string") return a.includes(shellDir);
4068
+ return String(a.include ?? "").includes(shellDir);
4069
+ });
4070
+ if (alreadyHas) return false;
4071
+ assets.push({
4072
+ include: `../${shellDir}/**/*`,
4073
+ outDir: `dist/${shellDir}`,
4074
+ watchAssets: true
4075
+ });
4076
+ compiler.assets = assets;
4077
+ json.compilerOptions = compiler;
4078
+ return true;
4505
4079
  });
4506
- compiler.assets = assets;
4507
- json.compilerOptions = compiler;
4508
- (0, import_node_fs2.writeFileSync)(filePath, `${JSON.stringify(json, null, 2)}
4509
- `, "utf8");
4510
- return "patched";
4511
4080
  }
4512
- async function runInit(opts = {}) {
4513
- const cwd = opts.cwd ?? process.cwd();
4514
- console.log(`
4515
- ${bold("nestjs-inertia init")}`);
4516
- let framework = await detectFramework(cwd);
4517
- if (!framework) {
4518
- framework = await promptFramework();
4519
- }
4520
- const engine = await detectTemplateEngine(cwd);
4521
- const engineLabel = engine === "html" ? "plain HTML" : engine;
4522
- const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
4523
- console.log(`
4524
- Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
4525
- const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
4526
- const entryExt = framework === "react" ? "tsx" : "ts";
4527
- const pageExt = framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte";
4081
+ async function scaffoldFiles(ctx) {
4082
+ const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
4528
4083
  logSection("Scaffold files");
4529
4084
  await writeIfNotExists(
4530
- (0, import_node_path14.join)(cwd, "nestjs-inertia.config.ts"),
4085
+ (0, import_node_path15.join)(cwd, "nestjs-inertia.config.ts"),
4531
4086
  configTemplate(framework),
4532
4087
  "nestjs-inertia.config.ts"
4533
4088
  );
4534
- await writeIfNotExists((0, import_node_path14.join)(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4089
+ await writeIfNotExists((0, import_node_path15.join)(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4535
4090
  await writeIfNotExists(
4536
- (0, import_node_path14.join)(cwd, "tsconfig.inertia.json"),
4091
+ (0, import_node_path15.join)(cwd, "tsconfig.inertia.json"),
4537
4092
  TSCONFIG_INERTIA_TEMPLATE,
4538
4093
  "tsconfig.inertia.json"
4539
4094
  );
4540
4095
  await writeIfNotExists(
4541
- (0, import_node_path14.join)(cwd, "inertia", "tsconfig.json"),
4096
+ (0, import_node_path15.join)(cwd, "inertia", "tsconfig.json"),
4542
4097
  INERTIA_TSCONFIG_TEMPLATE,
4543
4098
  "inertia/tsconfig.json"
4544
4099
  );
4545
4100
  await writeIfNotExists(
4546
- (0, import_node_path14.join)(cwd, "inertia", shellFileName),
4101
+ (0, import_node_path15.join)(cwd, "inertia", shellFileName),
4547
4102
  htmlShellTemplate(framework, engine),
4548
4103
  `inertia/${shellFileName}`
4549
4104
  );
4550
4105
  await handleViteConfig(cwd, framework);
4551
4106
  await writeIfNotExists(
4552
- (0, import_node_path14.join)(cwd, "inertia", "app", `client.${entryExt}`),
4107
+ (0, import_node_path15.join)(cwd, "inertia", "app", `client.${entryExt}`),
4553
4108
  entryPointTemplate(framework),
4554
4109
  `inertia/app/client.${entryExt}`
4555
4110
  );
4556
4111
  await writeIfNotExists(
4557
- (0, import_node_path14.join)(cwd, "inertia", "pages", `Home.${pageExt}`),
4112
+ (0, import_node_path15.join)(cwd, "inertia", "pages", `Home.${pageExt}`),
4558
4113
  samplePageTemplate(framework),
4559
4114
  `inertia/pages/Home.${pageExt}`
4560
4115
  );
4561
4116
  await writeIfNotExists(
4562
- (0, import_node_path14.join)(cwd, "src", "home.controller.ts"),
4117
+ (0, import_node_path15.join)(cwd, "src", "home.controller.ts"),
4563
4118
  SAMPLE_CONTROLLER,
4564
4119
  "src/home.controller.ts"
4565
4120
  );
4566
- logSection("Patch existing files");
4567
- const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
4568
- const appModulePath = (0, import_node_path14.join)(cwd, "src", "app.module.ts");
4121
+ }
4122
+ function patchServerAppModule(ctx) {
4123
+ const { cwd, rootView } = ctx;
4124
+ const appModulePath = (0, import_node_path15.join)(cwd, "src", "app.module.ts");
4569
4125
  const appModuleResult = patchAppModule(appModulePath, rootView);
4570
4126
  if (appModuleResult === "patched") {
4571
4127
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -4577,7 +4133,9 @@ ${bold("nestjs-inertia init")}`);
4577
4133
  } else {
4578
4134
  logWarning("src/app.module.ts not found \u2014 add InertiaModule.forRoot() manually");
4579
4135
  }
4580
- const mainTsPath = (0, import_node_path14.join)(cwd, "src", "main.ts");
4136
+ }
4137
+ function patchServerMainTs(ctx) {
4138
+ const mainTsPath = (0, import_node_path15.join)(ctx.cwd, "src", "main.ts");
4581
4139
  const mainTsResult = patchMainTs(mainTsPath);
4582
4140
  if (mainTsResult === "patched") {
4583
4141
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -4586,7 +4144,9 @@ ${bold("nestjs-inertia init")}`);
4586
4144
  } else {
4587
4145
  logWarning("src/main.ts not found \u2014 add setupInertiaVite() manually");
4588
4146
  }
4589
- const shellDir = rootView.split("/")[0];
4147
+ }
4148
+ function patchBuildConfigs(ctx) {
4149
+ const { cwd, shellDir } = ctx;
4590
4150
  const nestCliResult = patchNestCliJson(cwd, shellDir);
4591
4151
  if (nestCliResult === "patched") {
4592
4152
  logPatched("nest-cli.json", `added asset copy for ${shellDir}/ \u2192 dist/${shellDir}/`);
@@ -4607,18 +4167,26 @@ ${bold("nestjs-inertia init")}`);
4607
4167
  );
4608
4168
  }
4609
4169
  }
4610
- await patchGitignore((0, import_node_path14.join)(cwd, ".gitignore"));
4170
+ }
4171
+ async function patchGitignoreAndDist(ctx) {
4172
+ const { cwd } = ctx;
4173
+ await patchGitignore((0, import_node_path15.join)(cwd, ".gitignore"));
4611
4174
  const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
4612
4175
  if (tsconfigDistResult === "patched") {
4613
4176
  logPatched("tsconfig.json", "excluded dist/ from server compilation");
4614
4177
  } else if (tsconfigDistResult === "already") {
4615
4178
  console.log(` ${cyan("\u2192")} tsconfig.json ${dim("(dist/ already excluded, skipped)")}`);
4616
4179
  }
4617
- await patchPackageJsonScripts(cwd, {
4180
+ }
4181
+ async function scaffoldPackageScripts(ctx) {
4182
+ await patchPackageJsonScripts(ctx.cwd, {
4618
4183
  "build:client": "vite build",
4619
4184
  "build:ssr": "VITE_SSR=1 vite build --ssr",
4620
4185
  "typecheck:inertia": "tsc --noEmit -p tsconfig.inertia.json"
4621
4186
  });
4187
+ }
4188
+ async function installMissingDeps(ctx) {
4189
+ const { cwd, framework, opts } = ctx;
4622
4190
  logSection("Install dependencies");
4623
4191
  const pkg = await readPackageJson(cwd);
4624
4192
  const installedDeps = allDeps(pkg);
@@ -4652,6 +4220,55 @@ ${bold("nestjs-inertia init")}`);
4652
4220
  installDeps(pkgManager, depsToInstall, false);
4653
4221
  installDeps(pkgManager, devDepsToInstall, true);
4654
4222
  }
4223
+ }
4224
+ var INIT_STEPS = [
4225
+ { label: "scaffold files", run: scaffoldFiles },
4226
+ {
4227
+ label: "patch app.module.ts",
4228
+ run: (ctx) => {
4229
+ logSection("Patch existing files");
4230
+ patchServerAppModule(ctx);
4231
+ }
4232
+ },
4233
+ { label: "patch main.ts", run: patchServerMainTs },
4234
+ { label: "patch build configs", run: patchBuildConfigs },
4235
+ { label: "patch .gitignore + dist exclude", run: patchGitignoreAndDist },
4236
+ { label: "add package.json scripts", run: scaffoldPackageScripts },
4237
+ { label: "install dependencies", run: installMissingDeps }
4238
+ ];
4239
+ async function runInit(opts = {}) {
4240
+ const cwd = opts.cwd ?? process.cwd();
4241
+ console.log(`
4242
+ ${bold("nestjs-inertia init")}`);
4243
+ let framework = await detectFramework(cwd);
4244
+ if (!framework) {
4245
+ framework = await promptFramework();
4246
+ }
4247
+ const engine = await detectTemplateEngine(cwd);
4248
+ const engineLabel = engine === "html" ? "plain HTML" : engine;
4249
+ const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
4250
+ console.log(`
4251
+ Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
4252
+ const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
4253
+ const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
4254
+ const ctx = {
4255
+ opts,
4256
+ cwd,
4257
+ framework,
4258
+ engine,
4259
+ shellFileName,
4260
+ entryExt: framework === "react" ? "tsx" : "ts",
4261
+ pageExt: framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte",
4262
+ rootView,
4263
+ shellDir: rootView.split("/")[0]
4264
+ // e.g. "inertia" from "inertia/index.html"
4265
+ };
4266
+ for (const step of INIT_STEPS) {
4267
+ if (process.env.NESTJS_CODEGEN_DEBUG) {
4268
+ console.log(dim(` \xB7 ${step.label}`));
4269
+ }
4270
+ await step.run(ctx);
4271
+ }
4655
4272
  console.log(`
4656
4273
  ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4657
4274
  `);
@@ -4659,18 +4276,18 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4659
4276
 
4660
4277
  // src/cli/doctor.ts
4661
4278
  function checkFileExists(cwd, file) {
4662
- return (0, import_node_fs3.existsSync)((0, import_node_path15.join)(cwd, file));
4279
+ return (0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, file));
4663
4280
  }
4664
4281
  function readJson(path) {
4665
4282
  try {
4666
- const raw = (0, import_node_fs3.readFileSync)(path, "utf8").replace(/\/\/.*$/gm, "");
4283
+ const raw = (0, import_node_fs4.readFileSync)(path, "utf8").replace(/\/\/.*$/gm, "");
4667
4284
  return JSON.parse(raw);
4668
4285
  } catch {
4669
4286
  return null;
4670
4287
  }
4671
4288
  }
4672
4289
  function writeJsonField(filePath, dotPath, value) {
4673
- const raw = (0, import_node_fs3.readFileSync)(filePath, "utf8");
4290
+ const raw = (0, import_node_fs4.readFileSync)(filePath, "utf8");
4674
4291
  const stripped = raw.replace(/\/\/.*$/gm, "");
4675
4292
  const obj = JSON.parse(stripped);
4676
4293
  let target = obj;
@@ -4690,20 +4307,20 @@ function writeJsonField(filePath, dotPath, value) {
4690
4307
  } else {
4691
4308
  target[lastKey] = value;
4692
4309
  }
4693
- (0, import_node_fs3.writeFileSync)(filePath, `${JSON.stringify(obj, null, 2)}
4310
+ (0, import_node_fs4.writeFileSync)(filePath, `${JSON.stringify(obj, null, 2)}
4694
4311
  `, "utf8");
4695
4312
  }
4696
4313
  function getPackageVersion(cwd, pkg) {
4697
4314
  try {
4698
- const pkgJson = readJson((0, import_node_path15.join)(cwd, "node_modules", pkg, "package.json"));
4315
+ const pkgJson = readJson((0, import_node_path16.join)(cwd, "node_modules", pkg, "package.json"));
4699
4316
  return pkgJson?.version ?? null;
4700
4317
  } catch {
4701
4318
  return null;
4702
4319
  }
4703
4320
  }
4704
4321
  function detectPkgManager(cwd) {
4705
- if ((0, import_node_fs3.existsSync)((0, import_node_path15.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
4706
- if ((0, import_node_fs3.existsSync)((0, import_node_path15.join)(cwd, "yarn.lock"))) return "yarn";
4322
+ if ((0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
4323
+ if ((0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, "yarn.lock"))) return "yarn";
4707
4324
  return "npm";
4708
4325
  }
4709
4326
  async function runDoctor(opts) {
@@ -4741,7 +4358,7 @@ async function runDoctor(opts) {
4741
4358
  autoFix: () => runInit({ cwd })
4742
4359
  });
4743
4360
  if (foundShellDir) {
4744
- const nestCliPath = (0, import_node_path15.join)(cwd, "nest-cli.json");
4361
+ const nestCliPath = (0, import_node_path16.join)(cwd, "nest-cli.json");
4745
4362
  const nestCli = readJson(nestCliPath);
4746
4363
  const compiler = nestCli?.compilerOptions ?? {};
4747
4364
  const assets = compiler.assets ?? [];
@@ -4780,7 +4397,7 @@ async function runDoctor(opts) {
4780
4397
  fix: "Run: nestjs-codegen codegen",
4781
4398
  autoFix: () => runCodegen({ cwd })
4782
4399
  });
4783
- const tsconfigPath = (0, import_node_path15.join)(cwd, "tsconfig.json");
4400
+ const tsconfigPath = (0, import_node_path16.join)(cwd, "tsconfig.json");
4784
4401
  const tsconfig = readJson(tsconfigPath);
4785
4402
  const paths = tsconfig?.compilerOptions?.paths;
4786
4403
  checks.push({
@@ -4791,7 +4408,7 @@ async function runDoctor(opts) {
4791
4408
  });
4792
4409
  const inertiaDir = foundShellDir ?? "inertia";
4793
4410
  for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
4794
- const tsc = readJson((0, import_node_path15.join)(cwd, tsconfigFile));
4411
+ const tsc = readJson((0, import_node_path16.join)(cwd, tsconfigFile));
4795
4412
  if (!tsc) continue;
4796
4413
  const excl = tsc.exclude ?? [];
4797
4414
  const excludesIt = excl.includes(inertiaDir);
@@ -4817,14 +4434,14 @@ async function runDoctor(opts) {
4817
4434
  }
4818
4435
  });
4819
4436
  }
4820
- const inertiaTsconfigPath = (0, import_node_path15.join)(cwd, "tsconfig.inertia.json");
4437
+ const inertiaTsconfigPath = (0, import_node_path16.join)(cwd, "tsconfig.inertia.json");
4821
4438
  const inertiaTsconfig = readJson(inertiaTsconfigPath);
4822
4439
  checks.push({
4823
4440
  name: "tsconfig.inertia.json exists",
4824
4441
  pass: !!inertiaTsconfig,
4825
4442
  fix: "Create tsconfig.inertia.json (typechecks inertia/ + .nestjs-inertia/)",
4826
4443
  autoFix: () => {
4827
- (0, import_node_fs3.writeFileSync)(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
4444
+ (0, import_node_fs4.writeFileSync)(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
4828
4445
  }
4829
4446
  });
4830
4447
  if (inertiaTsconfig) {
@@ -4869,17 +4486,17 @@ async function runDoctor(opts) {
4869
4486
  fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
4870
4487
  });
4871
4488
  }
4872
- const innerTsconfigPath = (0, import_node_path15.join)(cwd, "inertia", "tsconfig.json");
4489
+ const innerTsconfigPath = (0, import_node_path16.join)(cwd, "inertia", "tsconfig.json");
4873
4490
  checks.push({
4874
4491
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
4875
- pass: (0, import_node_fs3.existsSync)(innerTsconfigPath),
4492
+ pass: (0, import_node_fs4.existsSync)(innerTsconfigPath),
4876
4493
  fix: "Create inertia/tsconfig.json that extends ../tsconfig.inertia.json",
4877
4494
  autoFix: () => {
4878
- (0, import_node_fs3.writeFileSync)(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
4495
+ (0, import_node_fs4.writeFileSync)(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
4879
4496
  }
4880
4497
  });
4881
4498
  if (checkFileExists(cwd, "vite.config.ts")) {
4882
- const viteContent = (0, import_node_fs3.readFileSync)((0, import_node_path15.join)(cwd, "vite.config.ts"), "utf8");
4499
+ const viteContent = (0, import_node_fs4.readFileSync)((0, import_node_path16.join)(cwd, "vite.config.ts"), "utf8");
4883
4500
  checks.push({
4884
4501
  name: "vite.config.ts has resolve.alias",
4885
4502
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -4944,16 +4561,16 @@ async function runDoctor(opts) {
4944
4561
  });
4945
4562
  }
4946
4563
  if (checkFileExists(cwd, ".gitignore")) {
4947
- const gitignorePath = (0, import_node_path15.join)(cwd, ".gitignore");
4948
- const gitignore = (0, import_node_fs3.readFileSync)(gitignorePath, "utf8");
4564
+ const gitignorePath = (0, import_node_path16.join)(cwd, ".gitignore");
4565
+ const gitignore = (0, import_node_fs4.readFileSync)(gitignorePath, "utf8");
4949
4566
  checks.push({
4950
4567
  name: ".gitignore includes .nestjs-inertia/",
4951
4568
  pass: gitignore.includes(".nestjs-inertia"),
4952
4569
  fix: "Add .nestjs-inertia/ to .gitignore",
4953
- autoFix: () => (0, import_node_fs3.appendFileSync)(gitignorePath, "\n.nestjs-inertia/\n")
4570
+ autoFix: () => (0, import_node_fs4.appendFileSync)(gitignorePath, "\n.nestjs-inertia/\n")
4954
4571
  });
4955
4572
  }
4956
- const pkgJsonPath = (0, import_node_path15.join)(cwd, "package.json");
4573
+ const pkgJsonPath = (0, import_node_path16.join)(cwd, "package.json");
4957
4574
  const pkgJson = readJson(pkgJsonPath);
4958
4575
  const scripts = pkgJson?.scripts ?? {};
4959
4576
  checks.push({