@dudousxd/nestjs-codegen 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.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,196 @@ 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 decls = [];
1289
+ const mapEntries = [];
1290
+ let used = false;
1291
+ const renderSource = (src, rename) => {
1292
+ if (src.schema) {
1293
+ const r = adapter.renderModule(src.schema);
1294
+ for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
1295
+ return { text: r.schemaText };
1296
+ }
1297
+ if (src.zodText) {
1298
+ if (!acceptsRawZod) {
1299
+ return {
1300
+ text: "",
1301
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1302
+ };
1303
+ }
1304
+ return { text: applyRenames(src.zodText, rename) };
1305
+ }
1306
+ if (src.zodRef) {
1307
+ if (!acceptsRawZod) {
1308
+ return {
1309
+ text: "",
1310
+ warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
1311
+ };
1312
+ }
1313
+ const root = refRootIdentifier(src.zodRef.name);
1314
+ const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
1315
+ const member = src.zodRef.name.slice(root.length);
1316
+ return { text: `${alias}${member}` };
1317
+ }
1318
+ return null;
1319
+ };
1320
+ for (const entry of entries) {
1321
+ const block = [];
1322
+ const rename = renamesByEntry.get(entry) ?? null;
1323
+ let bodyConst;
1324
+ if (entry.warnings && entry.warnings.length > 0) {
1325
+ for (const w of entry.warnings) block.push(`// warning: ${w}`);
1326
+ }
1327
+ if (entry.body) {
1328
+ const rendered = renderSource(entry.body, rename);
1329
+ if (rendered?.warn) {
1330
+ block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
1331
+ } else if (rendered) {
1332
+ used = true;
1333
+ bodyConst = `${entry.baseName}BodySchema`;
1334
+ block.push(`export const ${bodyConst} = ${rendered.text};`);
1335
+ block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
1336
+ }
1337
+ }
1338
+ if (entry.query) {
1339
+ const rendered = renderSource(entry.query, rename);
1340
+ if (rendered?.warn) {
1341
+ block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
1342
+ } else if (rendered) {
1343
+ used = true;
1344
+ const queryConst = `${entry.baseName}QuerySchema`;
1345
+ block.push(`export const ${queryConst} = ${rendered.text};`);
1346
+ block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
1347
+ }
1348
+ }
1349
+ if (block.length === 0) continue;
1350
+ decls.push(`// ${entry.routeName}`, ...block, "");
1351
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
1352
+ }
1353
+ if (!used) return null;
1354
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
1355
+ if (acceptsRawZod) {
1356
+ const zodImport = config?.zodImport ?? "zod";
1357
+ lines.push(`import { z } from '${zodImport}';`);
1358
+ } else {
1359
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
1360
+ }
1361
+ lines.push(...importLines);
1362
+ lines.push("");
1363
+ const allNested = /* @__PURE__ */ new Map();
1364
+ for (const [n, t] of globalSchemas) allNested.set(n, t);
1365
+ for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
1366
+ if (allNested.size > 0) {
1367
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
1368
+ for (const [n, t] of allNested) lines.push(`const ${n} = ${t};`);
1369
+ lines.push("");
1370
+ }
1371
+ lines.push(...decls);
1372
+ lines.push("/** Route name \u2192 body schema map. */");
1373
+ lines.push("export const formSchemas = {");
1374
+ lines.push(...mapEntries);
1375
+ lines.push("} as const;");
1376
+ lines.push("");
1377
+ return lines.join("\n");
1378
+ }
1379
+
1380
+ // src/emit/emit-index.ts
1381
+ var import_promises6 = require("fs/promises");
1382
+ var import_node_path7 = require("path");
1383
+ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1384
+ await (0, import_promises6.mkdir)(outDir, { recursive: true });
1385
+ const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
1386
+ if (hasContracts) {
1387
+ exports2.push("export * from './api.js';");
1388
+ }
1389
+ if (hasForms) {
1390
+ exports2.push("export * from './forms.js';");
1391
+ }
1392
+ const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
1393
+ "\n"
1394
+ );
1395
+ await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
1396
+ }
1397
+
1398
+ // src/emit/emit-pages.ts
1399
+ var import_promises7 = require("fs/promises");
1400
+ var import_node_path8 = require("path");
1401
+ async function emitPages(pages, outDir, _options = {}) {
1402
+ await (0, import_promises7.mkdir)(outDir, { recursive: true });
1403
+ const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
1404
+ const augBody = pages.map((p) => {
1405
+ const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
1406
+ const valueType = buildAugmentationType(p, outDir);
1505
1407
  return ` ${key}: ${valueType};`;
1506
1408
  }).join("\n");
1507
1409
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
@@ -1516,7 +1418,7 @@ ${augBody}
1516
1418
  }
1517
1419
  ${sharedPropsBlock}}
1518
1420
  `;
1519
- await (0, import_promises7.writeFile)((0, import_node_path7.join)(outDir, "pages.d.ts"), content, "utf8");
1421
+ await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, "pages.d.ts"), content, "utf8");
1520
1422
  }
1521
1423
  function buildSharedPropsBlock(sharedProps) {
1522
1424
  if (!sharedProps) return "";
@@ -1535,7 +1437,7 @@ ${propsBody}
1535
1437
  `;
1536
1438
  }
1537
1439
  function buildAugmentationType(page, outDir) {
1538
- let importPath = (0, import_node_path7.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1440
+ let importPath = (0, import_node_path8.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
1539
1441
  if (!importPath.startsWith(".")) {
1540
1442
  importPath = `./${importPath}`;
1541
1443
  }
@@ -1547,11 +1449,11 @@ function needsQuotes(name) {
1547
1449
 
1548
1450
  // src/emit/emit-routes.ts
1549
1451
  var import_promises8 = require("fs/promises");
1550
- var import_node_path8 = require("path");
1452
+ var import_node_path9 = require("path");
1551
1453
  async function emitRoutes(routes, outDir) {
1552
1454
  await (0, import_promises8.mkdir)(outDir, { recursive: true });
1553
1455
  const content = buildRoutesFile(routes);
1554
- await (0, import_promises8.writeFile)((0, import_node_path8.join)(outDir, "routes.ts"), content, "utf8");
1456
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, "routes.ts"), content, "utf8");
1555
1457
  }
1556
1458
  function buildRoutesFile(routes) {
1557
1459
  if (routes.length === 0) {
@@ -1679,30 +1581,7 @@ async function generate(config, inputRoutes = []) {
1679
1581
  propsExport: pagesConfig.propsExport,
1680
1582
  componentNameStrategy: pagesConfig.componentNameStrategy
1681
1583
  });
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
- }
1584
+ const sharedProps = discoverSharedPropsFromConfig(config);
1706
1585
  await emitPages(pages, config.codegen.outDir, {
1707
1586
  propsExport: pagesConfig.propsExport,
1708
1587
  sharedProps
@@ -1726,8 +1605,8 @@ async function generate(config, inputRoutes = []) {
1726
1605
  if (extensions.length > 0) {
1727
1606
  const extraFiles = await collectEmittedFiles(extensions, ctx);
1728
1607
  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 });
1608
+ const dest = (0, import_node_path10.join)(config.codegen.outDir, file.path);
1609
+ await (0, import_promises9.mkdir)((0, import_node_path10.dirname)(dest), { recursive: true });
1731
1610
  await (0, import_promises9.writeFile)(dest, file.contents, "utf8");
1732
1611
  }
1733
1612
  }
@@ -1735,35 +1614,31 @@ async function generate(config, inputRoutes = []) {
1735
1614
 
1736
1615
  // src/watch/watcher.ts
1737
1616
  var import_promises12 = require("fs/promises");
1738
- var import_node_path13 = require("path");
1617
+ var import_node_path14 = require("path");
1739
1618
  var import_chokidar = __toESM(require("chokidar"), 1);
1740
1619
 
1741
1620
  // src/discovery/contracts-fast.ts
1742
- var import_node_path11 = require("path");
1621
+ var import_node_path12 = require("path");
1743
1622
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
1744
1623
  var import_ts_morph9 = require("ts-morph");
1745
1624
 
1625
+ // src/discovery/dto-type-resolver.ts
1626
+ var import_ts_morph7 = require("ts-morph");
1627
+
1746
1628
  // src/discovery/dto-to-ir.ts
1747
- var import_ts_morph5 = require("ts-morph");
1629
+ var import_ts_morph4 = require("ts-morph");
1748
1630
 
1749
1631
  // src/discovery/type-ref-resolution.ts
1750
1632
  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;
1633
+ var import_node_path11 = require("path");
1634
+ var import_ts_morph3 = require("ts-morph");
1635
+ var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
1636
+ var _ctxByProject = /* @__PURE__ */ new WeakMap();
1637
+ function setDiscoveryContext(project, ctx) {
1638
+ _ctxByProject.set(project, ctx);
1764
1639
  }
1765
- function _tsconfigPaths() {
1766
- return _ctx.tsconfigPaths;
1640
+ function _ctxFor(project) {
1641
+ return _ctxByProject.get(project) ?? _EMPTY_CTX;
1767
1642
  }
1768
1643
  var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
1769
1644
  function dbg(...args) {
@@ -1805,18 +1680,19 @@ function findTypeInFile(name, file) {
1805
1680
  }
1806
1681
  return null;
1807
1682
  }
1808
- function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1683
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
1809
1684
  if (moduleSpecifier.startsWith(".")) {
1810
- const dir = (0, import_node_path10.dirname)(sourceFile.getFilePath());
1685
+ const dir = (0, import_node_path11.dirname)(sourceFile.getFilePath());
1811
1686
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
1812
1687
  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")
1688
+ (0, import_node_path11.resolve)(dir, `${noExt}.ts`),
1689
+ (0, import_node_path11.resolve)(dir, `${moduleSpecifier}.ts`),
1690
+ (0, import_node_path11.resolve)(dir, moduleSpecifier, "index.ts")
1816
1691
  ];
1817
1692
  }
1818
- const baseUrl = _projectRoot();
1819
- const tsconfigPaths = _tsconfigPaths();
1693
+ const ctx = _ctxFor(project);
1694
+ const baseUrl = ctx.projectRoot;
1695
+ const tsconfigPaths = ctx.tsconfigPaths;
1820
1696
  dbg(
1821
1697
  "resolveModuleSpecifier",
1822
1698
  moduleSpecifier,
@@ -1832,8 +1708,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
1832
1708
  const rest = moduleSpecifier.slice(prefix.length);
1833
1709
  const candidates = [];
1834
1710
  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"));
1711
+ const resolved = (0, import_node_path11.resolve)(baseUrl, mapping.replace("*", rest));
1712
+ candidates.push(`${resolved}.ts`, (0, import_node_path11.resolve)(resolved, "index.ts"));
1837
1713
  }
1838
1714
  dbg(" resolved candidates:", candidates);
1839
1715
  return candidates;
@@ -1933,13 +1809,13 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1933
1809
  name = nodeOrName;
1934
1810
  } else {
1935
1811
  const typeNode = nodeOrName;
1936
- if (opts.unwrapContainers && import_ts_morph4.Node.isArrayTypeNode(typeNode)) {
1812
+ if (opts.unwrapContainers && import_ts_morph3.Node.isArrayTypeNode(typeNode)) {
1937
1813
  const inner = resolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project, opts);
1938
1814
  return inner ? { ...inner, isArray: true } : null;
1939
1815
  }
1940
- if (!import_ts_morph4.Node.isTypeReference(typeNode)) return null;
1816
+ if (!import_ts_morph3.Node.isTypeReference(typeNode)) return null;
1941
1817
  const typeName = typeNode.getTypeName();
1942
- const refName = import_ts_morph4.Node.isIdentifier(typeName) ? typeName.getText() : null;
1818
+ const refName = import_ts_morph3.Node.isIdentifier(typeName) ? typeName.getText() : null;
1943
1819
  if (!refName) return null;
1944
1820
  if (opts.unwrapContainers && refName === "Promise") {
1945
1821
  const first = typeNode.getTypeArguments()[0];
@@ -1962,7 +1838,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
1962
1838
  if (!namedImport) continue;
1963
1839
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
1964
1840
  if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
1965
- const tsconfigPaths = _tsconfigPaths();
1841
+ const tsconfigPaths = _ctxFor(project).tsconfigPaths;
1966
1842
  const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
1967
1843
  const prefix = p.replace("*", "");
1968
1844
  return moduleSpecifier.startsWith(prefix);
@@ -2234,22 +2110,22 @@ function firstArgText(decorator) {
2234
2110
  }
2235
2111
  function numericArg(decorator) {
2236
2112
  const arg = firstArg(decorator);
2237
- if (arg && import_ts_morph5.Node.isNumericLiteral(arg)) return arg.getText();
2113
+ if (arg && import_ts_morph4.Node.isNumericLiteral(arg)) return arg.getText();
2238
2114
  return null;
2239
2115
  }
2240
2116
  function numericArgs(decorator) {
2241
2117
  const args = decorator?.getArguments() ?? [];
2242
- const num = (n) => n && import_ts_morph5.Node.isNumericLiteral(n) ? n.getText() : null;
2118
+ const num = (n) => n && import_ts_morph4.Node.isNumericLiteral(n) ? n.getText() : null;
2243
2119
  return [num(args[0]), num(args[1])];
2244
2120
  }
2245
2121
  function messageRaw(decorator) {
2246
2122
  const args = decorator?.getArguments() ?? [];
2247
2123
  for (const arg of args) {
2248
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2124
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
2249
2125
  for (const prop of arg.getProperties()) {
2250
- if (import_ts_morph5.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2126
+ if (import_ts_morph4.Node.isPropertyAssignment(prop) && prop.getName() === "message") {
2251
2127
  const init = prop.getInitializer();
2252
- if (init && import_ts_morph5.Node.isStringLiteral(init)) return init.getText();
2128
+ if (init && import_ts_morph4.Node.isStringLiteral(init)) return init.getText();
2253
2129
  }
2254
2130
  }
2255
2131
  }
@@ -2259,9 +2135,9 @@ function messageRaw(decorator) {
2259
2135
  function resolveTypeFactoryName(decorator) {
2260
2136
  const arg = firstArg(decorator);
2261
2137
  if (!arg) return null;
2262
- if (import_ts_morph5.Node.isArrowFunction(arg)) {
2138
+ if (import_ts_morph4.Node.isArrowFunction(arg)) {
2263
2139
  const body = arg.getBody();
2264
- if (import_ts_morph5.Node.isIdentifier(body)) return body.getText();
2140
+ if (import_ts_morph4.Node.isIdentifier(body)) return body.getText();
2265
2141
  }
2266
2142
  return null;
2267
2143
  }
@@ -2272,7 +2148,7 @@ function singularClassName(typeText) {
2272
2148
  function enumSchemaFromDecorator(decorator, classFile, ctx) {
2273
2149
  const arg = firstArg(decorator);
2274
2150
  if (!arg) return null;
2275
- if (import_ts_morph5.Node.isIdentifier(arg)) {
2151
+ if (import_ts_morph4.Node.isIdentifier(arg)) {
2276
2152
  const name = arg.getText();
2277
2153
  const resolved = findType(name, classFile, ctx.project);
2278
2154
  if (resolved && resolved.kind === "enum") {
@@ -2286,12 +2162,12 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
2286
2162
  }
2287
2163
  return { kind: "unknown", note: `@IsEnum(${name}): enum not resolvable to literals` };
2288
2164
  }
2289
- if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2165
+ if (import_ts_morph4.Node.isObjectLiteralExpression(arg)) {
2290
2166
  const values = [];
2291
2167
  for (const p of arg.getProperties()) {
2292
- if (!import_ts_morph5.Node.isPropertyAssignment(p)) continue;
2168
+ if (!import_ts_morph4.Node.isPropertyAssignment(p)) continue;
2293
2169
  const init = p.getInitializer();
2294
- if (init && import_ts_morph5.Node.isStringLiteral(init)) values.push(init.getText());
2170
+ if (init && import_ts_morph4.Node.isStringLiteral(init)) values.push(init.getText());
2295
2171
  }
2296
2172
  if (values.length > 0) return { kind: "enum", literals: values };
2297
2173
  }
@@ -2299,9 +2175,9 @@ function enumSchemaFromDecorator(decorator, classFile, ctx) {
2299
2175
  }
2300
2176
  function inSchemaFromDecorator(decorator) {
2301
2177
  const arg = firstArg(decorator);
2302
- if (arg && import_ts_morph5.Node.isArrayLiteralExpression(arg)) {
2178
+ if (arg && import_ts_morph4.Node.isArrayLiteralExpression(arg)) {
2303
2179
  const elements = arg.getElements();
2304
- const allStrings = elements.every((e) => import_ts_morph5.Node.isStringLiteral(e));
2180
+ const allStrings = elements.every((e) => import_ts_morph4.Node.isStringLiteral(e));
2305
2181
  if (allStrings && elements.length > 0) {
2306
2182
  return { kind: "enum", literals: elements.map((e) => e.getText()) };
2307
2183
  }
@@ -2315,427 +2191,99 @@ function inSchemaFromDecorator(decorator) {
2315
2191
  return null;
2316
2192
  }
2317
2193
 
2318
- // src/discovery/dto-to-zod.ts
2194
+ // src/discovery/filter-for.ts
2319
2195
  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
- };
2196
+
2197
+ // src/discovery/filter-field-types.ts
2198
+ var import_ts_morph5 = require("ts-morph");
2199
+
2200
+ // src/discovery/enum-resolution.ts
2201
+ function resolveEnumValues(name, sourceFile, project) {
2202
+ const resolved = findType(name, sourceFile, project);
2203
+ if (!resolved || resolved.kind !== "enum") return null;
2204
+ let numeric = true;
2205
+ const values = resolved.members.map((m) => {
2206
+ const parsed = JSON.parse(m);
2207
+ if (typeof parsed === "string") numeric = false;
2208
+ return String(parsed);
2209
+ });
2210
+ if (values.length === 0) return null;
2211
+ return { values, numeric };
2369
2212
  }
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(", ")} })`;
2213
+
2214
+ // src/discovery/filter-field-types.ts
2215
+ var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
2216
+ var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
2217
+ var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
2218
+ var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
2219
+ var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
2220
+ function classifyTypeKeyword(raw) {
2221
+ const t = raw.toLowerCase();
2222
+ if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
2223
+ if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
2224
+ if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
2225
+ if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
2226
+ if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
2227
+ return null;
2382
2228
  }
2383
- function toObjectKey3(name) {
2384
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2229
+ function markNullable(r, nullable) {
2230
+ return nullable ? { ...r, nullable: true } : r;
2385
2231
  }
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})`);
2232
+ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2233
+ if (import_ts_morph5.Node.isUnionTypeNode(typeNode)) {
2234
+ let nullable = false;
2235
+ const stringLits = [];
2236
+ const numberLits = [];
2237
+ const others = [];
2238
+ for (const member of typeNode.getTypeNodes()) {
2239
+ const kind = member.getKind();
2240
+ if (kind === import_ts_morph5.SyntaxKind.NullKeyword || kind === import_ts_morph5.SyntaxKind.UndefinedKeyword) {
2241
+ nullable = true;
2242
+ continue;
2243
+ }
2244
+ if (import_ts_morph5.Node.isLiteralTypeNode(member)) {
2245
+ const lit = member.getLiteral();
2246
+ if (import_ts_morph5.Node.isStringLiteral(lit)) {
2247
+ stringLits.push(lit.getLiteralValue());
2248
+ continue;
2249
+ }
2250
+ if (import_ts_morph5.Node.isNumericLiteral(lit)) {
2251
+ numberLits.push(lit.getText());
2252
+ continue;
2253
+ }
2254
+ if (lit.getKind() === import_ts_morph5.SyntaxKind.NullKeyword) {
2255
+ nullable = true;
2256
+ continue;
2257
+ }
2258
+ }
2259
+ others.push(member);
2432
2260
  }
2261
+ if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
2262
+ return markNullable({ kind: "string", enumValues: stringLits }, nullable);
2263
+ }
2264
+ if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
2265
+ return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
2266
+ }
2267
+ if (others.length === 1) {
2268
+ const inner = classifyTypeNode(others[0], sourceFile, project, opts);
2269
+ return markNullable(inner, nullable || inner.nullable === true);
2270
+ }
2271
+ return markNullable({ kind: "unknown" }, nullable);
2433
2272
  }
2434
- if (has("MinLength")) {
2435
- const n = numericArg2(dec("MinLength"));
2436
- if (n !== null) refinements.push(`.min(${n})`);
2437
- }
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})`);
2273
+ switch (typeNode.getKind()) {
2274
+ case import_ts_morph5.SyntaxKind.StringKeyword:
2275
+ return { kind: "string" };
2276
+ case import_ts_morph5.SyntaxKind.NumberKeyword:
2277
+ return { kind: "number" };
2278
+ case import_ts_morph5.SyntaxKind.BooleanKeyword:
2279
+ return { kind: "boolean" };
2280
+ case import_ts_morph5.SyntaxKind.AnyKeyword:
2281
+ case import_ts_morph5.SyntaxKind.UnknownKeyword:
2282
+ return { kind: "unknown" };
2283
+ default:
2284
+ break;
2450
2285
  }
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)) {
2286
+ if (import_ts_morph5.Node.isTypeReference(typeNode)) {
2739
2287
  const refName = typeNode.getTypeName().getText();
2740
2288
  if (refName === "Date") return { kind: "date" };
2741
2289
  if (refName === "Record" || refName === "Object") return { kind: "json" };
@@ -2748,25 +2296,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
2748
2296
  if (typeRef) return { kind: "unknown", typeRef };
2749
2297
  return { kind: "unknown" };
2750
2298
  }
2751
- if (import_ts_morph7.Node.isTypeLiteral(typeNode)) return { kind: "json" };
2299
+ if (import_ts_morph5.Node.isTypeLiteral(typeNode)) return { kind: "json" };
2752
2300
  return { kind: "unknown" };
2753
2301
  }
2754
2302
  function enumFromDecoratorArgs(args, sourceFile, project) {
2755
2303
  for (const arg of args) {
2756
- if (import_ts_morph7.Node.isArrowFunction(arg)) {
2304
+ if (import_ts_morph5.Node.isArrowFunction(arg)) {
2757
2305
  const body = arg.getBody();
2758
- if (import_ts_morph7.Node.isIdentifier(body)) {
2306
+ if (import_ts_morph5.Node.isIdentifier(body)) {
2759
2307
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2760
2308
  if (en) return en;
2761
2309
  }
2762
2310
  }
2763
- if (import_ts_morph7.Node.isObjectLiteralExpression(arg)) {
2311
+ if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2764
2312
  const itemsProp = arg.getProperty("items");
2765
- if (itemsProp && import_ts_morph7.Node.isPropertyAssignment(itemsProp)) {
2313
+ if (itemsProp && import_ts_morph5.Node.isPropertyAssignment(itemsProp)) {
2766
2314
  const init = itemsProp.getInitializer();
2767
- if (init && import_ts_morph7.Node.isArrowFunction(init)) {
2315
+ if (init && import_ts_morph5.Node.isArrowFunction(init)) {
2768
2316
  const body = init.getBody();
2769
- if (import_ts_morph7.Node.isIdentifier(body)) {
2317
+ if (import_ts_morph5.Node.isIdentifier(body)) {
2770
2318
  const en = resolveEnumValues(body.getText(), sourceFile, project);
2771
2319
  if (en) return en;
2772
2320
  }
@@ -2789,7 +2337,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2789
2337
  return { kind: "string" };
2790
2338
  }
2791
2339
  for (const arg of args) {
2792
- if (import_ts_morph7.Node.isStringLiteral(arg)) {
2340
+ if (import_ts_morph5.Node.isStringLiteral(arg)) {
2793
2341
  const raw = arg.getLiteralValue();
2794
2342
  const kind = classifyTypeKeyword(raw);
2795
2343
  if (kind) {
@@ -2797,11 +2345,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2797
2345
  return { kind };
2798
2346
  }
2799
2347
  }
2800
- if (import_ts_morph7.Node.isObjectLiteralExpression(arg)) {
2348
+ if (import_ts_morph5.Node.isObjectLiteralExpression(arg)) {
2801
2349
  const enumProp = arg.getProperty("enum");
2802
- if (enumProp && import_ts_morph7.Node.isPropertyAssignment(enumProp)) {
2350
+ if (enumProp && import_ts_morph5.Node.isPropertyAssignment(enumProp)) {
2803
2351
  const init = enumProp.getInitializer();
2804
- if (init && import_ts_morph7.Node.isIdentifier(init)) {
2352
+ if (init && import_ts_morph5.Node.isIdentifier(init)) {
2805
2353
  const en = resolveEnumValues(init.getText(), sourceFile, project);
2806
2354
  if (en) {
2807
2355
  return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
@@ -2810,9 +2358,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
2810
2358
  }
2811
2359
  }
2812
2360
  const typeProp = arg.getProperty("type");
2813
- if (typeProp && import_ts_morph7.Node.isPropertyAssignment(typeProp)) {
2361
+ if (typeProp && import_ts_morph5.Node.isPropertyAssignment(typeProp)) {
2814
2362
  const init = typeProp.getInitializer();
2815
- if (init && import_ts_morph7.Node.isStringLiteral(init)) {
2363
+ if (init && import_ts_morph5.Node.isStringLiteral(init)) {
2816
2364
  const kind = classifyTypeKeyword(init.getLiteralValue());
2817
2365
  if (kind) return { kind };
2818
2366
  }
@@ -2848,7 +2396,7 @@ function toFilterFieldType(name, r) {
2848
2396
 
2849
2397
  // src/discovery/filter-for.ts
2850
2398
  function classifyFilterForHint(typeInit) {
2851
- if (import_ts_morph8.Node.isStringLiteral(typeInit)) {
2399
+ if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
2852
2400
  switch (typeInit.getLiteralValue()) {
2853
2401
  case "string":
2854
2402
  return { kind: "string" };
@@ -2862,10 +2410,10 @@ function classifyFilterForHint(typeInit) {
2862
2410
  return null;
2863
2411
  }
2864
2412
  }
2865
- if (import_ts_morph8.Node.isArrayLiteralExpression(typeInit)) {
2413
+ if (import_ts_morph6.Node.isArrayLiteralExpression(typeInit)) {
2866
2414
  const values = [];
2867
2415
  for (const el of typeInit.getElements()) {
2868
- if (!import_ts_morph8.Node.isStringLiteral(el)) return null;
2416
+ if (!import_ts_morph6.Node.isStringLiteral(el)) return null;
2869
2417
  values.push(el.getLiteralValue());
2870
2418
  }
2871
2419
  if (values.length === 0) return null;
@@ -2900,11 +2448,11 @@ function extractFilterForHints(classDecl, project) {
2900
2448
  if (!filterForDec) continue;
2901
2449
  const args = filterForDec.getArguments();
2902
2450
  const keyArg = args[0];
2903
- const inputKey = keyArg && import_ts_morph8.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2451
+ const inputKey = keyArg && import_ts_morph6.Node.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
2904
2452
  const optsArg = args[1];
2905
- if (optsArg && import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) {
2453
+ if (optsArg && import_ts_morph6.Node.isObjectLiteralExpression(optsArg)) {
2906
2454
  const typeProp = optsArg.getProperty("type");
2907
- if (typeProp && import_ts_morph8.Node.isPropertyAssignment(typeProp)) {
2455
+ if (typeProp && import_ts_morph6.Node.isPropertyAssignment(typeProp)) {
2908
2456
  const typeInit = typeProp.getInitializer();
2909
2457
  if (typeInit) {
2910
2458
  const classified = classifyFilterForHint(typeInit);
@@ -2927,14 +2475,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
2927
2475
  const args = filterDecorator.getArguments();
2928
2476
  if (args.length === 0) continue;
2929
2477
  const filterClassArg = args[0];
2930
- if (!filterClassArg || !import_ts_morph8.Node.isIdentifier(filterClassArg)) continue;
2478
+ if (!filterClassArg || !import_ts_morph6.Node.isIdentifier(filterClassArg)) continue;
2931
2479
  let source = "query";
2932
2480
  const optionsArg = args[1];
2933
- if (optionsArg && import_ts_morph8.Node.isObjectLiteralExpression(optionsArg)) {
2481
+ if (optionsArg && import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) {
2934
2482
  const sourceProp = optionsArg.getProperty("source");
2935
- if (sourceProp && import_ts_morph8.Node.isPropertyAssignment(sourceProp)) {
2483
+ if (sourceProp && import_ts_morph6.Node.isPropertyAssignment(sourceProp)) {
2936
2484
  const init = sourceProp.getInitializer();
2937
- if (init && import_ts_morph8.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
2485
+ if (init && import_ts_morph6.Node.isStringLiteral(init) && init.getLiteralValue() === "body") {
2938
2486
  source = "body";
2939
2487
  }
2940
2488
  }
@@ -2997,22 +2545,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
2997
2545
  const args = dec.getArguments();
2998
2546
  if (args.length === 0) continue;
2999
2547
  const arg = args[0];
3000
- if (import_ts_morph8.Node.isObjectLiteralExpression(arg)) {
2548
+ if (import_ts_morph6.Node.isObjectLiteralExpression(arg)) {
3001
2549
  const entityProp = arg.getProperty("entity");
3002
- if (entityProp && import_ts_morph8.Node.isPropertyAssignment(entityProp)) {
2550
+ if (entityProp && import_ts_morph6.Node.isPropertyAssignment(entityProp)) {
3003
2551
  const init = entityProp.getInitializer();
3004
- if (init && import_ts_morph8.Node.isArrowFunction(init)) {
2552
+ if (init && import_ts_morph6.Node.isArrowFunction(init)) {
3005
2553
  const body = init.getBody();
3006
- if (import_ts_morph8.Node.isIdentifier(body)) {
2554
+ if (import_ts_morph6.Node.isIdentifier(body)) {
3007
2555
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3008
2556
  if (resolved?.kind === "class") return resolved.decl;
3009
2557
  }
3010
2558
  }
3011
2559
  }
3012
2560
  }
3013
- if (import_ts_morph8.Node.isArrowFunction(arg)) {
2561
+ if (import_ts_morph6.Node.isArrowFunction(arg)) {
3014
2562
  const body = arg.getBody();
3015
- if (import_ts_morph8.Node.isIdentifier(body)) {
2563
+ if (import_ts_morph6.Node.isIdentifier(body)) {
3016
2564
  const resolved = findType(body.getText(), prop.getSourceFile(), project);
3017
2565
  if (resolved?.kind === "class") return resolved.decl;
3018
2566
  }
@@ -3036,11 +2584,11 @@ function extractFilterableEntityFields(filterClass, project) {
3036
2584
  const args = filterableDecorator.getArguments();
3037
2585
  if (args.length === 0) return [];
3038
2586
  const optionsArg = args[0];
3039
- if (!import_ts_morph8.Node.isObjectLiteralExpression(optionsArg)) return [];
2587
+ if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
3040
2588
  const entityProp = optionsArg.getProperty("entity");
3041
- if (!entityProp || !import_ts_morph8.Node.isPropertyAssignment(entityProp)) return [];
2589
+ if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return [];
3042
2590
  const entityInit = entityProp.getInitializer();
3043
- if (!entityInit || !import_ts_morph8.Node.isIdentifier(entityInit)) return [];
2591
+ if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
3044
2592
  const entityName = entityInit.getText();
3045
2593
  const filterSourceFile = filterClass.getSourceFile();
3046
2594
  const resolvedEntity = findType(entityName, filterSourceFile, project);
@@ -3056,17 +2604,17 @@ function extractFilterableEntityFields(filterClass, project) {
3056
2604
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
3057
2605
  if (relationsDecorator) {
3058
2606
  const relArgs = relationsDecorator.getArguments();
3059
- if (relArgs.length > 0 && import_ts_morph8.Node.isObjectLiteralExpression(relArgs[0])) {
2607
+ if (relArgs.length > 0 && import_ts_morph6.Node.isObjectLiteralExpression(relArgs[0])) {
3060
2608
  for (const relProp of relArgs[0].getProperties()) {
3061
- if (!import_ts_morph8.Node.isPropertyAssignment(relProp)) continue;
2609
+ if (!import_ts_morph6.Node.isPropertyAssignment(relProp)) continue;
3062
2610
  const relInit = relProp.getInitializer();
3063
- if (!relInit || !import_ts_morph8.Node.isObjectLiteralExpression(relInit)) continue;
2611
+ if (!relInit || !import_ts_morph6.Node.isObjectLiteralExpression(relInit)) continue;
3064
2612
  const keysProp = relInit.getProperty("keys");
3065
- if (!keysProp || !import_ts_morph8.Node.isPropertyAssignment(keysProp)) continue;
2613
+ if (!keysProp || !import_ts_morph6.Node.isPropertyAssignment(keysProp)) continue;
3066
2614
  const keysInit = keysProp.getInitializer();
3067
- if (!keysInit || !import_ts_morph8.Node.isArrayLiteralExpression(keysInit)) continue;
2615
+ if (!keysInit || !import_ts_morph6.Node.isArrayLiteralExpression(keysInit)) continue;
3068
2616
  for (const el of keysInit.getElements()) {
3069
- if (import_ts_morph8.Node.isStringLiteral(el)) {
2617
+ if (import_ts_morph6.Node.isStringLiteral(el)) {
3070
2618
  fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
3071
2619
  }
3072
2620
  }
@@ -3076,267 +2624,65 @@ function extractFilterableEntityFields(filterClass, project) {
3076
2624
  return fields;
3077
2625
  }
3078
2626
 
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
- });
2627
+ // src/discovery/dto-type-resolver.ts
2628
+ var WRAPPER_TYPES = {
2629
+ // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
2630
+ // wrappers around related entities; the wire shape is just the referenced
2631
+ // entity. Unwrap to the type argument.
2632
+ Ref: "unwrap",
2633
+ Reference: "unwrap",
2634
+ LoadedReference: "unwrap",
2635
+ IdentifiedReference: "unwrap",
2636
+ // MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
2637
+ Opt: "unwrap",
2638
+ Loaded: "unwrap",
2639
+ // Promise<T> — unwrap
2640
+ Promise: "unwrap",
2641
+ // MikroORM Collection<T> serializes as an array of T on the wire.
2642
+ Collection: "arrayOf",
2643
+ // Array<T> generic form
2644
+ Array: "arrayOf"
2645
+ };
2646
+ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2647
+ "Record",
2648
+ "Omit",
2649
+ "Pick",
2650
+ "Partial",
2651
+ "Required",
2652
+ "Readonly",
2653
+ "Map",
2654
+ "Set"
2655
+ ]);
2656
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
2657
+ if (depth <= 0) return "unknown";
2658
+ if (import_ts_morph7.Node.isArrayTypeNode(typeNode)) {
2659
+ const elementType = typeNode.getElementTypeNode();
2660
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
3102
2661
  }
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);
2662
+ if (import_ts_morph7.Node.isUnionTypeNode(typeNode)) {
2663
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
3106
2664
  }
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);
2665
+ if (import_ts_morph7.Node.isIntersectionTypeNode(typeNode)) {
2666
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
3118
2667
  }
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`;
2668
+ if (import_ts_morph7.Node.isParenthesizedTypeNode(typeNode)) {
2669
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
2670
+ }
2671
+ if (import_ts_morph7.Node.isTypeReference(typeNode)) {
2672
+ const typeName = typeNode.getTypeName();
2673
+ const name = import_ts_morph7.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2674
+ if (name === "string" || name === "number" || name === "boolean") return name;
2675
+ if (name === "Date") return "string";
2676
+ if (name === "unknown" || name === "any" || name === "void") return "unknown";
2677
+ if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
2678
+ return "unknown";
2679
+ const wrapperMode = WRAPPER_TYPES[name];
2680
+ if (wrapperMode) {
2681
+ return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
3129
2682
  }
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)) {
3287
- return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
3288
- }
3289
- if (import_ts_morph9.Node.isTypeReference(typeNode)) {
3290
- const typeName = typeNode.getTypeName();
3291
- const name = import_ts_morph9.Node.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
3292
- if (name === "string" || name === "number" || name === "boolean") return name;
3293
- if (name === "Date") return "string";
3294
- if (name === "unknown" || name === "any" || name === "void") return "unknown";
3295
- if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
3296
- 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";
3304
- }
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)) {
2683
+ if (PASSTHROUGH_UTILITY.has(name)) {
3330
2684
  return typeNode.getText();
3331
2685
  }
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
2686
  const resolved = findType(name, sourceFile, project);
3341
2687
  if (resolved) {
3342
2688
  return expandTypeDecl(resolved, project, depth - 1);
@@ -3345,13 +2691,22 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
3345
2691
  return "unknown";
3346
2692
  }
3347
2693
  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";
2694
+ if (kind === import_ts_morph7.SyntaxKind.StringKeyword) return "string";
2695
+ if (kind === import_ts_morph7.SyntaxKind.NumberKeyword) return "number";
2696
+ if (kind === import_ts_morph7.SyntaxKind.BooleanKeyword) return "boolean";
2697
+ if (kind === import_ts_morph7.SyntaxKind.UnknownKeyword) return "unknown";
2698
+ if (kind === import_ts_morph7.SyntaxKind.AnyKeyword) return "unknown";
3353
2699
  return typeNode.getText();
3354
2700
  }
2701
+ function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
2702
+ const typeArgs = typeNode.getTypeArguments();
2703
+ const firstTypeArg = typeArgs[0];
2704
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
2705
+ const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
2706
+ return mode === "arrayOf" ? `Array<${inner}>` : inner;
2707
+ }
2708
+ return mode === "arrayOf" ? "Array<unknown>" : "unknown";
2709
+ }
3355
2710
  function expandTypeDecl(result, project, depth) {
3356
2711
  if (depth < 0) return "unknown";
3357
2712
  switch (result.kind) {
@@ -3417,7 +2772,7 @@ function extractParamsType(method, sourceFile, project) {
3417
2772
  const paramArgs = paramDecorator.getArguments();
3418
2773
  if (paramArgs.length === 0) continue;
3419
2774
  const nameArg = paramArgs[0];
3420
- if (!import_ts_morph9.Node.isStringLiteral(nameArg)) continue;
2775
+ if (!import_ts_morph7.Node.isStringLiteral(nameArg)) continue;
3421
2776
  const paramName = nameArg.getLiteralValue();
3422
2777
  const typeNode = param.getTypeNode();
3423
2778
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -3430,13 +2785,13 @@ function extractResponseType(method, sourceFile, project) {
3430
2785
  if (apiResponseDecorator) {
3431
2786
  const args = apiResponseDecorator.getArguments();
3432
2787
  const optsArg = args[0];
3433
- if (optsArg && import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) {
2788
+ if (optsArg && import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) {
3434
2789
  for (const prop of optsArg.getProperties()) {
3435
- if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
2790
+ if (!import_ts_morph7.Node.isPropertyAssignment(prop)) continue;
3436
2791
  if (prop.getName() !== "type") continue;
3437
2792
  const val = prop.getInitializer();
3438
2793
  if (!val) continue;
3439
- if (import_ts_morph9.Node.isArrayLiteralExpression(val)) {
2794
+ if (import_ts_morph7.Node.isArrayLiteralExpression(val)) {
3440
2795
  const elements = val.getElements();
3441
2796
  const firstEl = elements[0];
3442
2797
  if (elements.length > 0 && firstEl !== void 0) {
@@ -3456,7 +2811,7 @@ function extractResponseType(method, sourceFile, project) {
3456
2811
  return "unknown";
3457
2812
  }
3458
2813
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
3459
- if (!import_ts_morph9.Node.isIdentifier(node)) return "unknown";
2814
+ if (!import_ts_morph7.Node.isIdentifier(node)) return "unknown";
3460
2815
  const name = node.getText();
3461
2816
  const resolved = findType(name, sourceFile, project);
3462
2817
  if (resolved) {
@@ -3503,11 +2858,11 @@ function extractDtoContract(method, sourceFile, project) {
3503
2858
  if (apiResp) {
3504
2859
  const args = apiResp.getArguments();
3505
2860
  const optsArg = args[0];
3506
- if (optsArg && import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) {
2861
+ if (optsArg && import_ts_morph7.Node.isObjectLiteralExpression(optsArg)) {
3507
2862
  for (const prop of optsArg.getProperties()) {
3508
- if (import_ts_morph9.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
2863
+ if (import_ts_morph7.Node.isPropertyAssignment(prop) && prop.getName() === "type") {
3509
2864
  const val = prop.getInitializer();
3510
- if (val && import_ts_morph9.Node.isIdentifier(val)) {
2865
+ if (val && import_ts_morph7.Node.isIdentifier(val)) {
3511
2866
  const name = val.getText();
3512
2867
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
3513
2868
  if (localDecl?.isExported()) {
@@ -3524,60 +2879,243 @@ function extractDtoContract(method, sourceFile, project) {
3524
2879
  }
3525
2880
  }
3526
2881
  }
3527
- let bodyZodText = null;
3528
- let queryZodText = null;
3529
2882
  let bodySchema = null;
3530
2883
  let querySchema = null;
3531
- const formNested = {};
3532
2884
  const formWarnings = [];
3533
2885
  const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
3534
2886
  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
2887
  bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2888
+ formWarnings.push(...bodySchema.warnings);
2889
+ }
2890
+ const queryClass = resolveParamClass(method, "Query", sourceFile, project);
2891
+ if (queryClass) {
2892
+ querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
2893
+ formWarnings.push(...querySchema.warnings);
2894
+ }
2895
+ return {
2896
+ query,
2897
+ body,
2898
+ response,
2899
+ params: paramsType,
2900
+ queryRef,
2901
+ bodyRef,
2902
+ responseRef,
2903
+ filterFields: filterInfo?.fieldNames ?? null,
2904
+ filterFieldTypes: filterInfo?.fieldTypes ?? null,
2905
+ filterSource: filterInfo?.source ?? null,
2906
+ formWarnings,
2907
+ bodySchema,
2908
+ querySchema
2909
+ };
2910
+ }
2911
+ function resolveParamClass(method, decoratorName, sourceFile, project) {
2912
+ for (const param of method.getParameters()) {
2913
+ if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
2914
+ const typeNode = param.getTypeNode();
2915
+ if (!typeNode) continue;
2916
+ const text = typeNode.getText().replace(/\[\]$/, "");
2917
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
2918
+ const resolved = findType(text, sourceFile, project);
2919
+ if (resolved && resolved.kind === "class") {
2920
+ return { decl: resolved.decl, file: resolved.file };
2921
+ }
2922
+ }
2923
+ return null;
2924
+ }
2925
+
2926
+ // src/discovery/zod-ast-to-ts.ts
2927
+ var import_ts_morph8 = require("ts-morph");
2928
+ function zodAstToTs(node) {
2929
+ if (!import_ts_morph8.Node.isCallExpression(node)) return "unknown";
2930
+ const expr = node.getExpression();
2931
+ if (import_ts_morph8.Node.isPropertyAccessExpression(expr)) {
2932
+ const methodName = expr.getName();
2933
+ const receiver = expr.getExpression();
2934
+ if (methodName === "optional") {
2935
+ return `${zodAstToTs(receiver)} | undefined`;
2936
+ }
2937
+ if (methodName === "nullable") {
2938
+ return `${zodAstToTs(receiver)} | null`;
2939
+ }
2940
+ const args = node.getArguments();
2941
+ switch (methodName) {
2942
+ case "string":
2943
+ return "string";
2944
+ case "number":
2945
+ return "number";
2946
+ case "boolean":
2947
+ return "boolean";
2948
+ case "unknown":
2949
+ return "unknown";
2950
+ case "any":
2951
+ return "unknown";
2952
+ case "literal": {
2953
+ const lit = args[0];
2954
+ if (!lit) return "unknown";
2955
+ if (import_ts_morph8.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2956
+ if (import_ts_morph8.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2957
+ if (lit.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword) return "true";
2958
+ if (lit.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) return "false";
2959
+ return "unknown";
2960
+ }
2961
+ case "enum": {
2962
+ const arrArg = args[0];
2963
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
2964
+ const members = arrArg.getElements().map(
2965
+ (el) => import_ts_morph8.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2966
+ );
2967
+ return members.join(" | ");
2968
+ }
2969
+ case "array": {
2970
+ const inner = args[0];
2971
+ if (!inner) return "unknown";
2972
+ return `Array<${zodAstToTs(inner)}>`;
2973
+ }
2974
+ case "object": {
2975
+ const objArg = args[0];
2976
+ if (!objArg || !import_ts_morph8.Node.isObjectLiteralExpression(objArg)) return "unknown";
2977
+ const lines = [];
2978
+ for (const prop of objArg.getProperties()) {
2979
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
2980
+ const key = prop.getName();
2981
+ const valNode = prop.getInitializer();
2982
+ if (!valNode) continue;
2983
+ const tsType = zodAstToTs(valNode);
2984
+ const isOpt = isOptionalChain(valNode);
2985
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
2986
+ }
2987
+ return `{ ${lines.join("; ")} }`;
2988
+ }
2989
+ case "union": {
2990
+ const arrArg = args[0];
2991
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
2992
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
2993
+ }
2994
+ case "record": {
2995
+ const valArg = args.length === 1 ? args[0] : args[1];
2996
+ if (!valArg) return "unknown";
2997
+ return `Record<string, ${zodAstToTs(valArg)}>`;
2998
+ }
2999
+ case "tuple": {
3000
+ const arrArg = args[0];
3001
+ if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
3002
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
3003
+ }
3004
+ default:
3005
+ return "unknown";
3006
+ }
3007
+ }
3008
+ return "unknown";
3009
+ }
3010
+ function isOptionalChain(node) {
3011
+ if (!import_ts_morph8.Node.isCallExpression(node)) return false;
3012
+ const expr = node.getExpression();
3013
+ return import_ts_morph8.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
3014
+ }
3015
+ function parseDefineContractCall(callExpr) {
3016
+ if (!import_ts_morph8.Node.isCallExpression(callExpr)) return null;
3017
+ const callee = callExpr.getExpression();
3018
+ const calleeName = import_ts_morph8.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph8.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
3019
+ if (calleeName !== "defineContract") return null;
3020
+ const args = callExpr.getArguments();
3021
+ const optsArg = args[0];
3022
+ if (!optsArg || !import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) return null;
3023
+ let query = null;
3024
+ let body = null;
3025
+ let response = "unknown";
3026
+ let bodyZodText = null;
3027
+ let queryZodText = null;
3028
+ for (const prop of optsArg.getProperties()) {
3029
+ if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
3030
+ const propName = prop.getName();
3031
+ const val = prop.getInitializer();
3032
+ if (!val) continue;
3033
+ if (propName === "query") {
3034
+ query = zodAstToTs(val);
3035
+ queryZodText = val.getText();
3036
+ } else if (propName === "body") {
3037
+ body = zodAstToTs(val);
3038
+ bodyZodText = val.getText();
3039
+ } else if (propName === "response") {
3040
+ response = zodAstToTs(val);
3041
+ }
3042
+ }
3043
+ return { query, body, response, bodyZodText, queryZodText };
3044
+ }
3045
+
3046
+ // src/discovery/contracts-fast.ts
3047
+ async function discoverContractsFast(opts) {
3048
+ const { cwd, glob, tsconfig } = opts;
3049
+ const tsconfigPath = tsconfig ? (0, import_node_path12.resolve)(tsconfig) : (0, import_node_path12.join)(cwd, "tsconfig.json");
3050
+ let project;
3051
+ try {
3052
+ project = new import_ts_morph9.Project({
3053
+ tsConfigFilePath: tsconfigPath,
3054
+ skipAddingFilesFromTsConfig: true,
3055
+ skipLoadingLibFiles: true,
3056
+ skipFileDependencyResolution: true
3057
+ });
3058
+ } catch {
3059
+ project = new import_ts_morph9.Project({
3060
+ skipAddingFilesFromTsConfig: true,
3061
+ skipLoadingLibFiles: true,
3062
+ skipFileDependencyResolution: true,
3063
+ compilerOptions: {
3064
+ allowJs: true,
3065
+ resolveJsonModule: false,
3066
+ strict: false
3067
+ }
3068
+ });
3069
+ }
3070
+ const files = await (0, import_fast_glob2.default)(glob, { cwd, absolute: true, onlyFiles: true });
3071
+ for (const f of files) {
3072
+ project.addSourceFileAtPath(f);
3540
3073
  }
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);
3074
+ const routes = [];
3075
+ setDiscoveryContext(project, {
3076
+ projectRoot: cwd,
3077
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
3078
+ });
3079
+ for (const sourceFile of project.getSourceFiles()) {
3080
+ routes.push(...extractFromSourceFile(sourceFile, project));
3548
3081
  }
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
- };
3082
+ return routes;
3567
3083
  }
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
- }
3084
+ function decoratorStringArg(decoratorExpr) {
3085
+ if (!decoratorExpr) return void 0;
3086
+ if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3087
+ if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
3088
+ const first = decoratorExpr.getElements()[0];
3089
+ if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
3579
3090
  }
3580
- return null;
3091
+ return void 0;
3092
+ }
3093
+ function deriveClassSegment(className) {
3094
+ const noSuffix = className.replace(/Controller$/, "");
3095
+ if (!noSuffix) {
3096
+ throw new Error(
3097
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
3098
+ );
3099
+ }
3100
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
3101
+ }
3102
+ function resolveRouteName(className, methodName, classAs, methodAs) {
3103
+ const classPortion = classAs ?? deriveClassSegment(className);
3104
+ const methodPortion = methodAs ?? methodName;
3105
+ return `${classPortion}.${methodPortion}`;
3106
+ }
3107
+ function joinPaths(prefix, suffix) {
3108
+ if (!prefix && !suffix) return "/";
3109
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
3110
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
3111
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
3112
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
3113
+ const combined = p + s;
3114
+ return combined === "" ? "/" : combined;
3115
+ }
3116
+ function extractParams(path) {
3117
+ const matches = path.matchAll(/:(\w+)/g);
3118
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
3581
3119
  }
3582
3120
  var HTTP_METHOD_DECORATORS = {
3583
3121
  Get: "GET",
@@ -3589,176 +3127,186 @@ var HTTP_METHOD_DECORATORS = {
3589
3127
  Head: "HEAD",
3590
3128
  All: "ALL"
3591
3129
  };
3130
+ function resolveVerb(method) {
3131
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
3132
+ const httpDecorator = method.getDecorator(decoratorName);
3133
+ if (httpDecorator) {
3134
+ const httpArgs = httpDecorator.getArguments();
3135
+ const pathArg = httpArgs[0];
3136
+ return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
3137
+ }
3138
+ }
3139
+ return null;
3140
+ }
3141
+ function readAsDecorator(node, label) {
3142
+ const asDecorator = node.getDecorator("As");
3143
+ if (!asDecorator) return void 0;
3144
+ const asName = decoratorStringArg(asDecorator.getArguments()[0]);
3145
+ if (!asName) {
3146
+ throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
3147
+ }
3148
+ return asName;
3149
+ }
3150
+ function buildRoute(args) {
3151
+ const {
3152
+ className,
3153
+ methodName,
3154
+ resolvedMethod,
3155
+ combinedPath,
3156
+ classAs,
3157
+ methodAs,
3158
+ sourceFile,
3159
+ seenNames,
3160
+ contractSource
3161
+ } = args;
3162
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
3163
+ const qualifiedRef = `${className}.${methodName}`;
3164
+ const existing = seenNames.get(routeName);
3165
+ if (existing !== void 0) {
3166
+ throw new Error(
3167
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
3168
+ );
3169
+ }
3170
+ seenNames.set(routeName, qualifiedRef);
3171
+ return {
3172
+ method: resolvedMethod,
3173
+ path: combinedPath,
3174
+ name: routeName,
3175
+ params: extractParams(combinedPath),
3176
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
3177
+ contract: { contractSource }
3178
+ };
3179
+ }
3180
+ function extractContractRoute(args) {
3181
+ const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
3182
+ const firstDecoratorArg = applyContractDecorator.getArguments()[0];
3183
+ if (!firstDecoratorArg) return null;
3184
+ let contractDef = null;
3185
+ let bodyZodRef = null;
3186
+ let queryZodRef = null;
3187
+ if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
3188
+ contractDef = parseDefineContractCall(firstDecoratorArg);
3189
+ } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
3190
+ const identName = firstDecoratorArg.getText();
3191
+ const varDecl = sourceFile.getVariableDeclaration(identName);
3192
+ if (!varDecl) {
3193
+ console.warn(
3194
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
3195
+ );
3196
+ return null;
3197
+ }
3198
+ const initializer = varDecl.getInitializer();
3199
+ if (!initializer) return null;
3200
+ contractDef = parseDefineContractCall(initializer);
3201
+ if (contractDef && varDecl.isExported()) {
3202
+ const filePath = sourceFile.getFilePath();
3203
+ if (contractDef.body !== null) {
3204
+ bodyZodRef = { name: `${identName}.body`, filePath };
3205
+ }
3206
+ if (contractDef.query !== null) {
3207
+ queryZodRef = { name: `${identName}.query`, filePath };
3208
+ }
3209
+ }
3210
+ } else {
3211
+ console.warn(
3212
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
3213
+ );
3214
+ return null;
3215
+ }
3216
+ if (!contractDef) return null;
3217
+ if (!verb) return null;
3218
+ const resolvedPath = joinPaths(prefix, verb.handlerPath);
3219
+ const methodName = method.getName();
3220
+ const classAs = readAsDecorator(cls, `class ${className}`);
3221
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3222
+ return buildRoute({
3223
+ className,
3224
+ methodName,
3225
+ resolvedMethod: verb.httpMethod,
3226
+ combinedPath: resolvedPath,
3227
+ classAs,
3228
+ methodAs,
3229
+ sourceFile,
3230
+ seenNames,
3231
+ contractSource: {
3232
+ query: contractDef.query,
3233
+ body: contractDef.body,
3234
+ response: contractDef.response,
3235
+ // Path A: capture both the importable ref and the raw text. The emitter
3236
+ // prefers inlining the text (client-safe — re-exporting from a controller
3237
+ // would drag server-only deps into the client bundle).
3238
+ bodyZodRef,
3239
+ bodyZodText: contractDef.bodyZodText,
3240
+ queryZodRef,
3241
+ queryZodText: contractDef.queryZodText
3242
+ }
3243
+ });
3244
+ }
3245
+ function extractDtoRoute(args) {
3246
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
3247
+ if (!verb) return null;
3248
+ const combined = joinPaths(prefix, verb.handlerPath);
3249
+ const methodName = method.getName();
3250
+ const classAs = readAsDecorator(cls, `class ${className}`);
3251
+ const methodAs = readAsDecorator(method, `${className}.${methodName}`);
3252
+ const dtoContract = extractDtoContract(method, sourceFile, project);
3253
+ return buildRoute({
3254
+ className,
3255
+ methodName,
3256
+ resolvedMethod: verb.httpMethod,
3257
+ combinedPath: combined,
3258
+ classAs,
3259
+ methodAs,
3260
+ sourceFile,
3261
+ seenNames,
3262
+ contractSource: {
3263
+ query: dtoContract?.query ?? null,
3264
+ body: dtoContract?.body ?? null,
3265
+ response: dtoContract?.response ?? "unknown",
3266
+ queryRef: dtoContract?.queryRef ?? null,
3267
+ bodyRef: dtoContract?.bodyRef ?? null,
3268
+ responseRef: dtoContract?.responseRef ?? null,
3269
+ filterFields: dtoContract?.filterFields ?? null,
3270
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
3271
+ filterSource: dtoContract?.filterSource ?? null,
3272
+ formWarnings: dtoContract?.formWarnings ?? [],
3273
+ bodySchema: dtoContract?.bodySchema ?? null,
3274
+ querySchema: dtoContract?.querySchema ?? null
3275
+ }
3276
+ });
3277
+ }
3592
3278
  function extractFromSourceFile(sourceFile, project) {
3593
3279
  const routes = [];
3594
3280
  const seenNames = /* @__PURE__ */ new Map();
3595
- const classes = sourceFile.getClasses();
3596
- for (const cls of classes) {
3281
+ for (const cls of sourceFile.getClasses()) {
3597
3282
  const controllerDecorator = cls.getDecorator("Controller");
3598
3283
  if (!controllerDecorator) continue;
3599
- const controllerArgs = controllerDecorator.getArguments();
3600
- const firstArg3 = controllerArgs[0];
3601
- const prefix = decoratorStringArg(firstArg3) ?? "";
3284
+ const firstArg2 = controllerDecorator.getArguments()[0];
3285
+ const prefix = decoratorStringArg(firstArg2) ?? "";
3602
3286
  const className = cls.getName() ?? "Unknown";
3603
3287
  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
- }
3288
+ const verb = resolveVerb(method);
3616
3289
  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
- }
3290
+ const route = applyContractDecorator ? extractContractRoute({
3291
+ cls,
3292
+ method,
3293
+ applyContractDecorator,
3294
+ verb,
3295
+ prefix,
3296
+ className,
3297
+ sourceFile,
3298
+ seenNames
3299
+ }) : extractDtoRoute({
3300
+ cls,
3301
+ method,
3302
+ verb,
3303
+ prefix,
3304
+ className,
3305
+ sourceFile,
3306
+ project,
3307
+ seenNames
3308
+ });
3309
+ if (route) routes.push(route);
3762
3310
  }
3763
3311
  }
3764
3312
  return routes;
@@ -3767,7 +3315,7 @@ function extractFromSourceFile(sourceFile, project) {
3767
3315
  // src/watch/lock-file.ts
3768
3316
  var import_promises10 = require("fs/promises");
3769
3317
  var import_promises11 = require("fs/promises");
3770
- var import_node_path12 = require("path");
3318
+ var import_node_path13 = require("path");
3771
3319
  var LOCK_FILE = ".watcher.lock";
3772
3320
  function isProcessAlive(pid) {
3773
3321
  try {
@@ -3779,7 +3327,7 @@ function isProcessAlive(pid) {
3779
3327
  }
3780
3328
  async function acquireLock(outDir) {
3781
3329
  await (0, import_promises11.mkdir)(outDir, { recursive: true });
3782
- const lockPath = (0, import_node_path12.join)(outDir, LOCK_FILE);
3330
+ const lockPath = (0, import_node_path13.join)(outDir, LOCK_FILE);
3783
3331
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3784
3332
  try {
3785
3333
  const fd = await (0, import_promises10.open)(lockPath, "wx");
@@ -3819,7 +3367,7 @@ async function watch(config, onChange) {
3819
3367
  if (lock === null) {
3820
3368
  let holderPid = "unknown";
3821
3369
  try {
3822
- const raw = await (0, import_promises12.readFile)((0, import_node_path13.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3370
+ const raw = await (0, import_promises12.readFile)((0, import_node_path14.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
3823
3371
  const data = JSON.parse(raw);
3824
3372
  if (data.pid !== void 0) holderPid = String(data.pid);
3825
3373
  } catch {
@@ -3847,7 +3395,7 @@ async function watch(config, onChange) {
3847
3395
  }
3848
3396
  let pagesDebounceTimer;
3849
3397
  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), {
3398
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, pagesGlob), {
3851
3399
  ignoreInitial: true,
3852
3400
  persistent: true,
3853
3401
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3873,7 +3421,7 @@ async function watch(config, onChange) {
3873
3421
  pagesWatcher.on("change", schedulePagesRegenerate);
3874
3422
  pagesWatcher.on("unlink", schedulePagesRegenerate);
3875
3423
  let contractsDebounceTimer;
3876
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.contracts.glob), {
3424
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.contracts.glob), {
3877
3425
  ignoreInitial: true,
3878
3426
  persistent: true,
3879
3427
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3903,7 +3451,7 @@ async function watch(config, onChange) {
3903
3451
  contractsWatcher.on("add", scheduleContractsRegenerate);
3904
3452
  contractsWatcher.on("change", scheduleContractsRegenerate);
3905
3453
  contractsWatcher.on("unlink", scheduleContractsRegenerate);
3906
- const formsWatcher = import_chokidar.default.watch((0, import_node_path13.join)(config.codegen.cwd, config.forms.watch), {
3454
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path14.join)(config.codegen.cwd, config.forms.watch), {
3907
3455
  ignoreInitial: true,
3908
3456
  persistent: true,
3909
3457
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -3930,7 +3478,7 @@ async function watch(config, onChange) {
3930
3478
  }
3931
3479
 
3932
3480
  // src/index.ts
3933
- var VERSION = "0.2.0";
3481
+ var VERSION = "0.3.0";
3934
3482
 
3935
3483
  // src/cli/codegen.ts
3936
3484
  async function runCodegen(opts = {}) {
@@ -3958,15 +3506,51 @@ async function runCodegen(opts = {}) {
3958
3506
 
3959
3507
  // src/cli/doctor.ts
3960
3508
  var import_node_child_process2 = require("child_process");
3961
- var import_node_fs3 = require("fs");
3962
- var import_node_path15 = require("path");
3509
+ var import_node_fs4 = require("fs");
3510
+ var import_node_path16 = require("path");
3963
3511
 
3964
3512
  // src/cli/init.ts
3965
3513
  var import_node_child_process = require("child_process");
3966
- var import_node_fs2 = require("fs");
3514
+ var import_node_fs3 = require("fs");
3967
3515
  var import_promises13 = require("fs/promises");
3968
- var import_node_path14 = require("path");
3516
+ var import_node_path15 = require("path");
3969
3517
  var import_node_readline = require("readline");
3518
+
3519
+ // src/cli/patch-utils.ts
3520
+ var import_node_fs2 = require("fs");
3521
+ function patchJsonFile(filePath, mutator, parse = (raw) => raw) {
3522
+ let raw;
3523
+ try {
3524
+ raw = (0, import_node_fs2.readFileSync)(filePath, "utf8");
3525
+ } catch {
3526
+ return "skipped";
3527
+ }
3528
+ const json = JSON.parse(parse(raw));
3529
+ if (!mutator(json)) return "already";
3530
+ (0, import_node_fs2.writeFileSync)(filePath, `${JSON.stringify(json, null, 2)}
3531
+ `, "utf8");
3532
+ return "patched";
3533
+ }
3534
+ function findAfterLastImport(content) {
3535
+ const lastImportIndex = content.lastIndexOf("\nimport ");
3536
+ if (lastImportIndex !== -1) {
3537
+ const endOfLine = content.indexOf("\n", lastImportIndex + 1);
3538
+ return endOfLine !== -1 ? endOfLine + 1 : content.length;
3539
+ }
3540
+ if (content.startsWith("import ")) {
3541
+ const endOfLine = content.indexOf("\n");
3542
+ return endOfLine !== -1 ? endOfLine + 1 : content.length;
3543
+ }
3544
+ return 0;
3545
+ }
3546
+ function insertImport(content, stmt) {
3547
+ const insertAt = findAfterLastImport(content);
3548
+ if (insertAt <= 0) return content;
3549
+ return `${content.slice(0, insertAt)}${stmt}
3550
+ ${content.slice(insertAt)}`;
3551
+ }
3552
+
3553
+ // src/cli/init.ts
3970
3554
  var GITIGNORE_ENTRY = ".nestjs-inertia/";
3971
3555
  var green = (s) => `\x1B[32m${s}\x1B[0m`;
3972
3556
  var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
@@ -3991,7 +3575,7 @@ ${bold(title)}`);
3991
3575
  }
3992
3576
  async function readPackageJson(cwd) {
3993
3577
  try {
3994
- const raw = await (0, import_promises13.readFile)((0, import_node_path14.join)(cwd, "package.json"), "utf8");
3578
+ const raw = await (0, import_promises13.readFile)((0, import_node_path15.join)(cwd, "package.json"), "utf8");
3995
3579
  return JSON.parse(raw);
3996
3580
  } catch {
3997
3581
  return {};
@@ -4022,7 +3606,7 @@ async function detectTemplateEngine(cwd) {
4022
3606
  async function detectPackageManager(cwd) {
4023
3607
  async function exists(file) {
4024
3608
  try {
4025
- await (0, import_promises13.access)((0, import_node_path14.join)(cwd, file));
3609
+ await (0, import_promises13.access)((0, import_node_path15.join)(cwd, file));
4026
3610
  return true;
4027
3611
  } catch {
4028
3612
  return false;
@@ -4069,7 +3653,7 @@ async function writeIfNotExists(filePath, content, label) {
4069
3653
  logCreated(label);
4070
3654
  }
4071
3655
  async function handleViteConfig(cwd, framework) {
4072
- const filePath = (0, import_node_path14.join)(cwd, "vite.config.ts");
3656
+ const filePath = (0, import_node_path15.join)(cwd, "vite.config.ts");
4073
3657
  if (await fileExists2(filePath)) {
4074
3658
  const existing = await (0, import_promises13.readFile)(filePath, "utf8");
4075
3659
  const hasPlugin = existing.includes("nestInertia") || existing.includes("nestjs-inertia-vite/plugin");
@@ -4127,7 +3711,7 @@ function installDeps(pkgManager, deps, dev) {
4127
3711
  }
4128
3712
  }
4129
3713
  async function patchPackageJsonScripts(cwd, scripts) {
4130
- const pkgPath = (0, import_node_path14.join)(cwd, "package.json");
3714
+ const pkgPath = (0, import_node_path15.join)(cwd, "package.json");
4131
3715
  let pkg = {};
4132
3716
  try {
4133
3717
  pkg = JSON.parse(await (0, import_promises13.readFile)(pkgPath, "utf8"));
@@ -4152,32 +3736,16 @@ async function patchPackageJsonScripts(cwd, scripts) {
4152
3736
  await (0, import_promises13.writeFile)(pkgPath, `${JSON.stringify(pkg, null, 2)}
4153
3737
  `, "utf8");
4154
3738
  }
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
3739
  function patchAppModule(filePath, rootView) {
4168
3740
  let content;
4169
3741
  try {
4170
- content = (0, import_node_fs2.readFileSync)(filePath, "utf8");
3742
+ content = (0, import_node_fs3.readFileSync)(filePath, "utf8");
4171
3743
  } catch {
4172
3744
  return "skipped";
4173
3745
  }
4174
3746
  let changed = false;
4175
3747
  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
- }
3748
+ content = insertImport(content, "import { InertiaModule } from '@dudousxd/nestjs-inertia';");
4181
3749
  if (!content.includes("from 'node:path'") && !content.includes('from "node:path"')) {
4182
3750
  const insertAt2 = findAfterLastImport(content);
4183
3751
  content = `${content.slice(0, insertAt2)}import { resolve } from 'node:path';
@@ -4195,11 +3763,7 @@ ${indent}}),${content.slice(bracketPos)}`;
4195
3763
  }
4196
3764
  }
4197
3765
  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
- }
3766
+ content = insertImport(content, "import { HomeController } from './home.controller';");
4203
3767
  const controllersMatch = content.match(/controllers\s*:\s*\[/);
4204
3768
  if (controllersMatch?.index !== void 0) {
4205
3769
  const bracketPos = content.indexOf("[", controllersMatch.index) + 1;
@@ -4210,22 +3774,21 @@ ${indent}HomeController,${content.slice(bracketPos)}`;
4210
3774
  }
4211
3775
  }
4212
3776
  if (!changed) return "already";
4213
- (0, import_node_fs2.writeFileSync)(filePath, content, "utf8");
3777
+ (0, import_node_fs3.writeFileSync)(filePath, content, "utf8");
4214
3778
  return "patched";
4215
3779
  }
4216
3780
  function patchMainTs(filePath) {
4217
3781
  let content;
4218
3782
  try {
4219
- content = (0, import_node_fs2.readFileSync)(filePath, "utf8");
3783
+ content = (0, import_node_fs3.readFileSync)(filePath, "utf8");
4220
3784
  } catch {
4221
3785
  return "skipped";
4222
3786
  }
4223
3787
  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
- }
3788
+ content = insertImport(
3789
+ content,
3790
+ "import { setupInertiaVite } from '@dudousxd/nestjs-inertia-vite';"
3791
+ );
4229
3792
  const createMatch = content.match(
4230
3793
  /(?:const|let)\s+(\w+)\s*=\s*await\s+NestFactory\.create[^;]+;/
4231
3794
  );
@@ -4242,7 +3805,7 @@ ${content.slice(insertAt)}`;
4242
3805
  });`;
4243
3806
  content = `${content.slice(0, insertAfterPos)}
4244
3807
  ${viteSetup}${content.slice(insertAfterPos)}`;
4245
- (0, import_node_fs2.writeFileSync)(filePath, content, "utf8");
3808
+ (0, import_node_fs3.writeFileSync)(filePath, content, "utf8");
4246
3809
  return "patched";
4247
3810
  }
4248
3811
  function configTemplate(framework) {
@@ -4465,107 +4028,84 @@ export class HomeController {
4465
4028
  }
4466
4029
  `;
4467
4030
  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";
4031
+ const filePath = (0, import_node_path15.join)(cwd, filename);
4032
+ return patchJsonFile(
4033
+ filePath,
4034
+ (json) => {
4035
+ const exclude = json.exclude ?? [];
4036
+ if (exclude.includes(dir)) return false;
4037
+ exclude.push(dir);
4038
+ json.exclude = exclude;
4039
+ return true;
4040
+ },
4041
+ // Strip single-line comments before JSON.parse
4042
+ (raw) => raw.replace(/\/\/.*$/gm, "")
4043
+ );
4484
4044
  }
4485
4045
  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
4046
+ const filePath = (0, import_node_path15.join)(cwd, "nest-cli.json");
4047
+ return patchJsonFile(filePath, (json) => {
4048
+ const compiler = json.compilerOptions ?? {};
4049
+ const assets = compiler.assets ?? [];
4050
+ const alreadyHas = assets.some((a) => {
4051
+ if (typeof a === "string") return a.includes(shellDir);
4052
+ return String(a.include ?? "").includes(shellDir);
4053
+ });
4054
+ if (alreadyHas) return false;
4055
+ assets.push({
4056
+ include: `../${shellDir}/**/*`,
4057
+ outDir: `dist/${shellDir}`,
4058
+ watchAssets: true
4059
+ });
4060
+ compiler.assets = assets;
4061
+ json.compilerOptions = compiler;
4062
+ return true;
4505
4063
  });
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
4064
  }
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";
4065
+ async function scaffoldFiles(ctx) {
4066
+ const { cwd, framework, engine, shellFileName, entryExt, pageExt } = ctx;
4528
4067
  logSection("Scaffold files");
4529
4068
  await writeIfNotExists(
4530
- (0, import_node_path14.join)(cwd, "nestjs-inertia.config.ts"),
4069
+ (0, import_node_path15.join)(cwd, "nestjs-inertia.config.ts"),
4531
4070
  configTemplate(framework),
4532
4071
  "nestjs-inertia.config.ts"
4533
4072
  );
4534
- await writeIfNotExists((0, import_node_path14.join)(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4073
+ await writeIfNotExists((0, import_node_path15.join)(cwd, "nestjs-inertia.d.ts"), DTS_TEMPLATE, "nestjs-inertia.d.ts");
4535
4074
  await writeIfNotExists(
4536
- (0, import_node_path14.join)(cwd, "tsconfig.inertia.json"),
4075
+ (0, import_node_path15.join)(cwd, "tsconfig.inertia.json"),
4537
4076
  TSCONFIG_INERTIA_TEMPLATE,
4538
4077
  "tsconfig.inertia.json"
4539
4078
  );
4540
4079
  await writeIfNotExists(
4541
- (0, import_node_path14.join)(cwd, "inertia", "tsconfig.json"),
4080
+ (0, import_node_path15.join)(cwd, "inertia", "tsconfig.json"),
4542
4081
  INERTIA_TSCONFIG_TEMPLATE,
4543
4082
  "inertia/tsconfig.json"
4544
4083
  );
4545
4084
  await writeIfNotExists(
4546
- (0, import_node_path14.join)(cwd, "inertia", shellFileName),
4085
+ (0, import_node_path15.join)(cwd, "inertia", shellFileName),
4547
4086
  htmlShellTemplate(framework, engine),
4548
4087
  `inertia/${shellFileName}`
4549
4088
  );
4550
4089
  await handleViteConfig(cwd, framework);
4551
4090
  await writeIfNotExists(
4552
- (0, import_node_path14.join)(cwd, "inertia", "app", `client.${entryExt}`),
4091
+ (0, import_node_path15.join)(cwd, "inertia", "app", `client.${entryExt}`),
4553
4092
  entryPointTemplate(framework),
4554
4093
  `inertia/app/client.${entryExt}`
4555
4094
  );
4556
4095
  await writeIfNotExists(
4557
- (0, import_node_path14.join)(cwd, "inertia", "pages", `Home.${pageExt}`),
4096
+ (0, import_node_path15.join)(cwd, "inertia", "pages", `Home.${pageExt}`),
4558
4097
  samplePageTemplate(framework),
4559
4098
  `inertia/pages/Home.${pageExt}`
4560
4099
  );
4561
4100
  await writeIfNotExists(
4562
- (0, import_node_path14.join)(cwd, "src", "home.controller.ts"),
4101
+ (0, import_node_path15.join)(cwd, "src", "home.controller.ts"),
4563
4102
  SAMPLE_CONTROLLER,
4564
4103
  "src/home.controller.ts"
4565
4104
  );
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");
4105
+ }
4106
+ function patchServerAppModule(ctx) {
4107
+ const { cwd, rootView } = ctx;
4108
+ const appModulePath = (0, import_node_path15.join)(cwd, "src", "app.module.ts");
4569
4109
  const appModuleResult = patchAppModule(appModulePath, rootView);
4570
4110
  if (appModuleResult === "patched") {
4571
4111
  logPatched("src/app.module.ts", "added InertiaModule.forRoot");
@@ -4577,7 +4117,9 @@ ${bold("nestjs-inertia init")}`);
4577
4117
  } else {
4578
4118
  logWarning("src/app.module.ts not found \u2014 add InertiaModule.forRoot() manually");
4579
4119
  }
4580
- const mainTsPath = (0, import_node_path14.join)(cwd, "src", "main.ts");
4120
+ }
4121
+ function patchServerMainTs(ctx) {
4122
+ const mainTsPath = (0, import_node_path15.join)(ctx.cwd, "src", "main.ts");
4581
4123
  const mainTsResult = patchMainTs(mainTsPath);
4582
4124
  if (mainTsResult === "patched") {
4583
4125
  logPatched("src/main.ts", "added setupInertiaVite after NestFactory.create");
@@ -4586,7 +4128,9 @@ ${bold("nestjs-inertia init")}`);
4586
4128
  } else {
4587
4129
  logWarning("src/main.ts not found \u2014 add setupInertiaVite() manually");
4588
4130
  }
4589
- const shellDir = rootView.split("/")[0];
4131
+ }
4132
+ function patchBuildConfigs(ctx) {
4133
+ const { cwd, shellDir } = ctx;
4590
4134
  const nestCliResult = patchNestCliJson(cwd, shellDir);
4591
4135
  if (nestCliResult === "patched") {
4592
4136
  logPatched("nest-cli.json", `added asset copy for ${shellDir}/ \u2192 dist/${shellDir}/`);
@@ -4607,18 +4151,26 @@ ${bold("nestjs-inertia init")}`);
4607
4151
  );
4608
4152
  }
4609
4153
  }
4610
- await patchGitignore((0, import_node_path14.join)(cwd, ".gitignore"));
4154
+ }
4155
+ async function patchGitignoreAndDist(ctx) {
4156
+ const { cwd } = ctx;
4157
+ await patchGitignore((0, import_node_path15.join)(cwd, ".gitignore"));
4611
4158
  const tsconfigDistResult = patchTsconfigExclude(cwd, "dist", "tsconfig.json");
4612
4159
  if (tsconfigDistResult === "patched") {
4613
4160
  logPatched("tsconfig.json", "excluded dist/ from server compilation");
4614
4161
  } else if (tsconfigDistResult === "already") {
4615
4162
  console.log(` ${cyan("\u2192")} tsconfig.json ${dim("(dist/ already excluded, skipped)")}`);
4616
4163
  }
4617
- await patchPackageJsonScripts(cwd, {
4164
+ }
4165
+ async function scaffoldPackageScripts(ctx) {
4166
+ await patchPackageJsonScripts(ctx.cwd, {
4618
4167
  "build:client": "vite build",
4619
4168
  "build:ssr": "VITE_SSR=1 vite build --ssr",
4620
4169
  "typecheck:inertia": "tsc --noEmit -p tsconfig.inertia.json"
4621
4170
  });
4171
+ }
4172
+ async function installMissingDeps(ctx) {
4173
+ const { cwd, framework, opts } = ctx;
4622
4174
  logSection("Install dependencies");
4623
4175
  const pkg = await readPackageJson(cwd);
4624
4176
  const installedDeps = allDeps(pkg);
@@ -4652,6 +4204,55 @@ ${bold("nestjs-inertia init")}`);
4652
4204
  installDeps(pkgManager, depsToInstall, false);
4653
4205
  installDeps(pkgManager, devDepsToInstall, true);
4654
4206
  }
4207
+ }
4208
+ var INIT_STEPS = [
4209
+ { label: "scaffold files", run: scaffoldFiles },
4210
+ {
4211
+ label: "patch app.module.ts",
4212
+ run: (ctx) => {
4213
+ logSection("Patch existing files");
4214
+ patchServerAppModule(ctx);
4215
+ }
4216
+ },
4217
+ { label: "patch main.ts", run: patchServerMainTs },
4218
+ { label: "patch build configs", run: patchBuildConfigs },
4219
+ { label: "patch .gitignore + dist exclude", run: patchGitignoreAndDist },
4220
+ { label: "add package.json scripts", run: scaffoldPackageScripts },
4221
+ { label: "install dependencies", run: installMissingDeps }
4222
+ ];
4223
+ async function runInit(opts = {}) {
4224
+ const cwd = opts.cwd ?? process.cwd();
4225
+ console.log(`
4226
+ ${bold("nestjs-inertia init")}`);
4227
+ let framework = await detectFramework(cwd);
4228
+ if (!framework) {
4229
+ framework = await promptFramework();
4230
+ }
4231
+ const engine = await detectTemplateEngine(cwd);
4232
+ const engineLabel = engine === "html" ? "plain HTML" : engine;
4233
+ const frameworkLabel = framework.charAt(0).toUpperCase() + framework.slice(1);
4234
+ console.log(`
4235
+ Detected: ${bold(`${frameworkLabel} + ${engineLabel}`)}`);
4236
+ const shellFileName = engine === "html" ? "index.html" : `index.${engine === "handlebars" ? "hbs" : engine}`;
4237
+ const rootView = engine === "html" ? "inertia/index.html" : `inertia/index.${engine === "handlebars" ? "hbs" : engine}`;
4238
+ const ctx = {
4239
+ opts,
4240
+ cwd,
4241
+ framework,
4242
+ engine,
4243
+ shellFileName,
4244
+ entryExt: framework === "react" ? "tsx" : "ts",
4245
+ pageExt: framework === "react" ? "tsx" : framework === "vue" ? "vue" : "svelte",
4246
+ rootView,
4247
+ shellDir: rootView.split("/")[0]
4248
+ // e.g. "inertia" from "inertia/index.html"
4249
+ };
4250
+ for (const step of INIT_STEPS) {
4251
+ if (process.env.NESTJS_CODEGEN_DEBUG) {
4252
+ console.log(dim(` \xB7 ${step.label}`));
4253
+ }
4254
+ await step.run(ctx);
4255
+ }
4655
4256
  console.log(`
4656
4257
  ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4657
4258
  `);
@@ -4659,18 +4260,18 @@ ${green("\u2713")} Setup complete! Run: ${bold("nest start --watch")}
4659
4260
 
4660
4261
  // src/cli/doctor.ts
4661
4262
  function checkFileExists(cwd, file) {
4662
- return (0, import_node_fs3.existsSync)((0, import_node_path15.join)(cwd, file));
4263
+ return (0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, file));
4663
4264
  }
4664
4265
  function readJson(path) {
4665
4266
  try {
4666
- const raw = (0, import_node_fs3.readFileSync)(path, "utf8").replace(/\/\/.*$/gm, "");
4267
+ const raw = (0, import_node_fs4.readFileSync)(path, "utf8").replace(/\/\/.*$/gm, "");
4667
4268
  return JSON.parse(raw);
4668
4269
  } catch {
4669
4270
  return null;
4670
4271
  }
4671
4272
  }
4672
4273
  function writeJsonField(filePath, dotPath, value) {
4673
- const raw = (0, import_node_fs3.readFileSync)(filePath, "utf8");
4274
+ const raw = (0, import_node_fs4.readFileSync)(filePath, "utf8");
4674
4275
  const stripped = raw.replace(/\/\/.*$/gm, "");
4675
4276
  const obj = JSON.parse(stripped);
4676
4277
  let target = obj;
@@ -4690,20 +4291,20 @@ function writeJsonField(filePath, dotPath, value) {
4690
4291
  } else {
4691
4292
  target[lastKey] = value;
4692
4293
  }
4693
- (0, import_node_fs3.writeFileSync)(filePath, `${JSON.stringify(obj, null, 2)}
4294
+ (0, import_node_fs4.writeFileSync)(filePath, `${JSON.stringify(obj, null, 2)}
4694
4295
  `, "utf8");
4695
4296
  }
4696
4297
  function getPackageVersion(cwd, pkg) {
4697
4298
  try {
4698
- const pkgJson = readJson((0, import_node_path15.join)(cwd, "node_modules", pkg, "package.json"));
4299
+ const pkgJson = readJson((0, import_node_path16.join)(cwd, "node_modules", pkg, "package.json"));
4699
4300
  return pkgJson?.version ?? null;
4700
4301
  } catch {
4701
4302
  return null;
4702
4303
  }
4703
4304
  }
4704
4305
  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";
4306
+ if ((0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
4307
+ if ((0, import_node_fs4.existsSync)((0, import_node_path16.join)(cwd, "yarn.lock"))) return "yarn";
4707
4308
  return "npm";
4708
4309
  }
4709
4310
  async function runDoctor(opts) {
@@ -4741,7 +4342,7 @@ async function runDoctor(opts) {
4741
4342
  autoFix: () => runInit({ cwd })
4742
4343
  });
4743
4344
  if (foundShellDir) {
4744
- const nestCliPath = (0, import_node_path15.join)(cwd, "nest-cli.json");
4345
+ const nestCliPath = (0, import_node_path16.join)(cwd, "nest-cli.json");
4745
4346
  const nestCli = readJson(nestCliPath);
4746
4347
  const compiler = nestCli?.compilerOptions ?? {};
4747
4348
  const assets = compiler.assets ?? [];
@@ -4780,7 +4381,7 @@ async function runDoctor(opts) {
4780
4381
  fix: "Run: nestjs-codegen codegen",
4781
4382
  autoFix: () => runCodegen({ cwd })
4782
4383
  });
4783
- const tsconfigPath = (0, import_node_path15.join)(cwd, "tsconfig.json");
4384
+ const tsconfigPath = (0, import_node_path16.join)(cwd, "tsconfig.json");
4784
4385
  const tsconfig = readJson(tsconfigPath);
4785
4386
  const paths = tsconfig?.compilerOptions?.paths;
4786
4387
  checks.push({
@@ -4791,7 +4392,7 @@ async function runDoctor(opts) {
4791
4392
  });
4792
4393
  const inertiaDir = foundShellDir ?? "inertia";
4793
4394
  for (const tsconfigFile of ["tsconfig.json", "tsconfig.build.json"]) {
4794
- const tsc = readJson((0, import_node_path15.join)(cwd, tsconfigFile));
4395
+ const tsc = readJson((0, import_node_path16.join)(cwd, tsconfigFile));
4795
4396
  if (!tsc) continue;
4796
4397
  const excl = tsc.exclude ?? [];
4797
4398
  const excludesIt = excl.includes(inertiaDir);
@@ -4817,14 +4418,14 @@ async function runDoctor(opts) {
4817
4418
  }
4818
4419
  });
4819
4420
  }
4820
- const inertiaTsconfigPath = (0, import_node_path15.join)(cwd, "tsconfig.inertia.json");
4421
+ const inertiaTsconfigPath = (0, import_node_path16.join)(cwd, "tsconfig.inertia.json");
4821
4422
  const inertiaTsconfig = readJson(inertiaTsconfigPath);
4822
4423
  checks.push({
4823
4424
  name: "tsconfig.inertia.json exists",
4824
4425
  pass: !!inertiaTsconfig,
4825
4426
  fix: "Create tsconfig.inertia.json (typechecks inertia/ + .nestjs-inertia/)",
4826
4427
  autoFix: () => {
4827
- (0, import_node_fs3.writeFileSync)(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
4428
+ (0, import_node_fs4.writeFileSync)(inertiaTsconfigPath, TSCONFIG_INERTIA_TEMPLATE, "utf8");
4828
4429
  }
4829
4430
  });
4830
4431
  if (inertiaTsconfig) {
@@ -4869,17 +4470,17 @@ async function runDoctor(opts) {
4869
4470
  fix: 'Add "nestjs-inertia.d.ts" to include array (resolves InertiaRegistry augmentation)'
4870
4471
  });
4871
4472
  }
4872
- const innerTsconfigPath = (0, import_node_path15.join)(cwd, "inertia", "tsconfig.json");
4473
+ const innerTsconfigPath = (0, import_node_path16.join)(cwd, "inertia", "tsconfig.json");
4873
4474
  checks.push({
4874
4475
  name: "inertia/tsconfig.json exists (VSCode picks up ~codegen alias)",
4875
- pass: (0, import_node_fs3.existsSync)(innerTsconfigPath),
4476
+ pass: (0, import_node_fs4.existsSync)(innerTsconfigPath),
4876
4477
  fix: "Create inertia/tsconfig.json that extends ../tsconfig.inertia.json",
4877
4478
  autoFix: () => {
4878
- (0, import_node_fs3.writeFileSync)(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
4479
+ (0, import_node_fs4.writeFileSync)(innerTsconfigPath, INERTIA_TSCONFIG_TEMPLATE, "utf8");
4879
4480
  }
4880
4481
  });
4881
4482
  if (checkFileExists(cwd, "vite.config.ts")) {
4882
- const viteContent = (0, import_node_fs3.readFileSync)((0, import_node_path15.join)(cwd, "vite.config.ts"), "utf8");
4483
+ const viteContent = (0, import_node_fs4.readFileSync)((0, import_node_path16.join)(cwd, "vite.config.ts"), "utf8");
4883
4484
  checks.push({
4884
4485
  name: "vite.config.ts has resolve.alias",
4885
4486
  pass: viteContent.includes("resolve") && viteContent.includes("alias"),
@@ -4944,16 +4545,16 @@ async function runDoctor(opts) {
4944
4545
  });
4945
4546
  }
4946
4547
  if (checkFileExists(cwd, ".gitignore")) {
4947
- const gitignorePath = (0, import_node_path15.join)(cwd, ".gitignore");
4948
- const gitignore = (0, import_node_fs3.readFileSync)(gitignorePath, "utf8");
4548
+ const gitignorePath = (0, import_node_path16.join)(cwd, ".gitignore");
4549
+ const gitignore = (0, import_node_fs4.readFileSync)(gitignorePath, "utf8");
4949
4550
  checks.push({
4950
4551
  name: ".gitignore includes .nestjs-inertia/",
4951
4552
  pass: gitignore.includes(".nestjs-inertia"),
4952
4553
  fix: "Add .nestjs-inertia/ to .gitignore",
4953
- autoFix: () => (0, import_node_fs3.appendFileSync)(gitignorePath, "\n.nestjs-inertia/\n")
4554
+ autoFix: () => (0, import_node_fs4.appendFileSync)(gitignorePath, "\n.nestjs-inertia/\n")
4954
4555
  });
4955
4556
  }
4956
- const pkgJsonPath = (0, import_node_path15.join)(cwd, "package.json");
4557
+ const pkgJsonPath = (0, import_node_path16.join)(cwd, "package.json");
4957
4558
  const pkgJson = readJson(pkgJsonPath);
4958
4559
  const scripts = pkgJson?.scripts ?? {};
4959
4560
  checks.push({