@dudousxd/nestjs-codegen 0.2.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.
@@ -0,0 +1,3919 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
12
+
13
+ // src/nest/module.ts
14
+ import { Inject, Injectable, Logger, Module } from "@nestjs/common";
15
+
16
+ // src/config/load-config.ts
17
+ import { access } from "fs/promises";
18
+ import { isAbsolute, join, relative, resolve, sep } from "path";
19
+ import { pathToFileURL } from "url";
20
+
21
+ // src/exceptions.ts
22
+ var ConfigError = class extends Error {
23
+ constructor(message, options) {
24
+ super(message, options);
25
+ this.name = "ConfigError";
26
+ }
27
+ };
28
+ var CodegenError = class extends Error {
29
+ constructor(message, options) {
30
+ super(message, options);
31
+ this.name = "CodegenError";
32
+ }
33
+ };
34
+
35
+ // src/adapters/zod.ts
36
+ function toObjectKey(name) {
37
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
38
+ }
39
+ function messageSuffix(messageRaw2) {
40
+ return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
41
+ }
42
+ function renderStringChecks(checks) {
43
+ let out = "";
44
+ for (const c of checks) {
45
+ switch (c.check) {
46
+ case "email":
47
+ out += `.email(${messageSuffix(c.messageRaw)})`;
48
+ break;
49
+ case "url":
50
+ out += `.url(${messageSuffix(c.messageRaw)})`;
51
+ break;
52
+ case "uuid":
53
+ out += `.uuid(${messageSuffix(c.messageRaw)})`;
54
+ break;
55
+ case "regex":
56
+ out += `.regex(${c.pattern})`;
57
+ break;
58
+ case "min":
59
+ out += `.min(${c.value})`;
60
+ break;
61
+ case "max":
62
+ out += `.max(${c.value})`;
63
+ break;
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ function render(node, ctx) {
69
+ switch (node.kind) {
70
+ case "string":
71
+ return `z.string()${renderStringChecks(node.checks)}`;
72
+ case "number": {
73
+ let out = "z.number()";
74
+ for (const c of node.checks) {
75
+ if (c.check === "int") out += ".int()";
76
+ else if (c.check === "positive") out += ".positive()";
77
+ else if (c.check === "negative") out += ".negative()";
78
+ else if (c.check === "min") out += `.min(${c.value})`;
79
+ else if (c.check === "max") out += `.max(${c.value})`;
80
+ }
81
+ return out;
82
+ }
83
+ case "boolean":
84
+ return "z.boolean()";
85
+ case "date":
86
+ return "z.coerce.date()";
87
+ case "unknown":
88
+ return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
89
+ case "instanceof":
90
+ return `z.instanceof(${node.ctor})`;
91
+ case "enum":
92
+ return `z.enum([${node.literals.join(", ")}])`;
93
+ case "literal":
94
+ return `z.literal(${node.raw})`;
95
+ case "union":
96
+ return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
97
+ case "object": {
98
+ if (node.fields.length === 0) {
99
+ return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
100
+ }
101
+ const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
102
+ return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
103
+ }
104
+ case "array":
105
+ return `z.array(${render(node.element, ctx)})`;
106
+ case "optional":
107
+ return `${render(node.inner, ctx)}.optional()`;
108
+ case "ref":
109
+ return node.name;
110
+ case "lazyRef":
111
+ return `z.lazy(() => ${node.name})`;
112
+ case "annotated": {
113
+ const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
114
+ return `${render(node.inner, ctx)} ${comments}`;
115
+ }
116
+ }
117
+ }
118
+ var zodAdapter = {
119
+ name: "zod",
120
+ importStatements(usage) {
121
+ return usage.used ? ["import { z } from 'zod';"] : [];
122
+ },
123
+ render,
124
+ inferType(schemaConst) {
125
+ return `z.infer<typeof ${schemaConst}>`;
126
+ },
127
+ renderModule(mod) {
128
+ const ctx = { named: mod.named };
129
+ const namedNestedSchemas = /* @__PURE__ */ new Map();
130
+ for (const [name, node] of mod.named) {
131
+ namedNestedSchemas.set(name, render(node, ctx));
132
+ }
133
+ return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
134
+ }
135
+ };
136
+
137
+ // src/adapters/registry.ts
138
+ function resolveAdapter(option) {
139
+ if (typeof option !== "string") return option;
140
+ if (option === "zod") return zodAdapter;
141
+ const pkg = `@dudousxd/nestjs-codegen-${option}`;
142
+ const named = `${option}Adapter`;
143
+ throw new ConfigError(
144
+ `Validation adapter "${option}" is not bundled in core. Install ${pkg} and pass the adapter instance instead of the string:
145
+
146
+ import { ${named} } from '${pkg}';
147
+ defineConfig({ validation: ${named} });`
148
+ );
149
+ }
150
+
151
+ // src/config/load-config.ts
152
+ function resolveAbsolute(cwd, p) {
153
+ if (isAbsolute(p)) return p;
154
+ return resolve(cwd, p);
155
+ }
156
+ function assertInsideCwd(cwd, resolvedPath, fieldName) {
157
+ const rel = relative(cwd, resolvedPath);
158
+ if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel)) {
159
+ throw new ConfigError(
160
+ `\`${fieldName}\` must be inside the project cwd.
161
+ Resolved to: ${resolvedPath}
162
+ Project cwd: ${cwd}
163
+ If this is intentional, move the file inside your project directory.`
164
+ );
165
+ }
166
+ }
167
+ function resolveConfig(userConfig, cwd) {
168
+ return applyDefaults(userConfig, cwd ?? process.cwd());
169
+ }
170
+ function applyDefaults(userConfig, cwd) {
171
+ const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-inertia");
172
+ const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
173
+ let app = null;
174
+ if (userConfig.app) {
175
+ const resolvedEntry = resolveAbsolute(cwd, userConfig.app.moduleEntry);
176
+ assertInsideCwd(cwd, resolvedEntry, "app.moduleEntry");
177
+ let resolvedTsconfig = null;
178
+ if (userConfig.app.tsconfig) {
179
+ resolvedTsconfig = resolveAbsolute(cwd, userConfig.app.tsconfig);
180
+ assertInsideCwd(cwd, resolvedTsconfig, "app.tsconfig");
181
+ }
182
+ app = {
183
+ moduleEntry: resolvedEntry,
184
+ tsconfig: resolvedTsconfig
185
+ };
186
+ }
187
+ return {
188
+ extensions: userConfig.extensions ?? [],
189
+ validation: resolveAdapter(userConfig.validation ?? "zod"),
190
+ pages: userConfig.pages ? {
191
+ glob: userConfig.pages.glob,
192
+ propsExport: userConfig.pages.propsExport ?? "ComponentProps",
193
+ componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext"
194
+ } : null,
195
+ contracts: {
196
+ glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
197
+ debounceMs: userConfig.contracts?.debounceMs ?? 500
198
+ },
199
+ scopes: userConfig.scopes ?? {},
200
+ codegen: {
201
+ outDir,
202
+ cwd: resolvedCwd
203
+ },
204
+ app,
205
+ fetcher: userConfig.fetcher ?? null,
206
+ forms: {
207
+ enabled: userConfig.forms?.enabled ?? true,
208
+ watch: userConfig.forms?.watch ?? "src/**/*.dto.ts",
209
+ zodImport: userConfig.forms?.zodImport ?? "zod"
210
+ }
211
+ };
212
+ }
213
+
214
+ // src/watch/watcher.ts
215
+ import { readFile as readFile3 } from "fs/promises";
216
+ import { join as join12 } from "path";
217
+ import chokidar from "chokidar";
218
+
219
+ // src/discovery/contracts-fast.ts
220
+ import { join as join2, resolve as resolve3 } from "path";
221
+ import fg from "fast-glob";
222
+ import {
223
+ Node as Node6,
224
+ Project,
225
+ SyntaxKind as SyntaxKind2
226
+ } from "ts-morph";
227
+
228
+ // src/discovery/dto-to-ir.ts
229
+ import {
230
+ Node as Node2
231
+ } from "ts-morph";
232
+
233
+ // src/discovery/type-ref-resolution.ts
234
+ import { readFileSync } from "fs";
235
+ import { dirname, resolve as resolve2 } from "path";
236
+ import {
237
+ Node
238
+ } from "ts-morph";
239
+ var _ctx = { projectRoot: "", tsconfigPaths: null };
240
+ function setDiscoveryContext(ctx) {
241
+ const prev = _ctx;
242
+ _ctx = ctx;
243
+ return prev;
244
+ }
245
+ function restoreDiscoveryContext(ctx) {
246
+ _ctx = ctx;
247
+ }
248
+ function _projectRoot() {
249
+ return _ctx.projectRoot;
250
+ }
251
+ function _tsconfigPaths() {
252
+ return _ctx.tsconfigPaths;
253
+ }
254
+ var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
255
+ function dbg(...args) {
256
+ if (_debug) console.log("[codegen:debug]", ...args);
257
+ }
258
+ function loadTsconfigPaths(tsconfigPath) {
259
+ try {
260
+ const raw = readFileSync(tsconfigPath, "utf8");
261
+ const stripped = raw.replace(/\/\/.*$/gm, "");
262
+ const parsed = JSON.parse(stripped);
263
+ return parsed.compilerOptions?.paths ?? null;
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+ function findTypeInFile(name, file) {
269
+ const cls = file.getClass(name);
270
+ if (cls) return { kind: "class", decl: cls, file };
271
+ const iface = file.getInterface(name);
272
+ if (iface) return { kind: "interface", decl: iface, file };
273
+ const alias = file.getTypeAlias(name);
274
+ if (alias) {
275
+ const typeNode = alias.getTypeNode();
276
+ return {
277
+ kind: "typeAlias",
278
+ typeNode,
279
+ file,
280
+ text: typeNode ? typeNode.getText() : "unknown"
281
+ };
282
+ }
283
+ const enumDecl = file.getEnum(name);
284
+ if (enumDecl) {
285
+ const members = enumDecl.getMembers().map((m) => {
286
+ const val = m.getValue();
287
+ if (typeof val === "string" || typeof val === "number") return JSON.stringify(val);
288
+ return JSON.stringify(m.getName());
289
+ });
290
+ return { kind: "enum", members };
291
+ }
292
+ return null;
293
+ }
294
+ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
295
+ if (moduleSpecifier.startsWith(".")) {
296
+ const dir = dirname(sourceFile.getFilePath());
297
+ const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
298
+ return [
299
+ resolve2(dir, `${noExt}.ts`),
300
+ resolve2(dir, `${moduleSpecifier}.ts`),
301
+ resolve2(dir, moduleSpecifier, "index.ts")
302
+ ];
303
+ }
304
+ const baseUrl = _projectRoot();
305
+ const tsconfigPaths = _tsconfigPaths();
306
+ dbg(
307
+ "resolveModuleSpecifier",
308
+ moduleSpecifier,
309
+ "paths:",
310
+ JSON.stringify(tsconfigPaths),
311
+ "baseUrl:",
312
+ baseUrl
313
+ );
314
+ if (tsconfigPaths) {
315
+ for (const [pattern, mappings] of Object.entries(tsconfigPaths)) {
316
+ const prefix = pattern.replace("*", "");
317
+ if (moduleSpecifier.startsWith(prefix)) {
318
+ const rest = moduleSpecifier.slice(prefix.length);
319
+ const candidates = [];
320
+ for (const mapping of mappings) {
321
+ const resolved = resolve2(baseUrl, mapping.replace("*", rest));
322
+ candidates.push(`${resolved}.ts`, resolve2(resolved, "index.ts"));
323
+ }
324
+ dbg(" resolved candidates:", candidates);
325
+ return candidates;
326
+ }
327
+ }
328
+ }
329
+ return [];
330
+ }
331
+ function resolveImportedType(name, sourceFile, project) {
332
+ for (const importDecl of sourceFile.getImportDeclarations()) {
333
+ const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
334
+ if (!namedImport) continue;
335
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
336
+ const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
337
+ if (candidates.length === 0) continue;
338
+ for (const candidate of candidates) {
339
+ let importedFile = project.getSourceFile(candidate);
340
+ if (!importedFile) {
341
+ try {
342
+ importedFile = project.addSourceFileAtPath(candidate);
343
+ } catch {
344
+ continue;
345
+ }
346
+ }
347
+ const result = findTypeInFile(name, importedFile);
348
+ if (result) return result;
349
+ const viaReExport = resolveReExportedType(name, importedFile, project, /* @__PURE__ */ new Set());
350
+ if (viaReExport) return viaReExport;
351
+ }
352
+ }
353
+ return resolveReExportedType(name, sourceFile, project, /* @__PURE__ */ new Set());
354
+ }
355
+ function resolveReExportedType(name, file, project, seen) {
356
+ const filePath = file.getFilePath();
357
+ if (seen.has(filePath)) return null;
358
+ seen.add(filePath);
359
+ for (const exportDecl of file.getExportDeclarations()) {
360
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue();
361
+ const namedExports = exportDecl.getNamedExports();
362
+ if (moduleSpecifier) {
363
+ const hasStar = namedExports.length === 0;
364
+ const reExportsName2 = namedExports.some(
365
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
366
+ );
367
+ if (!hasStar && !reExportsName2) continue;
368
+ const sourceName2 = hasStar ? name : namedExports.find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name)?.getName() ?? name;
369
+ const target = followModuleForType(sourceName2, moduleSpecifier, file, project, seen);
370
+ if (target) return target;
371
+ continue;
372
+ }
373
+ const reExportsName = namedExports.some(
374
+ (n) => (n.getAliasNode()?.getText() ?? n.getName()) === name
375
+ );
376
+ if (!reExportsName) continue;
377
+ const sourceName = namedExports.find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name)?.getName() ?? name;
378
+ const local = findTypeInFile(sourceName, file);
379
+ if (local) return local;
380
+ const imported = resolveImportedType(sourceName, file, project);
381
+ if (imported) return imported;
382
+ }
383
+ return null;
384
+ }
385
+ function followModuleForType(name, moduleSpecifier, fromFile, project, seen) {
386
+ const candidates = resolveModuleSpecifier(moduleSpecifier, fromFile, project);
387
+ for (const candidate of candidates) {
388
+ let importedFile = project.getSourceFile(candidate);
389
+ if (!importedFile) {
390
+ try {
391
+ importedFile = project.addSourceFileAtPath(candidate);
392
+ } catch {
393
+ continue;
394
+ }
395
+ }
396
+ const result = findTypeInFile(name, importedFile);
397
+ if (result) return result;
398
+ const viaReExport = resolveReExportedType(name, importedFile, project, seen);
399
+ if (viaReExport) return viaReExport;
400
+ }
401
+ return null;
402
+ }
403
+ function findType(name, sourceFile, project) {
404
+ const local = findTypeInFile(name, sourceFile);
405
+ if (local) return local;
406
+ return resolveImportedType(name, sourceFile, project);
407
+ }
408
+ var _NON_REF_NAMES = /* @__PURE__ */ new Set(["string", "number", "boolean", "void", "unknown", "any", "Date"]);
409
+ function _localDeclForKinds(name, file, kinds) {
410
+ if (kinds.includes("class") && file.getClass(name)?.isExported()) return true;
411
+ if (kinds.includes("interface") && file.getInterface(name)?.isExported()) return true;
412
+ if (kinds.includes("typeAlias") && file.getTypeAlias(name)?.isExported()) return true;
413
+ if (kinds.includes("enum") && file.getEnum(name)?.isExported()) return true;
414
+ return false;
415
+ }
416
+ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
417
+ let name;
418
+ if (typeof nodeOrName === "string") {
419
+ name = nodeOrName;
420
+ } else {
421
+ const typeNode = nodeOrName;
422
+ if (opts.unwrapContainers && Node.isArrayTypeNode(typeNode)) {
423
+ const inner = resolveTypeRef(typeNode.getElementTypeNode(), sourceFile, project, opts);
424
+ return inner ? { ...inner, isArray: true } : null;
425
+ }
426
+ if (!Node.isTypeReference(typeNode)) return null;
427
+ const typeName = typeNode.getTypeName();
428
+ const refName = Node.isIdentifier(typeName) ? typeName.getText() : null;
429
+ if (!refName) return null;
430
+ if (opts.unwrapContainers && refName === "Promise") {
431
+ const first = typeNode.getTypeArguments()[0];
432
+ return first ? resolveTypeRef(first, sourceFile, project, opts) : null;
433
+ }
434
+ if (opts.unwrapContainers && refName === "Array") {
435
+ const first = typeNode.getTypeArguments()[0];
436
+ if (!first) return null;
437
+ const inner = resolveTypeRef(first, sourceFile, project, opts);
438
+ return inner ? { ...inner, isArray: true } : null;
439
+ }
440
+ if (_NON_REF_NAMES.has(refName)) return null;
441
+ name = refName;
442
+ }
443
+ if (_localDeclForKinds(name, sourceFile, opts.kinds)) {
444
+ return { name, filePath: sourceFile.getFilePath() };
445
+ }
446
+ for (const importDecl of sourceFile.getImportDeclarations()) {
447
+ const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
448
+ if (!namedImport) continue;
449
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
450
+ if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
451
+ const tsconfigPaths = _tsconfigPaths();
452
+ const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
453
+ const prefix = p.replace("*", "");
454
+ return moduleSpecifier.startsWith(prefix);
455
+ });
456
+ if (!isAlias) {
457
+ return { name, filePath: moduleSpecifier };
458
+ }
459
+ }
460
+ const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
461
+ for (const candidate of candidates) {
462
+ let importedFile = project.getSourceFile(candidate);
463
+ if (!importedFile) {
464
+ try {
465
+ importedFile = project.addSourceFileAtPath(candidate);
466
+ } catch {
467
+ continue;
468
+ }
469
+ }
470
+ if (_localDeclForKinds(name, importedFile, opts.kinds)) {
471
+ return { name, filePath: importedFile.getFilePath() };
472
+ }
473
+ }
474
+ }
475
+ return null;
476
+ }
477
+
478
+ // src/discovery/dto-to-ir.ts
479
+ var KNOWN_DECORATORS = /* @__PURE__ */ new Set([
480
+ "IsString",
481
+ "IsNumber",
482
+ "IsInt",
483
+ "IsBoolean",
484
+ "IsDate",
485
+ "IsEmail",
486
+ "IsUrl",
487
+ "IsUUID",
488
+ "MinLength",
489
+ "MaxLength",
490
+ "Length",
491
+ "Min",
492
+ "Max",
493
+ "IsPositive",
494
+ "IsNegative",
495
+ "Matches",
496
+ "IsEnum",
497
+ "IsIn",
498
+ "IsOptional",
499
+ "IsNotEmpty",
500
+ "IsArray",
501
+ "ValidateNested",
502
+ "Type",
503
+ "IsObject",
504
+ "Allow",
505
+ "IsDefined"
506
+ ]);
507
+ function extractSchemaFromDto(classDecl, sourceFile, project) {
508
+ const ctx = {
509
+ sourceFile,
510
+ project,
511
+ named: /* @__PURE__ */ new Map(),
512
+ warnings: [],
513
+ warnedDecorators: /* @__PURE__ */ new Set(),
514
+ emittedClasses: /* @__PURE__ */ new Map(),
515
+ visiting: /* @__PURE__ */ new Set(),
516
+ recursiveSchemas: /* @__PURE__ */ new Set(),
517
+ depth: 0
518
+ };
519
+ const root = buildObject(classDecl, sourceFile, ctx);
520
+ for (const schemaName of ctx.recursiveSchemas) {
521
+ ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
522
+ }
523
+ return { root, named: ctx.named, warnings: ctx.warnings };
524
+ }
525
+ function buildObject(classDecl, classFile, ctx) {
526
+ const props = classDecl.getProperties();
527
+ if (props.length === 0) {
528
+ return { kind: "object", fields: [], passthrough: true };
529
+ }
530
+ const fields = [];
531
+ for (const prop of props) {
532
+ fields.push({ key: prop.getName(), value: buildProperty(prop, classFile, ctx) });
533
+ }
534
+ return { kind: "object", fields, passthrough: false };
535
+ }
536
+ function buildProperty(prop, classFile, ctx) {
537
+ const decorators = /* @__PURE__ */ new Map();
538
+ for (const d of prop.getDecorators()) decorators.set(d.getName(), d);
539
+ const has = (n) => decorators.has(n);
540
+ const dec = (n) => decorators.get(n);
541
+ const typeNode = prop.getTypeNode();
542
+ const typeText = typeNode?.getText() ?? "unknown";
543
+ const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
544
+ const typeRefName = resolveTypeFactoryName(dec("Type"));
545
+ if (has("ValidateNested") || typeRefName) {
546
+ const childName = typeRefName ?? singularClassName(typeText);
547
+ if (childName) {
548
+ const childNode = buildNestedReference(childName, classFile, ctx);
549
+ const wrapArray = has("IsArray") || isArrayType;
550
+ const node2 = wrapArray ? { kind: "array", element: childNode } : childNode;
551
+ return applyPresence(node2, decorators);
552
+ }
553
+ }
554
+ let base = baseFromType(typeText, isArrayType);
555
+ const stringChecks = [];
556
+ const numberChecks = [];
557
+ if (has("IsString")) base = { kind: "string", checks: stringChecks };
558
+ if (has("IsBoolean")) base = { kind: "boolean" };
559
+ if (has("IsDate")) base = { kind: "date" };
560
+ if (has("IsNumber")) base = { kind: "number", checks: numberChecks };
561
+ if (has("IsInt")) {
562
+ base = { kind: "number", checks: numberChecks };
563
+ numberChecks.push({ check: "int" });
564
+ }
565
+ if (has("IsObject") && !has("ValidateNested")) {
566
+ base = { kind: "object", fields: [], passthrough: true };
567
+ }
568
+ if (has("Allow")) base = { kind: "unknown" };
569
+ const ensureString = () => {
570
+ if (base.kind !== "string") base = { kind: "string", checks: stringChecks };
571
+ };
572
+ if (has("IsEmail")) {
573
+ ensureString();
574
+ const m = messageRaw(dec("IsEmail"));
575
+ stringChecks.push(m === void 0 ? { check: "email" } : { check: "email", messageRaw: m });
576
+ }
577
+ if (has("IsUrl")) {
578
+ ensureString();
579
+ const m = messageRaw(dec("IsUrl"));
580
+ stringChecks.push(m === void 0 ? { check: "url" } : { check: "url", messageRaw: m });
581
+ }
582
+ if (has("IsUUID")) {
583
+ ensureString();
584
+ const m = messageRaw(dec("IsUUID"));
585
+ stringChecks.push(m === void 0 ? { check: "uuid" } : { check: "uuid", messageRaw: m });
586
+ }
587
+ if (has("Matches")) {
588
+ const re = firstArgText(dec("Matches"));
589
+ if (re) {
590
+ ensureString();
591
+ stringChecks.push({ check: "regex", pattern: re });
592
+ }
593
+ }
594
+ if (has("MinLength")) {
595
+ const n = numericArg(dec("MinLength"));
596
+ if (n !== null) stringChecks.push({ check: "min", value: n });
597
+ }
598
+ if (has("MaxLength")) {
599
+ const n = numericArg(dec("MaxLength"));
600
+ if (n !== null) stringChecks.push({ check: "max", value: n });
601
+ }
602
+ if (has("Length")) {
603
+ const [min, max] = numericArgs(dec("Length"));
604
+ if (min !== null) stringChecks.push({ check: "min", value: min });
605
+ if (max !== null) stringChecks.push({ check: "max", value: max });
606
+ }
607
+ if (has("Min")) {
608
+ const n = numericArg(dec("Min"));
609
+ if (n !== null) numberChecks.push({ check: "min", value: n });
610
+ }
611
+ if (has("Max")) {
612
+ const n = numericArg(dec("Max"));
613
+ if (n !== null) numberChecks.push({ check: "max", value: n });
614
+ }
615
+ if (has("IsPositive")) numberChecks.push({ check: "positive" });
616
+ if (has("IsNegative")) numberChecks.push({ check: "negative" });
617
+ if (has("IsNotEmpty") && base.kind === "string") stringChecks.push({ check: "min", value: "1" });
618
+ if (has("IsEnum")) {
619
+ const enumNode = enumSchemaFromDecorator(dec("IsEnum"), classFile, ctx);
620
+ if (enumNode) base = enumNode;
621
+ }
622
+ if (has("IsIn")) {
623
+ const inNode = inSchemaFromDecorator(dec("IsIn"));
624
+ if (inNode) base = inNode;
625
+ }
626
+ const unmappable = [];
627
+ for (const name of decorators.keys()) {
628
+ if (!KNOWN_DECORATORS.has(name)) {
629
+ unmappable.push(name);
630
+ if (!ctx.warnedDecorators.has(name)) {
631
+ ctx.warnedDecorators.add(name);
632
+ const msg = `@${name} is not translatable to a client validation schema and was skipped (server-only validation).`;
633
+ ctx.warnings.push(msg);
634
+ console.warn(`[nestjs-codegen] ${msg}`);
635
+ }
636
+ }
637
+ }
638
+ if (base.kind === "string") base = { kind: "string", checks: stringChecks };
639
+ else if (base.kind === "number") base = { kind: "number", checks: numberChecks };
640
+ let node = base;
641
+ if (isArrayType && node.kind !== "array") {
642
+ node = { kind: "array", element: node };
643
+ }
644
+ node = applyPresence(node, decorators);
645
+ if (unmappable.length > 0) {
646
+ node = { kind: "annotated", inner: node, unmappable };
647
+ }
648
+ return node;
649
+ }
650
+ function applyPresence(node, decorators) {
651
+ if (decorators.has("IsDefined")) return node;
652
+ if (decorators.has("IsOptional")) return { kind: "optional", inner: node };
653
+ return node;
654
+ }
655
+ function baseFromType(typeText, isArrayType) {
656
+ const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
657
+ switch (inner) {
658
+ case "string":
659
+ return { kind: "string", checks: [] };
660
+ case "number":
661
+ return { kind: "number", checks: [] };
662
+ case "boolean":
663
+ return { kind: "boolean" };
664
+ case "Date":
665
+ return { kind: "date" };
666
+ case "File":
667
+ case "Express.Multer.File":
668
+ return { kind: "instanceof", ctor: "File" };
669
+ default:
670
+ return { kind: "unknown" };
671
+ }
672
+ }
673
+ function buildNestedReference(className, fromFile, ctx) {
674
+ if (ctx.visiting.has(className) || ctx.depth >= 8) {
675
+ const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
676
+ ctx.emittedClasses.set(className, reserved);
677
+ ctx.recursiveSchemas.add(reserved);
678
+ if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
679
+ ctx.warnedDecorators.add(`recursive:${reserved}`);
680
+ const msg = `${className} is a recursive type and was not expanded; the generated schema uses unknown for it.`;
681
+ ctx.warnings.push(msg);
682
+ console.warn(`[nestjs-codegen] ${msg}`);
683
+ }
684
+ return { kind: "lazyRef", name: reserved };
685
+ }
686
+ const existing = ctx.emittedClasses.get(className);
687
+ if (existing) return { kind: "ref", name: existing };
688
+ const schemaName = aliasFor(className, ctx);
689
+ const resolved = findType(className, fromFile, ctx.project);
690
+ if (!resolved || resolved.kind !== "class") {
691
+ return { kind: "object", fields: [], passthrough: true };
692
+ }
693
+ ctx.emittedClasses.set(className, schemaName);
694
+ ctx.visiting.add(className);
695
+ ctx.depth += 1;
696
+ const childNode = buildObject(resolved.decl, resolved.file, ctx);
697
+ ctx.depth -= 1;
698
+ ctx.visiting.delete(className);
699
+ ctx.named.set(schemaName, childNode);
700
+ return { kind: "ref", name: schemaName };
701
+ }
702
+ function aliasFor(className, ctx) {
703
+ const baseName = `${className}Schema`;
704
+ let candidate = baseName;
705
+ let i = 1;
706
+ const used = new Set(ctx.named.keys());
707
+ for (const v of ctx.emittedClasses.values()) used.add(v);
708
+ while (used.has(candidate)) {
709
+ candidate = `${baseName}_${i}`;
710
+ i += 1;
711
+ }
712
+ return candidate;
713
+ }
714
+ function firstArg(decorator) {
715
+ return decorator?.getArguments()[0];
716
+ }
717
+ function firstArgText(decorator) {
718
+ const arg = firstArg(decorator);
719
+ return arg ? arg.getText() : null;
720
+ }
721
+ function numericArg(decorator) {
722
+ const arg = firstArg(decorator);
723
+ if (arg && Node2.isNumericLiteral(arg)) return arg.getText();
724
+ return null;
725
+ }
726
+ function numericArgs(decorator) {
727
+ const args = decorator?.getArguments() ?? [];
728
+ const num = (n) => n && Node2.isNumericLiteral(n) ? n.getText() : null;
729
+ return [num(args[0]), num(args[1])];
730
+ }
731
+ function messageRaw(decorator) {
732
+ const args = decorator?.getArguments() ?? [];
733
+ for (const arg of args) {
734
+ if (Node2.isObjectLiteralExpression(arg)) {
735
+ for (const prop of arg.getProperties()) {
736
+ if (Node2.isPropertyAssignment(prop) && prop.getName() === "message") {
737
+ const init = prop.getInitializer();
738
+ if (init && Node2.isStringLiteral(init)) return init.getText();
739
+ }
740
+ }
741
+ }
742
+ }
743
+ return void 0;
744
+ }
745
+ function resolveTypeFactoryName(decorator) {
746
+ const arg = firstArg(decorator);
747
+ if (!arg) return null;
748
+ if (Node2.isArrowFunction(arg)) {
749
+ const body = arg.getBody();
750
+ if (Node2.isIdentifier(body)) return body.getText();
751
+ }
752
+ return null;
753
+ }
754
+ function singularClassName(typeText) {
755
+ const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
756
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
757
+ }
758
+ function enumSchemaFromDecorator(decorator, classFile, ctx) {
759
+ const arg = firstArg(decorator);
760
+ if (!arg) return null;
761
+ if (Node2.isIdentifier(arg)) {
762
+ const name = arg.getText();
763
+ const resolved = findType(name, classFile, ctx.project);
764
+ if (resolved && resolved.kind === "enum") {
765
+ return { kind: "enum", literals: resolved.members };
766
+ }
767
+ const msg = `@IsEnum(${name}): enum could not be resolved to literal members and is not importable into the generated schema; falling back to unknown.`;
768
+ if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
769
+ ctx.warnedDecorators.add(`IsEnum:${name}`);
770
+ ctx.warnings.push(msg);
771
+ console.warn(`[nestjs-codegen] ${msg}`);
772
+ }
773
+ return { kind: "unknown", note: `@IsEnum(${name}): enum not resolvable to literals` };
774
+ }
775
+ if (Node2.isObjectLiteralExpression(arg)) {
776
+ const values = [];
777
+ for (const p of arg.getProperties()) {
778
+ if (!Node2.isPropertyAssignment(p)) continue;
779
+ const init = p.getInitializer();
780
+ if (init && Node2.isStringLiteral(init)) values.push(init.getText());
781
+ }
782
+ if (values.length > 0) return { kind: "enum", literals: values };
783
+ }
784
+ return null;
785
+ }
786
+ function inSchemaFromDecorator(decorator) {
787
+ const arg = firstArg(decorator);
788
+ if (arg && Node2.isArrayLiteralExpression(arg)) {
789
+ const elements = arg.getElements();
790
+ const allStrings = elements.every((e) => Node2.isStringLiteral(e));
791
+ if (allStrings && elements.length > 0) {
792
+ return { kind: "enum", literals: elements.map((e) => e.getText()) };
793
+ }
794
+ if (elements.length > 0) {
795
+ return {
796
+ kind: "union",
797
+ options: elements.map((e) => ({ kind: "literal", raw: e.getText() }))
798
+ };
799
+ }
800
+ }
801
+ return null;
802
+ }
803
+
804
+ // src/discovery/dto-to-zod.ts
805
+ import {
806
+ Node as Node3
807
+ } from "ts-morph";
808
+ var KNOWN_DECORATORS2 = /* @__PURE__ */ new Set([
809
+ "IsString",
810
+ "IsNumber",
811
+ "IsInt",
812
+ "IsBoolean",
813
+ "IsDate",
814
+ "IsEmail",
815
+ "IsUrl",
816
+ "IsUUID",
817
+ "MinLength",
818
+ "MaxLength",
819
+ "Length",
820
+ "Min",
821
+ "Max",
822
+ "IsPositive",
823
+ "IsNegative",
824
+ "Matches",
825
+ "IsEnum",
826
+ "IsIn",
827
+ "IsOptional",
828
+ "IsNotEmpty",
829
+ "IsArray",
830
+ "ValidateNested",
831
+ "Type",
832
+ "IsObject",
833
+ "Allow",
834
+ "IsDefined"
835
+ ]);
836
+ function extractZodFromDto(classDecl, sourceFile, project) {
837
+ const ctx = {
838
+ sourceFile,
839
+ project,
840
+ namedNestedSchemas: /* @__PURE__ */ new Map(),
841
+ warnings: [],
842
+ warnedDecorators: /* @__PURE__ */ new Set(),
843
+ emittedClasses: /* @__PURE__ */ new Map(),
844
+ visiting: /* @__PURE__ */ new Set(),
845
+ recursiveSchemas: /* @__PURE__ */ new Set(),
846
+ depth: 0
847
+ };
848
+ const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
849
+ for (const schemaName of ctx.recursiveSchemas) {
850
+ ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
851
+ }
852
+ return {
853
+ schemaText,
854
+ namedNestedSchemas: ctx.namedNestedSchemas,
855
+ warnings: ctx.warnings
856
+ };
857
+ }
858
+ function buildObjectSchema(classDecl, classFile, ctx) {
859
+ const props = classDecl.getProperties();
860
+ if (props.length === 0) {
861
+ return "z.object({}).passthrough()";
862
+ }
863
+ const fields = [];
864
+ for (const prop of props) {
865
+ const name = prop.getName();
866
+ const expr = buildPropertySchema(prop, classFile, ctx);
867
+ fields.push(`${toObjectKey2(name)}: ${expr}`);
868
+ }
869
+ return `z.object({ ${fields.join(", ")} })`;
870
+ }
871
+ function toObjectKey2(name) {
872
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
873
+ }
874
+ function buildPropertySchema(prop, classFile, ctx) {
875
+ const decorators = /* @__PURE__ */ new Map();
876
+ for (const d of prop.getDecorators()) decorators.set(d.getName(), d);
877
+ const has = (n) => decorators.has(n);
878
+ const dec = (n) => decorators.get(n);
879
+ const typeNode = prop.getTypeNode();
880
+ const typeText = typeNode?.getText() ?? "unknown";
881
+ const isArrayType = !!typeNode && typeNode.getText().endsWith("[]");
882
+ const comments = [];
883
+ const typeRefName = resolveTypeFactoryName2(dec("Type"));
884
+ if (has("ValidateNested") || typeRefName) {
885
+ const childName = typeRefName ?? singularClassName2(typeText);
886
+ if (childName) {
887
+ const childExpr = buildNestedReference2(childName, classFile, ctx);
888
+ const wrapArray = has("IsArray") || isArrayType;
889
+ let expr2 = wrapArray ? `z.array(${childExpr})` : childExpr;
890
+ expr2 = applyPresence2(expr2, decorators);
891
+ return expr2;
892
+ }
893
+ }
894
+ let base = baseFromType2(typeText, isArrayType, ctx, classFile);
895
+ const refinements = [];
896
+ if (has("IsString")) base = "z.string()";
897
+ if (has("IsBoolean")) base = "z.boolean()";
898
+ if (has("IsDate")) base = "z.coerce.date()";
899
+ if (has("IsNumber")) base = "z.number()";
900
+ if (has("IsInt")) base = "z.number().int()";
901
+ if (has("IsObject") && !has("ValidateNested")) base = "z.object({}).passthrough()";
902
+ if (has("Allow")) base = "z.unknown()";
903
+ if (has("IsEmail")) {
904
+ base = ensureStringBase(base);
905
+ refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
906
+ }
907
+ if (has("IsUrl")) {
908
+ base = ensureStringBase(base);
909
+ refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
910
+ }
911
+ if (has("IsUUID")) {
912
+ base = ensureStringBase(base);
913
+ refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
914
+ }
915
+ if (has("Matches")) {
916
+ const re = firstArgText2(dec("Matches"));
917
+ if (re) {
918
+ base = ensureStringBase(base);
919
+ refinements.push(`.regex(${re})`);
920
+ }
921
+ }
922
+ if (has("MinLength")) {
923
+ const n = numericArg2(dec("MinLength"));
924
+ if (n !== null) refinements.push(`.min(${n})`);
925
+ }
926
+ if (has("MaxLength")) {
927
+ const n = numericArg2(dec("MaxLength"));
928
+ if (n !== null) refinements.push(`.max(${n})`);
929
+ }
930
+ if (has("Length")) {
931
+ const [min, max] = numericArgs2(dec("Length"));
932
+ if (min !== null) refinements.push(`.min(${min})`);
933
+ if (max !== null) refinements.push(`.max(${max})`);
934
+ }
935
+ if (has("Min")) {
936
+ const n = numericArg2(dec("Min"));
937
+ if (n !== null) refinements.push(`.min(${n})`);
938
+ }
939
+ if (has("Max")) {
940
+ const n = numericArg2(dec("Max"));
941
+ if (n !== null) refinements.push(`.max(${n})`);
942
+ }
943
+ if (has("IsPositive")) refinements.push(".positive()");
944
+ if (has("IsNegative")) refinements.push(".negative()");
945
+ if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
946
+ if (has("IsEnum")) {
947
+ const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
948
+ if (enumExpr) base = enumExpr;
949
+ }
950
+ if (has("IsIn")) {
951
+ const inExpr = inSchemaFromDecorator2(dec("IsIn"));
952
+ if (inExpr) base = inExpr;
953
+ }
954
+ for (const name of decorators.keys()) {
955
+ if (!KNOWN_DECORATORS2.has(name)) {
956
+ comments.push(`/* @${name}: not translatable to zod (server-only) */`);
957
+ if (!ctx.warnedDecorators.has(name)) {
958
+ ctx.warnedDecorators.add(name);
959
+ const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
960
+ ctx.warnings.push(msg);
961
+ console.warn(`[nestjs-codegen/forms] ${msg}`);
962
+ }
963
+ }
964
+ }
965
+ let expr = base + refinements.join("");
966
+ if (isArrayType && !expr.startsWith("z.array(")) {
967
+ expr = `z.array(${expr})`;
968
+ }
969
+ expr = applyPresence2(expr, decorators);
970
+ if (comments.length > 0) {
971
+ expr = `${expr} ${comments.join(" ")}`;
972
+ }
973
+ return expr;
974
+ }
975
+ function applyPresence2(expr, decorators) {
976
+ if (decorators.has("IsDefined")) return expr;
977
+ if (decorators.has("IsOptional")) return `${expr}.optional()`;
978
+ return expr;
979
+ }
980
+ function baseFromType2(typeText, isArrayType, ctx, classFile) {
981
+ const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
982
+ switch (inner) {
983
+ case "string":
984
+ return "z.string()";
985
+ case "number":
986
+ return "z.number()";
987
+ case "boolean":
988
+ return "z.boolean()";
989
+ case "Date":
990
+ return "z.coerce.date()";
991
+ case "File":
992
+ case "Express.Multer.File":
993
+ return "z.instanceof(File)";
994
+ default:
995
+ return "z.unknown()";
996
+ }
997
+ }
998
+ function ensureStringBase(base) {
999
+ return isStringBase(base) ? base : "z.string()";
1000
+ }
1001
+ function isStringBase(base) {
1002
+ return base.startsWith("z.string(");
1003
+ }
1004
+ function buildNestedReference2(className, fromFile, ctx) {
1005
+ if (ctx.visiting.has(className) || ctx.depth >= 8) {
1006
+ const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
1007
+ ctx.emittedClasses.set(className, reserved);
1008
+ ctx.recursiveSchemas.add(reserved);
1009
+ if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
1010
+ ctx.warnedDecorators.add(`recursive:${reserved}`);
1011
+ const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
1012
+ ctx.warnings.push(msg);
1013
+ console.warn(`[nestjs-codegen/forms] ${msg}`);
1014
+ }
1015
+ return `z.lazy(() => ${reserved})`;
1016
+ }
1017
+ const existing = ctx.emittedClasses.get(className);
1018
+ if (existing) return existing;
1019
+ const schemaName = aliasFor2(className, ctx);
1020
+ const resolved = findType(className, fromFile, ctx.project);
1021
+ if (!resolved || resolved.kind !== "class") {
1022
+ return "z.object({}).passthrough()";
1023
+ }
1024
+ ctx.emittedClasses.set(className, schemaName);
1025
+ ctx.visiting.add(className);
1026
+ ctx.depth += 1;
1027
+ const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
1028
+ ctx.depth -= 1;
1029
+ ctx.visiting.delete(className);
1030
+ ctx.namedNestedSchemas.set(schemaName, childText);
1031
+ return schemaName;
1032
+ }
1033
+ function aliasFor2(className, ctx) {
1034
+ const baseName = `${className}Schema`;
1035
+ let candidate = baseName;
1036
+ let i = 1;
1037
+ const used = new Set(ctx.namedNestedSchemas.keys());
1038
+ for (const v of ctx.emittedClasses.values()) used.add(v);
1039
+ while (used.has(candidate)) {
1040
+ candidate = `${baseName}_${i}`;
1041
+ i += 1;
1042
+ }
1043
+ return candidate;
1044
+ }
1045
+ function firstArg2(decorator) {
1046
+ return decorator?.getArguments()[0];
1047
+ }
1048
+ function firstArgText2(decorator) {
1049
+ const arg = firstArg2(decorator);
1050
+ return arg ? arg.getText() : null;
1051
+ }
1052
+ function numericArg2(decorator) {
1053
+ const arg = firstArg2(decorator);
1054
+ if (arg && Node3.isNumericLiteral(arg)) return arg.getText();
1055
+ return null;
1056
+ }
1057
+ function numericArgs2(decorator) {
1058
+ const args = decorator?.getArguments() ?? [];
1059
+ const num = (n) => n && Node3.isNumericLiteral(n) ? n.getText() : null;
1060
+ return [num(args[0]), num(args[1])];
1061
+ }
1062
+ function messageArg(decorator) {
1063
+ const args = decorator?.getArguments() ?? [];
1064
+ for (const arg of args) {
1065
+ if (Node3.isObjectLiteralExpression(arg)) {
1066
+ for (const prop of arg.getProperties()) {
1067
+ if (Node3.isPropertyAssignment(prop) && prop.getName() === "message") {
1068
+ const init = prop.getInitializer();
1069
+ if (init && Node3.isStringLiteral(init)) {
1070
+ return `{ message: ${init.getText()} }`;
1071
+ }
1072
+ }
1073
+ }
1074
+ }
1075
+ }
1076
+ return "";
1077
+ }
1078
+ function resolveTypeFactoryName2(decorator) {
1079
+ const arg = firstArg2(decorator);
1080
+ if (!arg) return null;
1081
+ if (Node3.isArrowFunction(arg)) {
1082
+ const body = arg.getBody();
1083
+ if (Node3.isIdentifier(body)) return body.getText();
1084
+ }
1085
+ return null;
1086
+ }
1087
+ function singularClassName2(typeText) {
1088
+ const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
1089
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
1090
+ }
1091
+ function enumSchemaFromDecorator2(decorator, classFile, ctx) {
1092
+ const arg = firstArg2(decorator);
1093
+ if (!arg) return null;
1094
+ if (Node3.isIdentifier(arg)) {
1095
+ const name = arg.getText();
1096
+ const resolved = findType(name, classFile, ctx.project);
1097
+ if (resolved && resolved.kind === "enum") {
1098
+ return `z.enum([${resolved.members.join(", ")}])`;
1099
+ }
1100
+ 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().`;
1101
+ if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
1102
+ ctx.warnedDecorators.add(`IsEnum:${name}`);
1103
+ ctx.warnings.push(msg);
1104
+ console.warn(`[nestjs-codegen/forms] ${msg}`);
1105
+ }
1106
+ return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
1107
+ }
1108
+ if (Node3.isObjectLiteralExpression(arg)) {
1109
+ const values = [];
1110
+ for (const p of arg.getProperties()) {
1111
+ if (!Node3.isPropertyAssignment(p)) continue;
1112
+ const init = p.getInitializer();
1113
+ if (init && Node3.isStringLiteral(init)) values.push(init.getText());
1114
+ }
1115
+ if (values.length > 0) return `z.enum([${values.join(", ")}])`;
1116
+ }
1117
+ return null;
1118
+ }
1119
+ function inSchemaFromDecorator2(decorator) {
1120
+ const arg = firstArg2(decorator);
1121
+ if (arg && Node3.isArrayLiteralExpression(arg)) {
1122
+ const elements = arg.getElements();
1123
+ const allStrings = elements.every((e) => Node3.isStringLiteral(e));
1124
+ if (allStrings && elements.length > 0) {
1125
+ return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
1126
+ }
1127
+ if (elements.length > 0) {
1128
+ return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
1129
+ }
1130
+ }
1131
+ return null;
1132
+ }
1133
+
1134
+ // src/discovery/filter-for.ts
1135
+ import {
1136
+ Node as Node5
1137
+ } from "ts-morph";
1138
+
1139
+ // src/discovery/filter-field-types.ts
1140
+ import {
1141
+ Node as Node4,
1142
+ SyntaxKind
1143
+ } from "ts-morph";
1144
+
1145
+ // src/discovery/enum-resolution.ts
1146
+ function resolveEnumValues(name, sourceFile, project) {
1147
+ const resolved = findType(name, sourceFile, project);
1148
+ if (!resolved || resolved.kind !== "enum") return null;
1149
+ let numeric = true;
1150
+ const values = resolved.members.map((m) => {
1151
+ const parsed = JSON.parse(m);
1152
+ if (typeof parsed === "string") numeric = false;
1153
+ return String(parsed);
1154
+ });
1155
+ if (values.length === 0) return null;
1156
+ return { values, numeric };
1157
+ }
1158
+
1159
+ // src/discovery/filter-field-types.ts
1160
+ var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
1161
+ var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
1162
+ var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
1163
+ var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
1164
+ var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
1165
+ function classifyTypeKeyword(raw) {
1166
+ const t = raw.toLowerCase();
1167
+ if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
1168
+ if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
1169
+ if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
1170
+ if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
1171
+ if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
1172
+ return null;
1173
+ }
1174
+ function markNullable(r, nullable) {
1175
+ return nullable ? { ...r, nullable: true } : r;
1176
+ }
1177
+ function classifyTypeNode(typeNode, sourceFile, project, opts) {
1178
+ if (Node4.isUnionTypeNode(typeNode)) {
1179
+ let nullable = false;
1180
+ const stringLits = [];
1181
+ const numberLits = [];
1182
+ const others = [];
1183
+ for (const member of typeNode.getTypeNodes()) {
1184
+ const kind = member.getKind();
1185
+ if (kind === SyntaxKind.NullKeyword || kind === SyntaxKind.UndefinedKeyword) {
1186
+ nullable = true;
1187
+ continue;
1188
+ }
1189
+ if (Node4.isLiteralTypeNode(member)) {
1190
+ const lit = member.getLiteral();
1191
+ if (Node4.isStringLiteral(lit)) {
1192
+ stringLits.push(lit.getLiteralValue());
1193
+ continue;
1194
+ }
1195
+ if (Node4.isNumericLiteral(lit)) {
1196
+ numberLits.push(lit.getText());
1197
+ continue;
1198
+ }
1199
+ if (lit.getKind() === SyntaxKind.NullKeyword) {
1200
+ nullable = true;
1201
+ continue;
1202
+ }
1203
+ }
1204
+ others.push(member);
1205
+ }
1206
+ if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
1207
+ return markNullable({ kind: "string", enumValues: stringLits }, nullable);
1208
+ }
1209
+ if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
1210
+ return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
1211
+ }
1212
+ if (others.length === 1) {
1213
+ const inner = classifyTypeNode(others[0], sourceFile, project, opts);
1214
+ return markNullable(inner, nullable || inner.nullable === true);
1215
+ }
1216
+ return markNullable({ kind: "unknown" }, nullable);
1217
+ }
1218
+ switch (typeNode.getKind()) {
1219
+ case SyntaxKind.StringKeyword:
1220
+ return { kind: "string" };
1221
+ case SyntaxKind.NumberKeyword:
1222
+ return { kind: "number" };
1223
+ case SyntaxKind.BooleanKeyword:
1224
+ return { kind: "boolean" };
1225
+ case SyntaxKind.AnyKeyword:
1226
+ case SyntaxKind.UnknownKeyword:
1227
+ return { kind: "unknown" };
1228
+ default:
1229
+ break;
1230
+ }
1231
+ if (Node4.isTypeReference(typeNode)) {
1232
+ const refName = typeNode.getTypeName().getText();
1233
+ if (refName === "Date") return { kind: "date" };
1234
+ if (refName === "Record" || refName === "Object") return { kind: "json" };
1235
+ const typeRef = opts?.resolveRef?.(refName) ?? null;
1236
+ const en = resolveEnumValues(refName, sourceFile, project);
1237
+ if (en) {
1238
+ const base = en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
1239
+ return typeRef ? { ...base, typeRef } : base;
1240
+ }
1241
+ if (typeRef) return { kind: "unknown", typeRef };
1242
+ return { kind: "unknown" };
1243
+ }
1244
+ if (Node4.isTypeLiteral(typeNode)) return { kind: "json" };
1245
+ return { kind: "unknown" };
1246
+ }
1247
+ function enumFromDecoratorArgs(args, sourceFile, project) {
1248
+ for (const arg of args) {
1249
+ if (Node4.isArrowFunction(arg)) {
1250
+ const body = arg.getBody();
1251
+ if (Node4.isIdentifier(body)) {
1252
+ const en = resolveEnumValues(body.getText(), sourceFile, project);
1253
+ if (en) return en;
1254
+ }
1255
+ }
1256
+ if (Node4.isObjectLiteralExpression(arg)) {
1257
+ const itemsProp = arg.getProperty("items");
1258
+ if (itemsProp && Node4.isPropertyAssignment(itemsProp)) {
1259
+ const init = itemsProp.getInitializer();
1260
+ if (init && Node4.isArrowFunction(init)) {
1261
+ const body = init.getBody();
1262
+ if (Node4.isIdentifier(body)) {
1263
+ const en = resolveEnumValues(body.getText(), sourceFile, project);
1264
+ if (en) return en;
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+ }
1270
+ return null;
1271
+ }
1272
+ function classifyFromColumnDecorator(prop, sourceFile, project) {
1273
+ for (const dec of prop.getDecorators()) {
1274
+ const decName = dec.getName();
1275
+ if (decName !== "Column" && decName !== "Property" && decName !== "Enum") continue;
1276
+ const args = dec.getArguments();
1277
+ if (decName === "Enum") {
1278
+ const en = enumFromDecoratorArgs(args, sourceFile, project);
1279
+ if (en) {
1280
+ return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
1281
+ }
1282
+ return { kind: "string" };
1283
+ }
1284
+ for (const arg of args) {
1285
+ if (Node4.isStringLiteral(arg)) {
1286
+ const raw = arg.getLiteralValue();
1287
+ const kind = classifyTypeKeyword(raw);
1288
+ if (kind) {
1289
+ if (kind === "string" && raw.toLowerCase().includes("enum")) continue;
1290
+ return { kind };
1291
+ }
1292
+ }
1293
+ if (Node4.isObjectLiteralExpression(arg)) {
1294
+ const enumProp = arg.getProperty("enum");
1295
+ if (enumProp && Node4.isPropertyAssignment(enumProp)) {
1296
+ const init = enumProp.getInitializer();
1297
+ if (init && Node4.isIdentifier(init)) {
1298
+ const en = resolveEnumValues(init.getText(), sourceFile, project);
1299
+ if (en) {
1300
+ return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
1301
+ }
1302
+ return { kind: "string" };
1303
+ }
1304
+ }
1305
+ const typeProp = arg.getProperty("type");
1306
+ if (typeProp && Node4.isPropertyAssignment(typeProp)) {
1307
+ const init = typeProp.getInitializer();
1308
+ if (init && Node4.isStringLiteral(init)) {
1309
+ const kind = classifyTypeKeyword(init.getLiteralValue());
1310
+ if (kind) return { kind };
1311
+ }
1312
+ }
1313
+ }
1314
+ }
1315
+ return null;
1316
+ }
1317
+ return null;
1318
+ }
1319
+ function classifyFieldType(prop, sourceFile, project) {
1320
+ let nullable = prop.hasQuestionToken();
1321
+ const typeNode = prop.getTypeNode();
1322
+ if (typeNode) {
1323
+ const r = classifyTypeNode(typeNode, sourceFile, project);
1324
+ if (r.nullable) nullable = true;
1325
+ if (r.kind !== "unknown") return markNullable(r, nullable);
1326
+ }
1327
+ const fromDecorator = classifyFromColumnDecorator(prop, sourceFile, project);
1328
+ if (fromDecorator) {
1329
+ return markNullable(fromDecorator, nullable || fromDecorator.nullable === true);
1330
+ }
1331
+ return markNullable({ kind: "unknown" }, nullable);
1332
+ }
1333
+ function toFilterFieldType(name, r) {
1334
+ const ft = { name, kind: r.kind };
1335
+ if (r.enumValues && r.enumValues.length > 0) ft.enumValues = r.enumValues;
1336
+ if (r.nullable) ft.nullable = true;
1337
+ if (r.numericEnum) ft.numericEnum = true;
1338
+ if (r.typeRef) ft.typeRef = r.typeRef;
1339
+ return ft;
1340
+ }
1341
+
1342
+ // src/discovery/filter-for.ts
1343
+ function classifyFilterForHint(typeInit) {
1344
+ if (Node5.isStringLiteral(typeInit)) {
1345
+ switch (typeInit.getLiteralValue()) {
1346
+ case "string":
1347
+ return { kind: "string" };
1348
+ case "number":
1349
+ return { kind: "number" };
1350
+ case "boolean":
1351
+ return { kind: "boolean" };
1352
+ case "Date":
1353
+ return { kind: "date" };
1354
+ default:
1355
+ return null;
1356
+ }
1357
+ }
1358
+ if (Node5.isArrayLiteralExpression(typeInit)) {
1359
+ const values = [];
1360
+ for (const el of typeInit.getElements()) {
1361
+ if (!Node5.isStringLiteral(el)) return null;
1362
+ values.push(el.getLiteralValue());
1363
+ }
1364
+ if (values.length === 0) return null;
1365
+ return { kind: "string", enumValues: values };
1366
+ }
1367
+ return null;
1368
+ }
1369
+ function classifyFilterForParam(method, sourceFile, project) {
1370
+ const param = method.getParameters()[0];
1371
+ if (!param) return null;
1372
+ const typeNode = param.getTypeNode();
1373
+ if (!typeNode) return null;
1374
+ const wellKnown = ["string", "number", "boolean", "Date", "any", "unknown"];
1375
+ const result = classifyTypeNode(typeNode, sourceFile, project, {
1376
+ resolveRef: (refName) => {
1377
+ if (wellKnown.includes(refName)) return null;
1378
+ if (!findType(refName, sourceFile, project)) return null;
1379
+ return resolveTypeRef(refName, sourceFile, project, {
1380
+ kinds: ["class", "interface", "typeAlias", "enum"],
1381
+ allowBareSpecifier: true
1382
+ });
1383
+ }
1384
+ });
1385
+ if (result.typeRef) return result;
1386
+ return result.kind === "unknown" ? null : result;
1387
+ }
1388
+ function extractFilterForHints(classDecl, project) {
1389
+ const hints = /* @__PURE__ */ new Map();
1390
+ const sourceFile = classDecl.getSourceFile();
1391
+ for (const method of classDecl.getMethods()) {
1392
+ const filterForDec = method.getDecorators().find((d) => d.getName() === "FilterFor");
1393
+ if (!filterForDec) continue;
1394
+ const args = filterForDec.getArguments();
1395
+ const keyArg = args[0];
1396
+ const inputKey = keyArg && Node5.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
1397
+ const optsArg = args[1];
1398
+ if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
1399
+ const typeProp = optsArg.getProperty("type");
1400
+ if (typeProp && Node5.isPropertyAssignment(typeProp)) {
1401
+ const typeInit = typeProp.getInitializer();
1402
+ if (typeInit) {
1403
+ const classified = classifyFilterForHint(typeInit);
1404
+ if (classified) {
1405
+ hints.set(inputKey, classified);
1406
+ continue;
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+ const fromParam = classifyFilterForParam(method, sourceFile, project);
1412
+ if (fromParam) hints.set(inputKey, fromParam);
1413
+ }
1414
+ return hints;
1415
+ }
1416
+ function extractApplyFilterInfo(method, sourceFile, project) {
1417
+ for (const param of method.getParameters()) {
1418
+ const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
1419
+ if (!filterDecorator) continue;
1420
+ const args = filterDecorator.getArguments();
1421
+ if (args.length === 0) continue;
1422
+ const filterClassArg = args[0];
1423
+ if (!filterClassArg || !Node5.isIdentifier(filterClassArg)) continue;
1424
+ let source = "query";
1425
+ const optionsArg = args[1];
1426
+ if (optionsArg && Node5.isObjectLiteralExpression(optionsArg)) {
1427
+ const sourceProp = optionsArg.getProperty("source");
1428
+ if (sourceProp && Node5.isPropertyAssignment(sourceProp)) {
1429
+ const init = sourceProp.getInitializer();
1430
+ if (init && Node5.isStringLiteral(init) && init.getLiteralValue() === "body") {
1431
+ source = "body";
1432
+ }
1433
+ }
1434
+ }
1435
+ const filterClassName = filterClassArg.getText();
1436
+ const resolved = findType(filterClassName, sourceFile, project);
1437
+ if (resolved && resolved.kind === "class") {
1438
+ const classDecl = resolved.decl;
1439
+ let fieldTypes = extractClassPropertyTypes(classDecl, project);
1440
+ if (fieldTypes.length === 0) {
1441
+ fieldTypes = extractFilterableEntityFields(classDecl, project);
1442
+ }
1443
+ const filterForHints = extractFilterForHints(classDecl, project);
1444
+ if (filterForHints.size > 0) {
1445
+ const byName = new Map(fieldTypes.map((f) => [f.name, f]));
1446
+ for (const [key, classified] of filterForHints) {
1447
+ byName.set(key, toFilterFieldType(key, classified));
1448
+ }
1449
+ fieldTypes = [...byName.values()];
1450
+ }
1451
+ if (fieldTypes.length === 0) return null;
1452
+ const fieldNames = fieldTypes.map((f) => f.name);
1453
+ return {
1454
+ fieldNames,
1455
+ fieldTypes,
1456
+ source
1457
+ };
1458
+ }
1459
+ }
1460
+ return null;
1461
+ }
1462
+ var RELATION_DECORATORS = /* @__PURE__ */ new Set(["OneToMany", "ManyToOne", "ManyToMany", "OneToOne"]);
1463
+ function collectEntityFields(entityDecl, sourceFile, project, prefix, visited) {
1464
+ const entityName = entityDecl.getName() ?? "";
1465
+ if (visited.has(entityName)) return [];
1466
+ visited.add(entityName);
1467
+ const fields = [];
1468
+ for (const prop of entityDecl.getProperties()) {
1469
+ const name = prop.getName();
1470
+ if (name.startsWith("$") || name.startsWith("_") || name.startsWith("[")) continue;
1471
+ if (prop.isStatic()) continue;
1472
+ const fullName = prefix ? `${prefix}.${name}` : name;
1473
+ const isRelation = prop.getDecorators().some((d) => RELATION_DECORATORS.has(d.getName()));
1474
+ if (isRelation) {
1475
+ const relEntity = resolveRelationEntity(prop, sourceFile, project);
1476
+ if (relEntity) {
1477
+ fields.push(
1478
+ ...collectEntityFields(relEntity, relEntity.getSourceFile(), project, fullName, visited)
1479
+ );
1480
+ }
1481
+ } else {
1482
+ fields.push(toFilterFieldType(fullName, classifyFieldType(prop, sourceFile, project)));
1483
+ }
1484
+ }
1485
+ return fields;
1486
+ }
1487
+ function resolveRelationEntity(prop, sourceFile, project) {
1488
+ for (const dec of prop.getDecorators()) {
1489
+ if (!RELATION_DECORATORS.has(dec.getName())) continue;
1490
+ const args = dec.getArguments();
1491
+ if (args.length === 0) continue;
1492
+ const arg = args[0];
1493
+ if (Node5.isObjectLiteralExpression(arg)) {
1494
+ const entityProp = arg.getProperty("entity");
1495
+ if (entityProp && Node5.isPropertyAssignment(entityProp)) {
1496
+ const init = entityProp.getInitializer();
1497
+ if (init && Node5.isArrowFunction(init)) {
1498
+ const body = init.getBody();
1499
+ if (Node5.isIdentifier(body)) {
1500
+ const resolved = findType(body.getText(), prop.getSourceFile(), project);
1501
+ if (resolved?.kind === "class") return resolved.decl;
1502
+ }
1503
+ }
1504
+ }
1505
+ }
1506
+ if (Node5.isArrowFunction(arg)) {
1507
+ const body = arg.getBody();
1508
+ if (Node5.isIdentifier(body)) {
1509
+ const resolved = findType(body.getText(), prop.getSourceFile(), project);
1510
+ if (resolved?.kind === "class") return resolved.decl;
1511
+ }
1512
+ }
1513
+ }
1514
+ return null;
1515
+ }
1516
+ function extractClassPropertyTypes(classDecl, project) {
1517
+ const sourceFile = classDecl.getSourceFile();
1518
+ const fields = [];
1519
+ for (const prop of classDecl.getProperties()) {
1520
+ const name = prop.getName();
1521
+ if (name.startsWith("$") || name.startsWith("_")) continue;
1522
+ fields.push(toFilterFieldType(name, classifyFieldType(prop, sourceFile, project)));
1523
+ }
1524
+ return fields;
1525
+ }
1526
+ function extractFilterableEntityFields(filterClass, project) {
1527
+ const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
1528
+ if (!filterableDecorator) return [];
1529
+ const args = filterableDecorator.getArguments();
1530
+ if (args.length === 0) return [];
1531
+ const optionsArg = args[0];
1532
+ if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
1533
+ const entityProp = optionsArg.getProperty("entity");
1534
+ if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
1535
+ const entityInit = entityProp.getInitializer();
1536
+ if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
1537
+ const entityName = entityInit.getText();
1538
+ const filterSourceFile = filterClass.getSourceFile();
1539
+ const resolvedEntity = findType(entityName, filterSourceFile, project);
1540
+ if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
1541
+ const entityDecl = resolvedEntity.decl;
1542
+ const fields = collectEntityFields(
1543
+ entityDecl,
1544
+ entityDecl.getSourceFile(),
1545
+ project,
1546
+ "",
1547
+ /* @__PURE__ */ new Set()
1548
+ );
1549
+ const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
1550
+ if (relationsDecorator) {
1551
+ const relArgs = relationsDecorator.getArguments();
1552
+ if (relArgs.length > 0 && Node5.isObjectLiteralExpression(relArgs[0])) {
1553
+ for (const relProp of relArgs[0].getProperties()) {
1554
+ if (!Node5.isPropertyAssignment(relProp)) continue;
1555
+ const relInit = relProp.getInitializer();
1556
+ if (!relInit || !Node5.isObjectLiteralExpression(relInit)) continue;
1557
+ const keysProp = relInit.getProperty("keys");
1558
+ if (!keysProp || !Node5.isPropertyAssignment(keysProp)) continue;
1559
+ const keysInit = keysProp.getInitializer();
1560
+ if (!keysInit || !Node5.isArrayLiteralExpression(keysInit)) continue;
1561
+ for (const el of keysInit.getElements()) {
1562
+ if (Node5.isStringLiteral(el)) {
1563
+ fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
1564
+ }
1565
+ }
1566
+ }
1567
+ }
1568
+ }
1569
+ return fields;
1570
+ }
1571
+
1572
+ // src/discovery/contracts-fast.ts
1573
+ async function discoverContractsFast(opts) {
1574
+ const { cwd, glob, tsconfig } = opts;
1575
+ const tsconfigPath = tsconfig ? resolve3(tsconfig) : join2(cwd, "tsconfig.json");
1576
+ let project;
1577
+ try {
1578
+ project = new Project({
1579
+ tsConfigFilePath: tsconfigPath,
1580
+ skipAddingFilesFromTsConfig: true,
1581
+ skipLoadingLibFiles: true,
1582
+ skipFileDependencyResolution: true
1583
+ });
1584
+ } catch {
1585
+ project = new Project({
1586
+ skipAddingFilesFromTsConfig: true,
1587
+ skipLoadingLibFiles: true,
1588
+ skipFileDependencyResolution: true,
1589
+ compilerOptions: {
1590
+ allowJs: true,
1591
+ resolveJsonModule: false,
1592
+ strict: false
1593
+ }
1594
+ });
1595
+ }
1596
+ const files = await fg(glob, { cwd, absolute: true, onlyFiles: true });
1597
+ for (const f of files) {
1598
+ project.addSourceFileAtPath(f);
1599
+ }
1600
+ const routes = [];
1601
+ const prevCtx = setDiscoveryContext({
1602
+ projectRoot: cwd,
1603
+ tsconfigPaths: loadTsconfigPaths(tsconfigPath)
1604
+ });
1605
+ try {
1606
+ for (const sourceFile of project.getSourceFiles()) {
1607
+ routes.push(...extractFromSourceFile(sourceFile, project));
1608
+ }
1609
+ } finally {
1610
+ restoreDiscoveryContext(prevCtx);
1611
+ }
1612
+ return routes;
1613
+ }
1614
+ function zodAstToTs(node) {
1615
+ if (!Node6.isCallExpression(node)) return "unknown";
1616
+ const expr = node.getExpression();
1617
+ if (Node6.isPropertyAccessExpression(expr)) {
1618
+ const methodName = expr.getName();
1619
+ const receiver = expr.getExpression();
1620
+ if (methodName === "optional") {
1621
+ return `${zodAstToTs(receiver)} | undefined`;
1622
+ }
1623
+ if (methodName === "nullable") {
1624
+ return `${zodAstToTs(receiver)} | null`;
1625
+ }
1626
+ const args = node.getArguments();
1627
+ switch (methodName) {
1628
+ case "string":
1629
+ return "string";
1630
+ case "number":
1631
+ return "number";
1632
+ case "boolean":
1633
+ return "boolean";
1634
+ case "unknown":
1635
+ return "unknown";
1636
+ case "any":
1637
+ return "unknown";
1638
+ case "literal": {
1639
+ const lit = args[0];
1640
+ if (!lit) return "unknown";
1641
+ if (Node6.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
1642
+ if (Node6.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
1643
+ if (lit.getKind() === SyntaxKind2.TrueKeyword) return "true";
1644
+ if (lit.getKind() === SyntaxKind2.FalseKeyword) return "false";
1645
+ return "unknown";
1646
+ }
1647
+ case "enum": {
1648
+ const arrArg = args[0];
1649
+ if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
1650
+ const members = arrArg.getElements().map(
1651
+ (el) => Node6.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
1652
+ );
1653
+ return members.join(" | ");
1654
+ }
1655
+ case "array": {
1656
+ const inner = args[0];
1657
+ if (!inner) return "unknown";
1658
+ return `Array<${zodAstToTs(inner)}>`;
1659
+ }
1660
+ case "object": {
1661
+ const objArg = args[0];
1662
+ if (!objArg || !Node6.isObjectLiteralExpression(objArg)) return "unknown";
1663
+ const lines = [];
1664
+ for (const prop of objArg.getProperties()) {
1665
+ if (!Node6.isPropertyAssignment(prop)) continue;
1666
+ const key = prop.getName();
1667
+ const valNode = prop.getInitializer();
1668
+ if (!valNode) continue;
1669
+ const tsType = zodAstToTs(valNode);
1670
+ const isOpt = isOptionalChain(valNode);
1671
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
1672
+ }
1673
+ return `{ ${lines.join("; ")} }`;
1674
+ }
1675
+ case "union": {
1676
+ const arrArg = args[0];
1677
+ if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
1678
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
1679
+ }
1680
+ case "record": {
1681
+ const valArg = args.length === 1 ? args[0] : args[1];
1682
+ if (!valArg) return "unknown";
1683
+ return `Record<string, ${zodAstToTs(valArg)}>`;
1684
+ }
1685
+ case "tuple": {
1686
+ const arrArg = args[0];
1687
+ if (!arrArg || !Node6.isArrayLiteralExpression(arrArg)) return "unknown";
1688
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
1689
+ }
1690
+ default:
1691
+ return "unknown";
1692
+ }
1693
+ }
1694
+ return "unknown";
1695
+ }
1696
+ function isOptionalChain(node) {
1697
+ if (!Node6.isCallExpression(node)) return false;
1698
+ const expr = node.getExpression();
1699
+ return Node6.isPropertyAccessExpression(expr) && expr.getName() === "optional";
1700
+ }
1701
+ function decoratorStringArg(decoratorExpr) {
1702
+ if (!decoratorExpr) return void 0;
1703
+ if (Node6.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
1704
+ if (Node6.isArrayLiteralExpression(decoratorExpr)) {
1705
+ const first = decoratorExpr.getElements()[0];
1706
+ if (first && Node6.isStringLiteral(first)) return first.getLiteralValue();
1707
+ }
1708
+ return void 0;
1709
+ }
1710
+ function parseDefineContractCall(callExpr) {
1711
+ if (!Node6.isCallExpression(callExpr)) return null;
1712
+ const callee = callExpr.getExpression();
1713
+ const calleeName = Node6.isIdentifier(callee) ? callee.getText() : Node6.isPropertyAccessExpression(callee) ? callee.getName() : "";
1714
+ if (calleeName !== "defineContract") return null;
1715
+ const args = callExpr.getArguments();
1716
+ const optsArg = args[0];
1717
+ if (!optsArg || !Node6.isObjectLiteralExpression(optsArg)) return null;
1718
+ let query = null;
1719
+ let body = null;
1720
+ let response = "unknown";
1721
+ let bodyZodText = null;
1722
+ let queryZodText = null;
1723
+ for (const prop of optsArg.getProperties()) {
1724
+ if (!Node6.isPropertyAssignment(prop)) continue;
1725
+ const propName = prop.getName();
1726
+ const val = prop.getInitializer();
1727
+ if (!val) continue;
1728
+ if (propName === "query") {
1729
+ query = zodAstToTs(val);
1730
+ queryZodText = val.getText();
1731
+ } else if (propName === "body") {
1732
+ body = zodAstToTs(val);
1733
+ bodyZodText = val.getText();
1734
+ } else if (propName === "response") {
1735
+ response = zodAstToTs(val);
1736
+ }
1737
+ }
1738
+ return { query, body, response, bodyZodText, queryZodText };
1739
+ }
1740
+ function deriveClassSegment(className) {
1741
+ const noSuffix = className.replace(/Controller$/, "");
1742
+ if (!noSuffix) {
1743
+ throw new Error(
1744
+ `Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
1745
+ );
1746
+ }
1747
+ return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
1748
+ }
1749
+ function resolveRouteName(className, methodName, classAs, methodAs) {
1750
+ const classPortion = classAs ?? deriveClassSegment(className);
1751
+ const methodPortion = methodAs ?? methodName;
1752
+ return `${classPortion}.${methodPortion}`;
1753
+ }
1754
+ function joinPaths(prefix, suffix) {
1755
+ if (!prefix && !suffix) return "/";
1756
+ if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
1757
+ if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
1758
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1759
+ const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
1760
+ const combined = p + s;
1761
+ return combined === "" ? "/" : combined;
1762
+ }
1763
+ function extractParams(path) {
1764
+ const matches = path.matchAll(/:(\w+)/g);
1765
+ return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
1766
+ }
1767
+ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
1768
+ if (depth <= 0) return "unknown";
1769
+ if (Node6.isArrayTypeNode(typeNode)) {
1770
+ const elementType = typeNode.getElementTypeNode();
1771
+ return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
1772
+ }
1773
+ if (Node6.isUnionTypeNode(typeNode)) {
1774
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
1775
+ }
1776
+ if (Node6.isIntersectionTypeNode(typeNode)) {
1777
+ return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
1778
+ }
1779
+ if (Node6.isParenthesizedTypeNode(typeNode)) {
1780
+ return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
1781
+ }
1782
+ if (Node6.isTypeReference(typeNode)) {
1783
+ const typeName = typeNode.getTypeName();
1784
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
1785
+ if (name === "string" || name === "number" || name === "boolean") return name;
1786
+ if (name === "Date") return "string";
1787
+ if (name === "unknown" || name === "any" || name === "void") return "unknown";
1788
+ if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
1789
+ return "unknown";
1790
+ if (name === "Ref" || name === "Reference" || name === "LoadedReference" || name === "IdentifiedReference") {
1791
+ const typeArgs = typeNode.getTypeArguments();
1792
+ const firstTypeArg = typeArgs[0];
1793
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1794
+ return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
1795
+ }
1796
+ return "unknown";
1797
+ }
1798
+ if (name === "Collection") {
1799
+ const typeArgs = typeNode.getTypeArguments();
1800
+ const firstTypeArg = typeArgs[0];
1801
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1802
+ return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
1803
+ }
1804
+ return "Array<unknown>";
1805
+ }
1806
+ if (name === "Opt" || name === "Loaded") {
1807
+ const typeArgs = typeNode.getTypeArguments();
1808
+ const firstTypeArg = typeArgs[0];
1809
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1810
+ return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
1811
+ }
1812
+ return "unknown";
1813
+ }
1814
+ if (name === "Array") {
1815
+ const typeArgs = typeNode.getTypeArguments();
1816
+ const firstTypeArg = typeArgs[0];
1817
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1818
+ return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
1819
+ }
1820
+ return "Array<unknown>";
1821
+ }
1822
+ if (["Record", "Omit", "Pick", "Partial", "Required", "Readonly", "Map", "Set"].includes(name)) {
1823
+ return typeNode.getText();
1824
+ }
1825
+ if (name === "Promise") {
1826
+ const typeArgs = typeNode.getTypeArguments();
1827
+ const firstTypeArg = typeArgs[0];
1828
+ if (typeArgs.length > 0 && firstTypeArg !== void 0) {
1829
+ return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
1830
+ }
1831
+ return "unknown";
1832
+ }
1833
+ const resolved = findType(name, sourceFile, project);
1834
+ if (resolved) {
1835
+ return expandTypeDecl(resolved, project, depth - 1);
1836
+ }
1837
+ dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
1838
+ return "unknown";
1839
+ }
1840
+ const kind = typeNode.getKind();
1841
+ if (kind === SyntaxKind2.StringKeyword) return "string";
1842
+ if (kind === SyntaxKind2.NumberKeyword) return "number";
1843
+ if (kind === SyntaxKind2.BooleanKeyword) return "boolean";
1844
+ if (kind === SyntaxKind2.UnknownKeyword) return "unknown";
1845
+ if (kind === SyntaxKind2.AnyKeyword) return "unknown";
1846
+ return typeNode.getText();
1847
+ }
1848
+ function expandTypeDecl(result, project, depth) {
1849
+ if (depth < 0) return "unknown";
1850
+ switch (result.kind) {
1851
+ case "class":
1852
+ return resolvePropertied(result.decl, result.file, project, depth);
1853
+ case "interface":
1854
+ return resolvePropertied(result.decl, result.file, project, depth);
1855
+ case "typeAlias":
1856
+ if (result.typeNode) {
1857
+ return resolveTypeNodeToString(result.typeNode, result.file, project, depth);
1858
+ }
1859
+ return result.text;
1860
+ case "enum":
1861
+ return result.members.join(" | ");
1862
+ }
1863
+ }
1864
+ function resolvePropertied(decl, sourceFile, project, depth) {
1865
+ if (depth < 0) return "unknown";
1866
+ const lines = [];
1867
+ for (const prop of decl.getProperties()) {
1868
+ const propName = prop.getName();
1869
+ const isOptional = prop.hasQuestionToken();
1870
+ const propTypeNode = prop.getTypeNode();
1871
+ let propType = "unknown";
1872
+ if (propTypeNode) {
1873
+ propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth);
1874
+ }
1875
+ lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
1876
+ }
1877
+ return `{ ${lines.join("; ")} }`;
1878
+ }
1879
+ function extractBodyType(method, sourceFile, project) {
1880
+ for (const param of method.getParameters()) {
1881
+ const bodyDecorator = param.getDecorators().find((d) => d.getName() === "Body");
1882
+ if (!bodyDecorator) continue;
1883
+ const bodyArgs = bodyDecorator.getArguments();
1884
+ if (bodyArgs.length > 0) continue;
1885
+ const typeNode = param.getTypeNode();
1886
+ if (typeNode) {
1887
+ return resolveTypeNodeToString(typeNode, sourceFile, project, 3);
1888
+ }
1889
+ }
1890
+ return null;
1891
+ }
1892
+ function extractQueryType(method, sourceFile, project) {
1893
+ for (const param of method.getParameters()) {
1894
+ const queryDecorator = param.getDecorators().find((d) => d.getName() === "Query");
1895
+ if (!queryDecorator) continue;
1896
+ const queryArgs = queryDecorator.getArguments();
1897
+ if (queryArgs.length > 0) continue;
1898
+ const typeNode = param.getTypeNode();
1899
+ if (typeNode) {
1900
+ return resolveTypeNodeToString(typeNode, sourceFile, project, 3);
1901
+ }
1902
+ }
1903
+ return null;
1904
+ }
1905
+ function extractParamsType(method, sourceFile, project) {
1906
+ const entries = [];
1907
+ for (const param of method.getParameters()) {
1908
+ const paramDecorator = param.getDecorators().find((d) => d.getName() === "Param");
1909
+ if (!paramDecorator) continue;
1910
+ const paramArgs = paramDecorator.getArguments();
1911
+ if (paramArgs.length === 0) continue;
1912
+ const nameArg = paramArgs[0];
1913
+ if (!Node6.isStringLiteral(nameArg)) continue;
1914
+ const paramName = nameArg.getLiteralValue();
1915
+ const typeNode = param.getTypeNode();
1916
+ const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
1917
+ entries.push(`${paramName}: ${paramType}`);
1918
+ }
1919
+ return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
1920
+ }
1921
+ function extractResponseType(method, sourceFile, project) {
1922
+ const apiResponseDecorator = method.getDecorator("ApiResponse");
1923
+ if (apiResponseDecorator) {
1924
+ const args = apiResponseDecorator.getArguments();
1925
+ const optsArg = args[0];
1926
+ if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
1927
+ for (const prop of optsArg.getProperties()) {
1928
+ if (!Node6.isPropertyAssignment(prop)) continue;
1929
+ if (prop.getName() !== "type") continue;
1930
+ const val = prop.getInitializer();
1931
+ if (!val) continue;
1932
+ if (Node6.isArrayLiteralExpression(val)) {
1933
+ const elements = val.getElements();
1934
+ const firstEl = elements[0];
1935
+ if (elements.length > 0 && firstEl !== void 0) {
1936
+ const innerType = resolveIdentifierToClassType(firstEl, sourceFile, project, 3);
1937
+ return `Array<${innerType}>`;
1938
+ }
1939
+ return "Array<unknown>";
1940
+ }
1941
+ return resolveIdentifierToClassType(val, sourceFile, project, 3);
1942
+ }
1943
+ }
1944
+ }
1945
+ const returnTypeNode = method.getReturnTypeNode();
1946
+ if (returnTypeNode) {
1947
+ return resolveTypeNodeToString(returnTypeNode, sourceFile, project, 3);
1948
+ }
1949
+ return "unknown";
1950
+ }
1951
+ function resolveIdentifierToClassType(node, sourceFile, project, depth) {
1952
+ if (!Node6.isIdentifier(node)) return "unknown";
1953
+ const name = node.getText();
1954
+ const resolved = findType(name, sourceFile, project);
1955
+ if (resolved) {
1956
+ return expandTypeDecl(resolved, project, depth - 1);
1957
+ }
1958
+ return name;
1959
+ }
1960
+ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
1961
+ return resolveTypeRef(typeNode, sourceFile, project, {
1962
+ kinds: ["class", "interface"],
1963
+ unwrapContainers: true
1964
+ });
1965
+ }
1966
+ function extractDtoContract(method, sourceFile, project) {
1967
+ let body = extractBodyType(method, sourceFile, project);
1968
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
1969
+ const query = extractQueryType(method, sourceFile, project);
1970
+ if (filterInfo && filterInfo.source === "body") {
1971
+ const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
1972
+ body = body ?? bodyType;
1973
+ }
1974
+ const paramsType = extractParamsType(method, sourceFile, project);
1975
+ const response = extractResponseType(method, sourceFile, project);
1976
+ if (body === null && query === null && paramsType === null && response === "unknown" && filterInfo === null) {
1977
+ return null;
1978
+ }
1979
+ let bodyRef = null;
1980
+ let queryRef = null;
1981
+ let responseRef = null;
1982
+ for (const param of method.getParameters()) {
1983
+ if (param.getDecorators().some((d) => d.getName() === "Body") && param.getTypeNode()) {
1984
+ bodyRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
1985
+ }
1986
+ if (param.getDecorators().some((d) => d.getName() === "Query") && param.getTypeNode()) {
1987
+ queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
1988
+ }
1989
+ }
1990
+ const returnTypeNode = method.getReturnTypeNode();
1991
+ if (returnTypeNode) {
1992
+ responseRef = resolveBodyQueryResponseRef(returnTypeNode, sourceFile, project);
1993
+ }
1994
+ if (!responseRef) {
1995
+ const apiResp = method.getDecorator("ApiResponse");
1996
+ if (apiResp) {
1997
+ const args = apiResp.getArguments();
1998
+ const optsArg = args[0];
1999
+ if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
2000
+ for (const prop of optsArg.getProperties()) {
2001
+ if (Node6.isPropertyAssignment(prop) && prop.getName() === "type") {
2002
+ const val = prop.getInitializer();
2003
+ if (val && Node6.isIdentifier(val)) {
2004
+ const name = val.getText();
2005
+ const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
2006
+ if (localDecl?.isExported()) {
2007
+ responseRef = { name, filePath: sourceFile.getFilePath() };
2008
+ } else {
2009
+ const resolved = resolveImportedType(name, sourceFile, project);
2010
+ if (resolved && (resolved.kind === "class" || resolved.kind === "interface") && resolved.decl.isExported()) {
2011
+ responseRef = { name, filePath: resolved.file.getFilePath() };
2012
+ }
2013
+ }
2014
+ }
2015
+ }
2016
+ }
2017
+ }
2018
+ }
2019
+ }
2020
+ let bodyZodText = null;
2021
+ let queryZodText = null;
2022
+ let bodySchema = null;
2023
+ let querySchema = null;
2024
+ const formNested = {};
2025
+ const formWarnings = [];
2026
+ const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
2027
+ if (bodyClass) {
2028
+ const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
2029
+ bodyZodText = result.schemaText;
2030
+ for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
2031
+ formWarnings.push(...result.warnings);
2032
+ bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
2033
+ }
2034
+ const queryClass = resolveParamClass(method, "Query", sourceFile, project);
2035
+ if (queryClass) {
2036
+ const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
2037
+ queryZodText = result.schemaText;
2038
+ for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
2039
+ formWarnings.push(...result.warnings);
2040
+ querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
2041
+ }
2042
+ return {
2043
+ query,
2044
+ body,
2045
+ response,
2046
+ params: paramsType,
2047
+ queryRef,
2048
+ bodyRef,
2049
+ responseRef,
2050
+ filterFields: filterInfo?.fieldNames ?? null,
2051
+ filterFieldTypes: filterInfo?.fieldTypes ?? null,
2052
+ filterSource: filterInfo?.source ?? null,
2053
+ bodyZodText,
2054
+ queryZodText,
2055
+ formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
2056
+ formWarnings,
2057
+ bodySchema,
2058
+ querySchema
2059
+ };
2060
+ }
2061
+ function resolveParamClass(method, decoratorName, sourceFile, project) {
2062
+ for (const param of method.getParameters()) {
2063
+ if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
2064
+ const typeNode = param.getTypeNode();
2065
+ if (!typeNode) continue;
2066
+ const text = typeNode.getText().replace(/\[\]$/, "");
2067
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
2068
+ const resolved = findType(text, sourceFile, project);
2069
+ if (resolved && resolved.kind === "class") {
2070
+ return { decl: resolved.decl, file: resolved.file };
2071
+ }
2072
+ }
2073
+ return null;
2074
+ }
2075
+ var HTTP_METHOD_DECORATORS = {
2076
+ Get: "GET",
2077
+ Post: "POST",
2078
+ Put: "PUT",
2079
+ Patch: "PATCH",
2080
+ Delete: "DELETE",
2081
+ Options: "OPTIONS",
2082
+ Head: "HEAD",
2083
+ All: "ALL"
2084
+ };
2085
+ function extractFromSourceFile(sourceFile, project) {
2086
+ const routes = [];
2087
+ const seenNames = /* @__PURE__ */ new Map();
2088
+ const classes = sourceFile.getClasses();
2089
+ for (const cls of classes) {
2090
+ const controllerDecorator = cls.getDecorator("Controller");
2091
+ if (!controllerDecorator) continue;
2092
+ const controllerArgs = controllerDecorator.getArguments();
2093
+ const firstArg3 = controllerArgs[0];
2094
+ const prefix = decoratorStringArg(firstArg3) ?? "";
2095
+ const className = cls.getName() ?? "Unknown";
2096
+ for (const method of cls.getMethods()) {
2097
+ let httpMethod;
2098
+ let handlerPath = "";
2099
+ for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
2100
+ const httpDecorator = method.getDecorator(decoratorName);
2101
+ if (httpDecorator) {
2102
+ httpMethod = verb;
2103
+ const httpArgs = httpDecorator.getArguments();
2104
+ const pathArg = httpArgs[0];
2105
+ handlerPath = decoratorStringArg(pathArg) ?? "";
2106
+ break;
2107
+ }
2108
+ }
2109
+ const applyContractDecorator = method.getDecorator("ApplyContract");
2110
+ if (applyContractDecorator) {
2111
+ const decoratorArgs = applyContractDecorator.getArguments();
2112
+ const firstDecoratorArg = decoratorArgs[0];
2113
+ if (!firstDecoratorArg) continue;
2114
+ let contractDef = null;
2115
+ let bodyZodRef = null;
2116
+ let queryZodRef = null;
2117
+ if (Node6.isCallExpression(firstDecoratorArg)) {
2118
+ contractDef = parseDefineContractCall(firstDecoratorArg);
2119
+ } else if (Node6.isIdentifier(firstDecoratorArg)) {
2120
+ const identName = firstDecoratorArg.getText();
2121
+ const varDecl = sourceFile.getVariableDeclaration(identName);
2122
+ if (!varDecl) {
2123
+ console.warn(
2124
+ `[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
2125
+ );
2126
+ continue;
2127
+ }
2128
+ const initializer = varDecl.getInitializer();
2129
+ if (!initializer) continue;
2130
+ contractDef = parseDefineContractCall(initializer);
2131
+ if (contractDef && varDecl.isExported()) {
2132
+ const filePath = sourceFile.getFilePath();
2133
+ if (contractDef.body !== null) {
2134
+ bodyZodRef = { name: `${identName}.body`, filePath };
2135
+ }
2136
+ if (contractDef.query !== null) {
2137
+ queryZodRef = { name: `${identName}.query`, filePath };
2138
+ }
2139
+ }
2140
+ } else {
2141
+ console.warn(
2142
+ `[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
2143
+ );
2144
+ continue;
2145
+ }
2146
+ if (!contractDef) continue;
2147
+ if (!httpMethod) continue;
2148
+ const resolvedMethod = httpMethod;
2149
+ const resolvedPath = joinPaths(prefix, handlerPath);
2150
+ const combined = resolvedPath;
2151
+ const params = extractParams(combined);
2152
+ const methodName = method.getName();
2153
+ const classAsDecorator = cls.getDecorator("As");
2154
+ let classAs;
2155
+ if (classAsDecorator) {
2156
+ const classAsArgs = classAsDecorator.getArguments();
2157
+ const classAsName = decoratorStringArg(classAsArgs[0]);
2158
+ if (!classAsName) {
2159
+ throw new Error(
2160
+ `@As decorator on class ${className} must have a non-empty string argument.`
2161
+ );
2162
+ }
2163
+ classAs = classAsName;
2164
+ }
2165
+ const methodAsDecorator = method.getDecorator("As");
2166
+ let methodAs;
2167
+ if (methodAsDecorator) {
2168
+ const methodAsArgs = methodAsDecorator.getArguments();
2169
+ const methodAsName = decoratorStringArg(methodAsArgs[0]);
2170
+ if (!methodAsName) {
2171
+ throw new Error(
2172
+ `@As decorator on ${className}.${methodName} must have a non-empty string argument.`
2173
+ );
2174
+ }
2175
+ methodAs = methodAsName;
2176
+ }
2177
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
2178
+ const qualifiedRef = `${className}.${methodName}`;
2179
+ const existing = seenNames.get(routeName);
2180
+ if (existing !== void 0) {
2181
+ throw new Error(
2182
+ `Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
2183
+ );
2184
+ }
2185
+ seenNames.set(routeName, qualifiedRef);
2186
+ routes.push({
2187
+ method: resolvedMethod,
2188
+ path: combined,
2189
+ name: routeName,
2190
+ params,
2191
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
2192
+ contract: {
2193
+ contractSource: {
2194
+ query: contractDef.query,
2195
+ body: contractDef.body,
2196
+ response: contractDef.response,
2197
+ // Path A: capture both the importable ref and the raw text. The
2198
+ // emitter prefers inlining the text (client-safe — re-exporting from
2199
+ // a controller would drag server-only deps into the client bundle).
2200
+ bodyZodRef,
2201
+ bodyZodText: contractDef.bodyZodText,
2202
+ queryZodRef,
2203
+ queryZodText: contractDef.queryZodText
2204
+ }
2205
+ }
2206
+ });
2207
+ } else {
2208
+ if (!httpMethod) continue;
2209
+ const combined = joinPaths(prefix, handlerPath);
2210
+ const params = extractParams(combined);
2211
+ const methodName = method.getName();
2212
+ const classAsDecorator = cls.getDecorator("As");
2213
+ let classAs;
2214
+ if (classAsDecorator) {
2215
+ const classAsArgs = classAsDecorator.getArguments();
2216
+ const classAsName = decoratorStringArg(classAsArgs[0]);
2217
+ if (classAsName) classAs = classAsName;
2218
+ }
2219
+ const methodAsDecorator = method.getDecorator("As");
2220
+ let methodAs;
2221
+ if (methodAsDecorator) {
2222
+ const methodAsArgs = methodAsDecorator.getArguments();
2223
+ const methodAsName = decoratorStringArg(methodAsArgs[0]);
2224
+ if (methodAsName) methodAs = methodAsName;
2225
+ }
2226
+ const routeName = resolveRouteName(className, methodName, classAs, methodAs);
2227
+ const dtoContract = extractDtoContract(method, sourceFile, project);
2228
+ routes.push({
2229
+ method: httpMethod,
2230
+ path: combined,
2231
+ name: routeName,
2232
+ params,
2233
+ controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
2234
+ contract: {
2235
+ contractSource: {
2236
+ query: dtoContract?.query ?? null,
2237
+ body: dtoContract?.body ?? null,
2238
+ response: dtoContract?.response ?? "unknown",
2239
+ queryRef: dtoContract?.queryRef ?? null,
2240
+ bodyRef: dtoContract?.bodyRef ?? null,
2241
+ responseRef: dtoContract?.responseRef ?? null,
2242
+ filterFields: dtoContract?.filterFields ?? null,
2243
+ filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
2244
+ filterSource: dtoContract?.filterSource ?? null,
2245
+ bodyZodText: dtoContract?.bodyZodText ?? null,
2246
+ queryZodText: dtoContract?.queryZodText ?? null,
2247
+ formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
2248
+ formWarnings: dtoContract?.formWarnings ?? [],
2249
+ bodySchema: dtoContract?.bodySchema ?? null,
2250
+ querySchema: dtoContract?.querySchema ?? null
2251
+ }
2252
+ }
2253
+ });
2254
+ }
2255
+ }
2256
+ }
2257
+ return routes;
2258
+ }
2259
+
2260
+ // src/generate.ts
2261
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
2262
+ import { dirname as dirname3, join as join10 } from "path";
2263
+ import { Project as Project3 } from "ts-morph";
2264
+
2265
+ // src/discovery/pages.ts
2266
+ import { readFile } from "fs/promises";
2267
+ import { join as join3, relative as relative2 } from "path";
2268
+ import fg2 from "fast-glob";
2269
+ var NON_PAGE_FILE_RE = /\.(?:test|spec|stories|story)\.(?:tsx?|jsx?|vue|svelte)$/i;
2270
+ async function discoverPages(opts) {
2271
+ const allFiles = await fg2(opts.glob, { cwd: opts.cwd, absolute: true });
2272
+ const files = allFiles.filter((f) => !NON_PAGE_FILE_RE.test(f));
2273
+ files.sort();
2274
+ const globStatic = opts.glob.split("*")[0]?.replace(/\/$/, "") ?? "";
2275
+ const pagesBase = join3(opts.cwd, globStatic);
2276
+ const out = [];
2277
+ for (const file of files) {
2278
+ const rel = relative2(opts.cwd, file);
2279
+ const nameRel = relative2(pagesBase, file);
2280
+ const name = computeName(nameRel, opts.componentNameStrategy);
2281
+ const source = await readFile(file, "utf8");
2282
+ const propsSource = extractPropsSource(source, opts.propsExport);
2283
+ out.push({ name, absolutePath: file, relativePath: rel, propsSource });
2284
+ }
2285
+ return out;
2286
+ }
2287
+ function computeName(rel, strat) {
2288
+ if (typeof strat === "function") return strat(rel);
2289
+ const noExt = rel.replace(/\.(tsx?|vue|svelte)$/, "");
2290
+ if (strat === "kebab") return noExt.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
2291
+ return noExt;
2292
+ }
2293
+ function extractPropsSource(source, exportName) {
2294
+ const re = new RegExp(`export\\s+type\\s+${exportName}\\s*=\\s*`, "m");
2295
+ const m = source.match(re);
2296
+ if (!m) return null;
2297
+ const start = m.index + m[0].length;
2298
+ let i = start;
2299
+ let depth = 0;
2300
+ let started = false;
2301
+ while (i < source.length) {
2302
+ const c = source[i];
2303
+ if (c === "{") {
2304
+ depth++;
2305
+ started = true;
2306
+ } else if (c === "}") {
2307
+ depth--;
2308
+ if (started && depth === 0) {
2309
+ return source.slice(start, i + 1);
2310
+ }
2311
+ } else if (c === ";" && !started) return source.slice(start, i);
2312
+ i++;
2313
+ }
2314
+ return source.slice(start);
2315
+ }
2316
+
2317
+ // src/discovery/shared-props.ts
2318
+ import { Node as Node7, SyntaxKind as SyntaxKind3 } from "ts-morph";
2319
+ function discoverSharedProps(project, moduleEntry) {
2320
+ try {
2321
+ let sourceFile = project.getSourceFile(moduleEntry);
2322
+ if (!sourceFile) {
2323
+ try {
2324
+ sourceFile = project.addSourceFileAtPath(moduleEntry);
2325
+ } catch {
2326
+ return null;
2327
+ }
2328
+ }
2329
+ const forRootCall = findForRootCall(sourceFile);
2330
+ if (!forRootCall) return null;
2331
+ const initializer = findShareInitializer(forRootCall);
2332
+ if (!initializer) return null;
2333
+ return extractShareType(initializer, sourceFile, project);
2334
+ } catch {
2335
+ return null;
2336
+ }
2337
+ }
2338
+ function findForRootCall(sourceFile) {
2339
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression);
2340
+ for (const call of callExpressions) {
2341
+ const expr = call.getExpression();
2342
+ if (!Node7.isPropertyAccessExpression(expr)) continue;
2343
+ const methodName = expr.getName();
2344
+ const objectExpr = expr.getExpression();
2345
+ if (methodName === "forRoot" && Node7.isIdentifier(objectExpr)) {
2346
+ const name = objectExpr.getText();
2347
+ if (name === "InertiaModule") {
2348
+ return call;
2349
+ }
2350
+ }
2351
+ }
2352
+ return null;
2353
+ }
2354
+ function findShareInitializer(forRootCall) {
2355
+ if (!Node7.isCallExpression(forRootCall)) return null;
2356
+ const args = forRootCall.getArguments();
2357
+ const firstArg3 = args[0];
2358
+ if (!firstArg3 || !Node7.isObjectLiteralExpression(firstArg3)) return null;
2359
+ for (const prop of firstArg3.getProperties()) {
2360
+ if (Node7.isPropertyAssignment(prop) && prop.getName() === "share") {
2361
+ return prop.getInitializer() ?? null;
2362
+ }
2363
+ }
2364
+ return null;
2365
+ }
2366
+ function extractShareType(node, sourceFile, project) {
2367
+ if (Node7.isIdentifier(node)) {
2368
+ const ref = resolveIdentifierToImportRef(node, sourceFile, project);
2369
+ if (ref) {
2370
+ return {
2371
+ typeString: `Awaited<ReturnType<typeof import('${ref.importPath}').${ref.exportName}>>`,
2372
+ properties: null,
2373
+ isImportRef: true
2374
+ };
2375
+ }
2376
+ }
2377
+ if (Node7.isArrowFunction(node)) {
2378
+ const result = extractFromFunctionLike(node, sourceFile);
2379
+ return result ? { ...result, isImportRef: false } : null;
2380
+ }
2381
+ if (Node7.isFunctionExpression(node)) {
2382
+ const result = extractFromFunctionLike(node, sourceFile);
2383
+ return result ? { ...result, isImportRef: false } : null;
2384
+ }
2385
+ if (Node7.isObjectLiteralExpression(node)) {
2386
+ const result = extractFromObjectLiteral(node);
2387
+ return result ? { ...result, isImportRef: false } : null;
2388
+ }
2389
+ return null;
2390
+ }
2391
+ function resolveIdentifierToImportRef(id, sourceFile, project) {
2392
+ if (!Node7.isIdentifier(id)) return null;
2393
+ const name = id.getText();
2394
+ const localFunc = sourceFile.getFunction(name);
2395
+ if (localFunc?.isExported()) {
2396
+ const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
2397
+ return { importPath: filePath, exportName: name };
2398
+ }
2399
+ const localVar = sourceFile.getVariableDeclaration(name);
2400
+ if (localVar?.isExported()) {
2401
+ const filePath = sourceFile.getFilePath().replace(/\.ts$/, "");
2402
+ return { importPath: filePath, exportName: name };
2403
+ }
2404
+ for (const imp of sourceFile.getImportDeclarations()) {
2405
+ for (const named of imp.getNamedImports()) {
2406
+ const importedName = named.getAliasNode()?.getText() ?? named.getName();
2407
+ if (importedName !== name) continue;
2408
+ const resolvedSource = imp.getModuleSpecifierSourceFile();
2409
+ if (!resolvedSource) continue;
2410
+ const originalName = named.getName();
2411
+ const fn = resolvedSource.getFunction(originalName);
2412
+ if (fn?.isExported()) {
2413
+ const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
2414
+ return { importPath: filePath, exportName: originalName };
2415
+ }
2416
+ const v = resolvedSource.getVariableDeclaration(originalName);
2417
+ if (v?.isExported()) {
2418
+ const filePath = resolvedSource.getFilePath().replace(/\.ts$/, "");
2419
+ return { importPath: filePath, exportName: originalName };
2420
+ }
2421
+ }
2422
+ }
2423
+ return null;
2424
+ }
2425
+ function extractFromFunctionLike(node, _sourceFile) {
2426
+ const returnTypeNode = Node7.isArrowFunction(node) || Node7.isFunctionExpression(node) ? node.getReturnTypeNode() : null;
2427
+ if (returnTypeNode) {
2428
+ return extractFromReturnTypeAnnotation(returnTypeNode);
2429
+ }
2430
+ if (Node7.isArrowFunction(node)) {
2431
+ const body = node.getBody();
2432
+ if (Node7.isParenthesizedExpression(body)) {
2433
+ const inner = body.getExpression();
2434
+ if (Node7.isObjectLiteralExpression(inner)) {
2435
+ return extractFromObjectLiteral(inner);
2436
+ }
2437
+ }
2438
+ if (Node7.isObjectLiteralExpression(body)) {
2439
+ return extractFromObjectLiteral(body);
2440
+ }
2441
+ if (Node7.isBlock(body)) {
2442
+ return extractFromBlockReturn(body);
2443
+ }
2444
+ }
2445
+ if (Node7.isFunctionExpression(node)) {
2446
+ const body = node.getBody();
2447
+ if (Node7.isBlock(body)) {
2448
+ return extractFromBlockReturn(body);
2449
+ }
2450
+ }
2451
+ return null;
2452
+ }
2453
+ function extractFromReturnTypeAnnotation(typeNode) {
2454
+ if (Node7.isTypeReference(typeNode)) {
2455
+ const typeName = typeNode.getTypeName();
2456
+ if (Node7.isIdentifier(typeName) && typeName.getText() === "Promise") {
2457
+ const typeArgs = typeNode.getTypeArguments();
2458
+ const firstArg3 = typeArgs[0];
2459
+ if (firstArg3) {
2460
+ return extractFromReturnTypeAnnotation(firstArg3);
2461
+ }
2462
+ return null;
2463
+ }
2464
+ }
2465
+ if (Node7.isTypeLiteral(typeNode)) {
2466
+ const properties = [];
2467
+ for (const member of typeNode.getMembers()) {
2468
+ if (Node7.isPropertySignature(member)) {
2469
+ const name = member.getName();
2470
+ const memberTypeNode = member.getTypeNode();
2471
+ const type = memberTypeNode ? memberTypeNode.getText() : "unknown";
2472
+ properties.push({ name, type });
2473
+ }
2474
+ }
2475
+ if (properties.length === 0) return null;
2476
+ const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
2477
+ return { typeString, properties, isImportRef: false };
2478
+ }
2479
+ return null;
2480
+ }
2481
+ function extractFromBlockReturn(block) {
2482
+ if (!Node7.isBlock(block)) return null;
2483
+ const statements = block.getStatements();
2484
+ for (let i = statements.length - 1; i >= 0; i--) {
2485
+ const stmt = statements[i];
2486
+ if (!Node7.isReturnStatement(stmt)) continue;
2487
+ const expr = stmt.getExpression();
2488
+ if (!expr) continue;
2489
+ if (Node7.isObjectLiteralExpression(expr)) {
2490
+ return extractFromObjectLiteral(expr);
2491
+ }
2492
+ if (Node7.isParenthesizedExpression(expr)) {
2493
+ const inner = expr.getExpression();
2494
+ if (Node7.isObjectLiteralExpression(inner)) {
2495
+ return extractFromObjectLiteral(inner);
2496
+ }
2497
+ }
2498
+ break;
2499
+ }
2500
+ return null;
2501
+ }
2502
+ function extractFromObjectLiteral(objLiteral) {
2503
+ if (!Node7.isObjectLiteralExpression(objLiteral)) return null;
2504
+ const properties = [];
2505
+ for (const prop of objLiteral.getProperties()) {
2506
+ if (!Node7.isPropertyAssignment(prop)) continue;
2507
+ const name = prop.getName();
2508
+ const initializer = prop.getInitializer();
2509
+ if (!initializer) continue;
2510
+ const type = inferExpressionType(initializer);
2511
+ properties.push({ name, type });
2512
+ }
2513
+ if (properties.length === 0) return null;
2514
+ const typeString = `{ ${properties.map((p) => `${p.name}: ${p.type}`).join("; ")} }`;
2515
+ return { typeString, properties, isImportRef: false };
2516
+ }
2517
+ function inferExpressionType(node) {
2518
+ if (Node7.isStringLiteral(node)) return "string";
2519
+ if (Node7.isTemplateExpression(node) || Node7.isNoSubstitutionTemplateLiteral(node))
2520
+ return "string";
2521
+ if (Node7.isNumericLiteral(node)) return "number";
2522
+ if (node.getKind() === SyntaxKind3.TrueKeyword || node.getKind() === SyntaxKind3.FalseKeyword) {
2523
+ return "boolean";
2524
+ }
2525
+ if (node.getKind() === SyntaxKind3.NullKeyword) return "null";
2526
+ if (Node7.isIdentifier(node) && node.getText() === "undefined") return "undefined";
2527
+ if (Node7.isObjectLiteralExpression(node)) {
2528
+ const props = node.getProperties();
2529
+ if (props.length === 0) return "Record<string, unknown>";
2530
+ const entries = [];
2531
+ for (const prop of props) {
2532
+ if (!Node7.isPropertyAssignment(prop)) continue;
2533
+ const key = prop.getName();
2534
+ const init = prop.getInitializer();
2535
+ if (!init) continue;
2536
+ entries.push(`${key}: ${inferExpressionType(init)}`);
2537
+ }
2538
+ if (entries.length === 0) return "Record<string, unknown>";
2539
+ return `{ ${entries.join("; ")} }`;
2540
+ }
2541
+ if (Node7.isArrayLiteralExpression(node)) {
2542
+ const elements = node.getElements();
2543
+ if (elements.length === 0) return "Array<unknown>";
2544
+ const first = elements[0];
2545
+ if (first) return `Array<${inferExpressionType(first)}>`;
2546
+ return "Array<unknown>";
2547
+ }
2548
+ if (Node7.isConditionalExpression(node)) {
2549
+ const whenTrue = inferExpressionType(node.getWhenTrue());
2550
+ const whenFalse = inferExpressionType(node.getWhenFalse());
2551
+ if (whenTrue === whenFalse) return whenTrue;
2552
+ return `${whenTrue} | ${whenFalse}`;
2553
+ }
2554
+ if (Node7.isParenthesizedExpression(node)) {
2555
+ return inferExpressionType(node.getExpression());
2556
+ }
2557
+ if (Node7.isAsExpression(node)) {
2558
+ const typeNode = node.getTypeNode();
2559
+ if (typeNode) return typeNode.getText();
2560
+ }
2561
+ return "unknown";
2562
+ }
2563
+
2564
+ // src/emit/emit-api.ts
2565
+ import { mkdir, writeFile } from "fs/promises";
2566
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
2567
+
2568
+ // src/extension/registry.ts
2569
+ import { Project as Project2 } from "ts-morph";
2570
+ function resolveApiSlots(extensions) {
2571
+ let transport;
2572
+ let transportOwner;
2573
+ let layer;
2574
+ let layerOwner;
2575
+ for (const ext of extensions) {
2576
+ if (ext.apiTransport) {
2577
+ if (transport) {
2578
+ throw new CodegenError(
2579
+ `api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
2580
+ );
2581
+ }
2582
+ transport = ext.apiTransport;
2583
+ transportOwner = ext.name;
2584
+ }
2585
+ if (ext.apiClientLayer) {
2586
+ if (layer) {
2587
+ throw new CodegenError(
2588
+ `api client layer claimed by both "${layerOwner}" and "${ext.name}" \u2014 only one extension may set apiClientLayer.`
2589
+ );
2590
+ }
2591
+ layer = ext.apiClientLayer;
2592
+ layerOwner = ext.name;
2593
+ }
2594
+ }
2595
+ return {
2596
+ ...transport ? { transport } : {},
2597
+ ...layer ? { layer } : {}
2598
+ };
2599
+ }
2600
+ var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
2601
+ function createExtensionContext(config, getRoutes) {
2602
+ let project;
2603
+ return {
2604
+ cwd: config.codegen.cwd,
2605
+ outDir: config.codegen.outDir,
2606
+ config,
2607
+ get routes() {
2608
+ return getRoutes();
2609
+ },
2610
+ project() {
2611
+ if (!project) {
2612
+ project = new Project2({
2613
+ skipAddingFilesFromTsConfig: true,
2614
+ skipLoadingLibFiles: true,
2615
+ skipFileDependencyResolution: true,
2616
+ compilerOptions: { allowJs: true, strict: false }
2617
+ });
2618
+ }
2619
+ return project;
2620
+ }
2621
+ };
2622
+ }
2623
+ async function applyTransformRoutes(routes, extensions, ctx) {
2624
+ let current = routes;
2625
+ for (const ext of extensions) {
2626
+ if (!ext.transformRoutes) continue;
2627
+ const result = await ext.transformRoutes(current, ctx);
2628
+ if (Array.isArray(result)) current = result;
2629
+ }
2630
+ return current;
2631
+ }
2632
+ async function collectEmittedFiles(extensions, ctx) {
2633
+ const files = [];
2634
+ const owners = /* @__PURE__ */ new Map();
2635
+ for (const ext of extensions) {
2636
+ if (!ext.emitFiles) continue;
2637
+ const emitted = await ext.emitFiles(ctx);
2638
+ for (const file of emitted) {
2639
+ const key = file.path.replace(/\\/g, "/").replace(/^\.\//, "");
2640
+ if (CORE_FILES.has(key)) {
2641
+ throw new CodegenError(
2642
+ `Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
2643
+ );
2644
+ }
2645
+ const prev = owners.get(key);
2646
+ if (prev !== void 0) {
2647
+ throw new CodegenError(
2648
+ `Output file "${file.path}" is emitted by both "${prev}" and "${ext.name}". Two extensions cannot write the same file.`
2649
+ );
2650
+ }
2651
+ owners.set(key, ext.name);
2652
+ files.push(file);
2653
+ }
2654
+ }
2655
+ return files;
2656
+ }
2657
+
2658
+ // src/emit/emit-api.ts
2659
+ async function emitApi(routes, outDir, opts = {}) {
2660
+ await mkdir(outDir, { recursive: true });
2661
+ const content = buildApiFile(routes, outDir, opts);
2662
+ await writeFile(join4(outDir, "api.ts"), content, "utf8");
2663
+ }
2664
+ function splitName(name) {
2665
+ return name.split(".");
2666
+ }
2667
+ function toObjectKey3(segment) {
2668
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
2669
+ return segment;
2670
+ }
2671
+ return JSON.stringify(segment);
2672
+ }
2673
+ function toCamelCase(s) {
2674
+ return s.split(/[^a-zA-Z0-9]+/).filter(Boolean).map(
2675
+ (word, i) => i === 0 ? word.charAt(0).toLowerCase() + word.slice(1) : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
2676
+ ).join("");
2677
+ }
2678
+ function validateNameSegment(seg, fullName) {
2679
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(seg)) {
2680
+ const suggested = toCamelCase(seg);
2681
+ throw new Error(
2682
+ `Contract name "${fullName}" has invalid segment "${seg}". Use camelCase identifiers only (lowercase letter then alphanumeric). Suggested: "${suggested}"`
2683
+ );
2684
+ }
2685
+ }
2686
+ function insertIntoTree(tree, segments, leaf, fullName) {
2687
+ const head = segments[0];
2688
+ const rest = segments.slice(1);
2689
+ if (rest.length === 0) {
2690
+ const existing = tree.get(head);
2691
+ if (existing !== void 0 && existing.kind === "branch") {
2692
+ throw new Error(
2693
+ `Contract name conflict: "${fullName}" cannot have both a direct entry and child entries`
2694
+ );
2695
+ }
2696
+ tree.set(head, leaf);
2697
+ } else {
2698
+ const existing = tree.get(head);
2699
+ if (existing !== void 0 && existing.kind === "leaf") {
2700
+ const prefixName = fullName.split(".").slice(0, segments.length - rest.length).join(".");
2701
+ throw new Error(
2702
+ `Contract name conflict: "${prefixName}" cannot have both a direct entry and child entries`
2703
+ );
2704
+ }
2705
+ let branch;
2706
+ if (existing === void 0) {
2707
+ branch = { kind: "branch", children: /* @__PURE__ */ new Map() };
2708
+ tree.set(head, branch);
2709
+ } else {
2710
+ branch = existing;
2711
+ }
2712
+ insertIntoTree(branch.children, rest, leaf, fullName);
2713
+ }
2714
+ }
2715
+ function buildParamsType(params) {
2716
+ const pathParams = params.filter((p) => p.source === "path");
2717
+ if (pathParams.length === 0) return "never";
2718
+ return `{ ${pathParams.map((p) => `${p.name}: string`).join("; ")} }`;
2719
+ }
2720
+ function hasPathParams(params) {
2721
+ return params.some((p) => p.source === "path");
2722
+ }
2723
+ function kindToTs(kind, enumValues, numericEnum) {
2724
+ if (enumValues && enumValues.length > 0) {
2725
+ return enumValues.map((v) => numericEnum ? v : JSON.stringify(v)).join(" | ");
2726
+ }
2727
+ switch (kind) {
2728
+ case "string":
2729
+ return "string";
2730
+ case "number":
2731
+ return "number";
2732
+ case "boolean":
2733
+ return "boolean";
2734
+ case "date":
2735
+ return "Date";
2736
+ case "json":
2737
+ return "Record<string, unknown>";
2738
+ default:
2739
+ return "unknown";
2740
+ }
2741
+ }
2742
+ function emitFieldTypesLiteral(fts) {
2743
+ const entries = fts.map((f) => {
2744
+ let t = f.typeRef ? f.typeRef.name : kindToTs(f.kind, f.enumValues, f.numericEnum);
2745
+ if (f.nullable) t = `${t} | null`;
2746
+ return `${JSON.stringify(f.name)}: ${t}`;
2747
+ });
2748
+ return `{ ${entries.join("; ")} }`;
2749
+ }
2750
+ function emitFilterQueryTypeArgs(c) {
2751
+ const fieldsUnion = (c.contractSource.filterFields ?? []).map((f) => JSON.stringify(f)).join(" | ");
2752
+ const fts = c.contractSource.filterFieldTypes;
2753
+ return fts?.length ? `${fieldsUnion}, ${emitFieldTypesLiteral(fts)}` : fieldsUnion;
2754
+ }
2755
+ function emitFilterQueryType(c) {
2756
+ return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
2757
+ }
2758
+ function buildResponseType(c, outDir) {
2759
+ if (c.controllerRef) {
2760
+ let relPath = relative3(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
2761
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`;
2762
+ return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
2763
+ }
2764
+ const respRef = c.contractSource.responseRef;
2765
+ if (respRef) {
2766
+ return respRef.isArray ? `Array<${respRef.name}>` : respRef.name;
2767
+ }
2768
+ return c.contractSource.response;
2769
+ }
2770
+ function emitRouterTypeBlock(tree, indent, outDir) {
2771
+ const pad = " ".repeat(indent);
2772
+ const lines = [];
2773
+ for (const [key, node] of tree) {
2774
+ const objKey = toObjectKey3(key);
2775
+ if (node.kind === "leaf") {
2776
+ const c = node;
2777
+ const method = c.method.toUpperCase();
2778
+ const queryRef = c.contractSource.queryRef;
2779
+ const isFilterQuery = c.contractSource.filterSource === "query" && !!c.contractSource.filterFields?.length;
2780
+ const query = queryRef ? queryRef.isArray ? `Array<${queryRef.name}>` : queryRef.name : isFilterQuery ? emitFilterQueryType(c) : c.contractSource.query ?? "never";
2781
+ const bodyRef = c.contractSource.bodyRef;
2782
+ const body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
2783
+ const response = buildResponseType(c, outDir);
2784
+ const params = buildParamsType(c.params);
2785
+ const safeMethod = JSON.stringify(method);
2786
+ const safeUrl = JSON.stringify(c.path);
2787
+ const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
2788
+ lines.push(
2789
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; filterFields: ${filterFields} };`
2790
+ );
2791
+ } else {
2792
+ lines.push(`${pad}${objKey}: {`);
2793
+ lines.push(...emitRouterTypeBlock(node.children, indent + 2, outDir));
2794
+ lines.push(`${pad}};`);
2795
+ }
2796
+ }
2797
+ return lines;
2798
+ }
2799
+ function buildRequestModel(c) {
2800
+ const isGet = c.method.toUpperCase() === "GET";
2801
+ const m = c.method.toLowerCase();
2802
+ const flat = JSON.stringify(c.name);
2803
+ const path = JSON.stringify(c.path);
2804
+ const TA = buildRouterTypeAccess(c.name);
2805
+ const withParams = hasPathParams(c.params);
2806
+ const hasBody = !!c.contractSource.bodyRef || c.contractSource.body != null && c.contractSource.body !== "never";
2807
+ const isQuery = isGet || !!c.contractSource.filterFields?.length;
2808
+ const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
2809
+ const fields = [];
2810
+ if (withParams) fields.push(`params: ${TA}['params']`);
2811
+ if (hasQuery) fields.push(`query?: ${TA}['query']`);
2812
+ if (hasBody) fields.push(`body?: ${TA}['body']`);
2813
+ const inputType = fields.length ? `{ ${fields.join("; ")} }` : "Record<string, never>";
2814
+ const urlExpr = withParams ? `route(${flat} as never, input?.params as never) || ${path}` : `route(${flat} as never) || ${path}`;
2815
+ const optsParts = [];
2816
+ if (hasQuery) optsParts.push("query: input?.query as Record<string, unknown> | undefined");
2817
+ if (hasBody) optsParts.push("body: input?.body");
2818
+ const optsExpr = optsParts.length ? `{ ${optsParts.join(", ")} }` : "{}";
2819
+ return {
2820
+ routeName: c.name,
2821
+ method: m,
2822
+ isGet,
2823
+ isQuery,
2824
+ hasParams: withParams,
2825
+ hasBody,
2826
+ inputType,
2827
+ urlExpr,
2828
+ optsExpr,
2829
+ responseType: `${TA}['response']`,
2830
+ bodyType: `${TA}['body']`,
2831
+ queryKeyExpr: `[${flat}, input] as const`
2832
+ };
2833
+ }
2834
+ function renderFetcherRequest(req) {
2835
+ return `fetcher.${req.method}<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
2836
+ }
2837
+ function emitReqHelper() {
2838
+ return [
2839
+ "/** Awaitable request handle. `await api.x.y({...})` runs the fetch; extensions add query/mutation helpers. */",
2840
+ "type __Req<R> = {",
2841
+ " then<T1 = R, T2 = never>(",
2842
+ " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
2843
+ " onrejected?: ((reason: unknown) => T2 | PromiseLike<T2>) | null,",
2844
+ " ): Promise<T1 | T2>;",
2845
+ " catch<T = never>(onrejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<R | T>;",
2846
+ " finally(onfinally?: (() => void) | null): Promise<R>;",
2847
+ " fetch(): Promise<R>;",
2848
+ "};",
2849
+ "function __req<R>(run: () => Promise<R>): __Req<R> {",
2850
+ " let __p: Promise<R> | undefined;",
2851
+ " const __promise = () => {",
2852
+ " __p ??= run();",
2853
+ " return __p;",
2854
+ " };",
2855
+ " return {",
2856
+ " then: (onfulfilled, onrejected) => __promise().then(onfulfilled, onrejected),",
2857
+ " catch: (onrejected) => __promise().catch(onrejected),",
2858
+ " finally: (onfinally) => __promise().finally(onfinally),",
2859
+ " fetch: run,",
2860
+ " };",
2861
+ "}",
2862
+ ""
2863
+ ];
2864
+ }
2865
+ function renderLeaf(pad, objKey, req, requestExpr, members) {
2866
+ const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
2867
+ lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
2868
+ for (const [name, value] of Object.entries(members)) {
2869
+ lines.push(`${pad} ${name}: ${value},`);
2870
+ }
2871
+ lines.push(`${pad}}),`);
2872
+ return lines;
2873
+ }
2874
+ function emitApiObjectBlock(tree, indent, p) {
2875
+ const pad = " ".repeat(indent);
2876
+ const lines = [];
2877
+ for (const [key, node] of tree) {
2878
+ const objKey = toObjectKey3(key);
2879
+ if (node.kind === "branch") {
2880
+ lines.push(`${pad}${objKey}: {`);
2881
+ lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
2882
+ lines.push(`${pad}},`);
2883
+ continue;
2884
+ }
2885
+ const req = buildRequestModel(node);
2886
+ const route = {
2887
+ method: node.method,
2888
+ path: node.path,
2889
+ name: node.name,
2890
+ params: node.params,
2891
+ contract: { contractSource: node.contractSource },
2892
+ ...node.controllerRef ? { controllerRef: node.controllerRef } : {}
2893
+ };
2894
+ const leaf = { route, request: req, requestExpr: "" };
2895
+ leaf.requestExpr = p.transport ? p.transport.renderRequest(leaf, p.ctx) : renderFetcherRequest(req);
2896
+ const members = {};
2897
+ if (p.layer) Object.assign(members, p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx));
2898
+ for (const ext of p.memberExts) {
2899
+ const extra = ext.apiMembers?.(leaf, p.ctx);
2900
+ if (!extra) continue;
2901
+ for (const [name, value] of Object.entries(extra)) {
2902
+ if (name in members) {
2903
+ throw new Error(
2904
+ `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict at "${ext.name}").`
2905
+ );
2906
+ }
2907
+ members[name] = value;
2908
+ }
2909
+ }
2910
+ lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
2911
+ }
2912
+ return lines;
2913
+ }
2914
+ function buildRouterTypeAccess(name) {
2915
+ const segments = splitName(name);
2916
+ return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
2917
+ }
2918
+ function buildApiFile(routes, outDir, opts = {}) {
2919
+ const fetcherImportPath = opts.fetcherImportPath;
2920
+ const extensions = opts.extensions ?? [];
2921
+ const { transport, layer } = resolveApiSlots(extensions);
2922
+ const memberExts = extensions.filter((e) => e.apiMembers);
2923
+ const headerExts = extensions.filter((e) => e.apiHeader);
2924
+ const contracted = routes.filter((r) => r.contract);
2925
+ const ctx = opts.ctx ?? {
2926
+ cwd: outDir ?? "",
2927
+ outDir: outDir ?? "",
2928
+ routes,
2929
+ config: {},
2930
+ project: () => {
2931
+ throw new Error("ExtensionContext.project() is unavailable in standalone emitApi.");
2932
+ }
2933
+ };
2934
+ const importsByFile = /* @__PURE__ */ new Map();
2935
+ for (const r of contracted) {
2936
+ const cs = r.contract?.contractSource;
2937
+ if (!cs) continue;
2938
+ const refs = r.controllerRef ? [cs.queryRef, cs.bodyRef] : [cs.queryRef, cs.bodyRef, cs.responseRef];
2939
+ for (const ref of refs) {
2940
+ if (!ref) continue;
2941
+ let names = importsByFile.get(ref.filePath);
2942
+ if (!names) {
2943
+ names = /* @__PURE__ */ new Set();
2944
+ importsByFile.set(ref.filePath, names);
2945
+ }
2946
+ names.add(ref.name);
2947
+ }
2948
+ for (const ft of cs.filterFieldTypes ?? []) {
2949
+ if (!ft.typeRef) continue;
2950
+ let names = importsByFile.get(ft.typeRef.filePath);
2951
+ if (!names) {
2952
+ names = /* @__PURE__ */ new Set();
2953
+ importsByFile.set(ft.typeRef.filePath, names);
2954
+ }
2955
+ names.add(ft.typeRef.name);
2956
+ }
2957
+ }
2958
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ""];
2959
+ const extImports = [];
2960
+ const seenImports = /* @__PURE__ */ new Set();
2961
+ const pushImport = (imp) => {
2962
+ if (seenImports.has(imp)) return;
2963
+ seenImports.add(imp);
2964
+ extImports.push(imp);
2965
+ };
2966
+ for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
2967
+ for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
2968
+ for (const ext of headerExts) {
2969
+ for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
2970
+ }
2971
+ lines.push(...extImports);
2972
+ lines.push(
2973
+ "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
2974
+ );
2975
+ const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
2976
+ lines.push(`import type { Fetcher } from '${runtimeImport}';`);
2977
+ if (importsByFile.size > 0 && outDir) {
2978
+ lines.push("");
2979
+ const emittedNames = /* @__PURE__ */ new Set();
2980
+ for (const [filePath, names] of importsByFile) {
2981
+ let relPath;
2982
+ if (isAbsolute2(filePath)) {
2983
+ relPath = relative3(outDir, filePath).replace(/\.ts$/, "");
2984
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`;
2985
+ } else {
2986
+ relPath = filePath;
2987
+ }
2988
+ const specifiers = [];
2989
+ for (const name of [...names].sort()) {
2990
+ if (emittedNames.has(name)) {
2991
+ const alias = `${name}_${emittedNames.size}`;
2992
+ specifiers.push(`${name} as ${alias}`);
2993
+ emittedNames.add(alias);
2994
+ } else {
2995
+ specifiers.push(name);
2996
+ emittedNames.add(name);
2997
+ }
2998
+ }
2999
+ lines.push(`import type { ${specifiers.join(", ")} } from '${relPath}';`);
3000
+ }
3001
+ }
3002
+ lines.push("");
3003
+ if (contracted.length === 0) {
3004
+ lines.push("export type ApiRouter = Record<string, never>;");
3005
+ lines.push("");
3006
+ lines.push("export function createApi(_fetcher: Fetcher): Record<string, never> {");
3007
+ lines.push(" return {};");
3008
+ lines.push("}");
3009
+ lines.push("export type Api = ReturnType<typeof createApi>;");
3010
+ lines.push("");
3011
+ lines.push("export namespace Route {");
3012
+ lines.push(" export type Response<K extends string> = never;");
3013
+ lines.push(" export type Body<K extends string> = never;");
3014
+ lines.push(" export type Query<K extends string> = never;");
3015
+ lines.push(" export type Params<K extends string> = never;");
3016
+ lines.push(" export type Error<K extends string> = never;");
3017
+ lines.push(" export type FilterFields<K extends string> = never;");
3018
+ lines.push(
3019
+ " export type Request<K extends string> = { body: never; query: never; params: never };"
3020
+ );
3021
+ lines.push("}");
3022
+ lines.push("");
3023
+ lines.push("export namespace Path {");
3024
+ lines.push(" export type Response<M extends string, U extends string> = never;");
3025
+ lines.push(" export type Body<M extends string, U extends string> = never;");
3026
+ lines.push(" export type Query<M extends string, U extends string> = never;");
3027
+ lines.push(" export type Params<M extends string, U extends string> = never;");
3028
+ lines.push(" export type Error<M extends string, U extends string> = never;");
3029
+ lines.push(" export type FilterFields<M extends string, U extends string> = never;");
3030
+ lines.push("}");
3031
+ lines.push("");
3032
+ return lines.join("\n");
3033
+ }
3034
+ const tree = /* @__PURE__ */ new Map();
3035
+ for (const r of contracted) {
3036
+ const c = r.contract;
3037
+ const name = r.name;
3038
+ const segments = splitName(name);
3039
+ for (const seg of segments) {
3040
+ validateNameSegment(seg, name);
3041
+ }
3042
+ const leaf = {
3043
+ kind: "leaf",
3044
+ method: r.method,
3045
+ name,
3046
+ path: r.path,
3047
+ params: r.params,
3048
+ controllerRef: r.controllerRef,
3049
+ contractSource: c.contractSource
3050
+ };
3051
+ insertIntoTree(tree, segments, leaf, name);
3052
+ }
3053
+ lines.push("export type ApiRouter = {");
3054
+ lines.push(...emitRouterTypeBlock(tree, 2, outDir ?? ""));
3055
+ lines.push("};");
3056
+ lines.push("");
3057
+ lines.push(...emitReqHelper());
3058
+ lines.push("export function createApi(fetcher: Fetcher) {");
3059
+ lines.push(" return {");
3060
+ lines.push(
3061
+ ...emitApiObjectBlock(tree, 4, {
3062
+ ...transport ? { transport } : {},
3063
+ ...layer ? { layer } : {},
3064
+ memberExts,
3065
+ ctx
3066
+ })
3067
+ );
3068
+ lines.push(" };");
3069
+ lines.push("}");
3070
+ lines.push("");
3071
+ lines.push("export type Api = ReturnType<typeof createApi>;");
3072
+ lines.push("");
3073
+ lines.push("type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`");
3074
+ lines.push(" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never");
3075
+ lines.push(" : P extends keyof R ? R[P] : never;");
3076
+ lines.push("");
3077
+ lines.push(
3078
+ "type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
3079
+ );
3080
+ lines.push("");
3081
+ lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
3082
+ lines.push(" ? T");
3083
+ lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
3084
+ lines.push("");
3085
+ lines.push(
3086
+ "type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
3087
+ );
3088
+ lines.push(" ? L extends { method: M; url: U }");
3089
+ lines.push(" ? Field extends keyof L ? L[Field] : never");
3090
+ lines.push(" : never");
3091
+ lines.push(" : never;");
3092
+ lines.push("");
3093
+ lines.push("export namespace Route {");
3094
+ lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
3095
+ lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
3096
+ lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
3097
+ lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
3098
+ lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
3099
+ lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
3100
+ lines.push(" export type Request<K extends string> = {");
3101
+ lines.push(" body: Body<K>;");
3102
+ lines.push(" query: Query<K>;");
3103
+ lines.push(" params: Params<K>;");
3104
+ lines.push(" };");
3105
+ lines.push("}");
3106
+ lines.push("");
3107
+ lines.push("export namespace Path {");
3108
+ lines.push(
3109
+ ' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
3110
+ );
3111
+ lines.push(
3112
+ ' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
3113
+ );
3114
+ lines.push(
3115
+ ' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
3116
+ );
3117
+ lines.push(
3118
+ ' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
3119
+ );
3120
+ lines.push(
3121
+ ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
3122
+ );
3123
+ lines.push(
3124
+ ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
3125
+ );
3126
+ lines.push("}");
3127
+ lines.push("");
3128
+ for (const ext of headerExts) {
3129
+ const statements = ext.apiHeader?.(ctx)?.statements;
3130
+ if (statements?.length) {
3131
+ lines.push(...statements, "");
3132
+ }
3133
+ }
3134
+ return lines.join("\n");
3135
+ }
3136
+
3137
+ // src/emit/emit-cache.ts
3138
+ import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
3139
+ import { join as join5 } from "path";
3140
+ async function emitCache(pages, outDir) {
3141
+ await mkdir2(outDir, { recursive: true });
3142
+ const entries = await Promise.all(
3143
+ pages.map(async (p) => {
3144
+ const s = await stat(p.absolutePath);
3145
+ return {
3146
+ name: p.name,
3147
+ relativePath: p.relativePath,
3148
+ mtime: s.mtime.toISOString()
3149
+ };
3150
+ })
3151
+ );
3152
+ const cache = { pages: entries };
3153
+ await writeFile2(join5(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
3154
+ `, "utf8");
3155
+ }
3156
+
3157
+ // src/emit/emit-forms.ts
3158
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
3159
+ import { join as join6, relative as relative4 } from "path";
3160
+ async function emitForms(routes, outDir, config, adapter) {
3161
+ if (config && config.enabled === false) return false;
3162
+ if (adapter && adapter.name !== "zod") {
3163
+ const content2 = buildFormsFileWithAdapter(routes, adapter);
3164
+ if (content2 === null) return false;
3165
+ await mkdir3(outDir, { recursive: true });
3166
+ await writeFile3(join6(outDir, "forms.ts"), content2, "utf8");
3167
+ return true;
3168
+ }
3169
+ const entries = collectFormEntries(routes);
3170
+ if (entries.length === 0) return false;
3171
+ await mkdir3(outDir, { recursive: true });
3172
+ const content = buildFormsFile(entries, outDir, config);
3173
+ await writeFile3(join6(outDir, "forms.ts"), content, "utf8");
3174
+ return true;
3175
+ }
3176
+ function buildFormsFileWithAdapter(routes, adapter) {
3177
+ const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
3178
+ const methodNameCounts = /* @__PURE__ */ new Map();
3179
+ for (const route of sorted) {
3180
+ const cs = route.contract.contractSource;
3181
+ if (!cs.bodySchema && !cs.querySchema) continue;
3182
+ methodNameCounts.set(
3183
+ deriveBaseName(route.name).method,
3184
+ (methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
3185
+ );
3186
+ }
3187
+ const named = /* @__PURE__ */ new Map();
3188
+ const decls = [];
3189
+ const mapEntries = [];
3190
+ let used = false;
3191
+ for (const route of sorted) {
3192
+ const cs = route.contract.contractSource;
3193
+ const { method, full } = deriveBaseName(route.name);
3194
+ const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
3195
+ const block = [];
3196
+ if (cs.bodyZodText && !cs.bodySchema) {
3197
+ block.push(
3198
+ `// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
3199
+ );
3200
+ }
3201
+ let bodyConst;
3202
+ if (cs.bodySchema) {
3203
+ used = true;
3204
+ const r = adapter.renderModule(cs.bodySchema);
3205
+ for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
3206
+ bodyConst = `${base}BodySchema`;
3207
+ block.push(`export const ${bodyConst} = ${r.schemaText};`);
3208
+ block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
3209
+ }
3210
+ if (cs.querySchema) {
3211
+ used = true;
3212
+ const r = adapter.renderModule(cs.querySchema);
3213
+ for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
3214
+ const queryConst = `${base}QuerySchema`;
3215
+ block.push(`export const ${queryConst} = ${r.schemaText};`);
3216
+ block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
3217
+ }
3218
+ if (block.length === 0) continue;
3219
+ decls.push(`// ${route.name}`, ...block, "");
3220
+ if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
3221
+ }
3222
+ if (!used) return null;
3223
+ const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
3224
+ for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
3225
+ lines.push("");
3226
+ if (named.size > 0) {
3227
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
3228
+ for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
3229
+ lines.push("");
3230
+ }
3231
+ lines.push(...decls);
3232
+ lines.push("/** Route name \u2192 body schema map. */");
3233
+ lines.push("export const formSchemas = {");
3234
+ lines.push(...mapEntries);
3235
+ lines.push("} as const;");
3236
+ lines.push("");
3237
+ return lines.join("\n");
3238
+ }
3239
+ function hasSchema(src) {
3240
+ return !!src && (src.ref !== null || src.text !== null);
3241
+ }
3242
+ function pascal(segment) {
3243
+ return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
3244
+ }
3245
+ function deriveBaseName(routeName) {
3246
+ const segments = routeName.split(".");
3247
+ const method = pascal(segments[segments.length - 1] ?? routeName);
3248
+ const full = segments.map(pascal).join("");
3249
+ return { method, full };
3250
+ }
3251
+ function collectFormEntries(routes) {
3252
+ const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
3253
+ const methodNameCounts = /* @__PURE__ */ new Map();
3254
+ const candidates = [];
3255
+ for (const route of sorted) {
3256
+ const cs = route.contract.contractSource;
3257
+ const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
3258
+ const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
3259
+ if (!hasSchema(body) && !hasSchema(query)) continue;
3260
+ const { method, full } = deriveBaseName(route.name);
3261
+ methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
3262
+ candidates.push({ route, method, full });
3263
+ }
3264
+ const entries = [];
3265
+ for (const { route, method, full } of candidates) {
3266
+ const cs = route.contract.contractSource;
3267
+ const collision = (methodNameCounts.get(method) ?? 0) > 1;
3268
+ const baseName = collision ? full : method;
3269
+ const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
3270
+ const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
3271
+ entries.push({
3272
+ routeName: route.name,
3273
+ baseName,
3274
+ body: hasSchema(body) ? body : void 0,
3275
+ query: hasSchema(query) ? query : void 0,
3276
+ nestedSchemas: cs.formNestedSchemas ?? null,
3277
+ warnings: cs.formWarnings ?? []
3278
+ });
3279
+ }
3280
+ return entries;
3281
+ }
3282
+ function relImport(outDir, filePath) {
3283
+ let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
3284
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`;
3285
+ return relPath;
3286
+ }
3287
+ function refRootIdentifier(refName) {
3288
+ return refName.split(".")[0] ?? refName;
3289
+ }
3290
+ function buildFormsFile(entries, outDir, config) {
3291
+ const zodImport = config?.zodImport ?? "zod";
3292
+ const lines = [
3293
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
3294
+ `import { z } from '${zodImport}';`
3295
+ ];
3296
+ const importsByFile = /* @__PURE__ */ new Map();
3297
+ const refAlias = /* @__PURE__ */ new Map();
3298
+ for (const entry of entries) {
3299
+ for (const src of [entry.body, entry.query]) {
3300
+ if (src?.ref && !src.text) {
3301
+ const root = refRootIdentifier(src.ref.name);
3302
+ const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
3303
+ set.add(root);
3304
+ importsByFile.set(src.ref.filePath, set);
3305
+ }
3306
+ }
3307
+ }
3308
+ if (importsByFile.size > 0) {
3309
+ const emitted = /* @__PURE__ */ new Set();
3310
+ for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
3311
+ const relPath = relImport(outDir, filePath);
3312
+ const specifiers = [];
3313
+ for (const root of [...roots].sort()) {
3314
+ if (emitted.has(root)) {
3315
+ const alias = `${root}_${emitted.size}`;
3316
+ specifiers.push(`${root} as ${alias}`);
3317
+ emitted.add(alias);
3318
+ refAlias.set(`${filePath}\0${root}`, alias);
3319
+ } else {
3320
+ specifiers.push(root);
3321
+ emitted.add(root);
3322
+ refAlias.set(`${filePath}\0${root}`, root);
3323
+ }
3324
+ }
3325
+ lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
3326
+ }
3327
+ }
3328
+ lines.push("");
3329
+ const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
3330
+ if (globalSchemas.size > 0) {
3331
+ lines.push("// Hoisted nested schemas (shared across endpoints).");
3332
+ for (const [name, text] of globalSchemas) {
3333
+ lines.push(`const ${name} = ${text};`);
3334
+ }
3335
+ lines.push("");
3336
+ }
3337
+ const mapEntries = [];
3338
+ for (const entry of entries) {
3339
+ lines.push(`// ${entry.routeName}`);
3340
+ if (entry.warnings && entry.warnings.length > 0) {
3341
+ for (const w of entry.warnings) {
3342
+ lines.push(`// warning: ${w}`);
3343
+ }
3344
+ }
3345
+ const rename = renamesByEntry.get(entry) ?? null;
3346
+ if (entry.body) {
3347
+ const schemaName = `${entry.baseName}BodySchema`;
3348
+ const typeName = `${entry.baseName}Body`;
3349
+ const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
3350
+ lines.push(`export const ${schemaName} = ${text};`);
3351
+ lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
3352
+ mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
3353
+ }
3354
+ if (entry.query) {
3355
+ const schemaName = `${entry.baseName}QuerySchema`;
3356
+ const typeName = `${entry.baseName}Query`;
3357
+ const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
3358
+ lines.push(`export const ${schemaName} = ${text};`);
3359
+ lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
3360
+ }
3361
+ lines.push("");
3362
+ }
3363
+ lines.push("/** Route name \u2192 body schema map. */");
3364
+ lines.push("export const formSchemas = {");
3365
+ lines.push(...mapEntries);
3366
+ lines.push("} as const;");
3367
+ lines.push("");
3368
+ return lines.join("\n");
3369
+ }
3370
+ function applyRenames(text, renames) {
3371
+ if (!renames || renames.size === 0) return text;
3372
+ let out = text;
3373
+ for (const [from, to] of renames) {
3374
+ if (from === to) continue;
3375
+ out = out.replace(new RegExp(`\\b${escapeRegExp(from)}\\b`, "g"), to);
3376
+ }
3377
+ return out;
3378
+ }
3379
+ function escapeRegExp(s) {
3380
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3381
+ }
3382
+ function isSelfReferential(name, text) {
3383
+ return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
3384
+ }
3385
+ function planNestedSchemas(entries) {
3386
+ const globalSchemas = /* @__PURE__ */ new Map();
3387
+ const renamesByEntry = /* @__PURE__ */ new Map();
3388
+ for (const entry of entries) {
3389
+ if (!entry.nestedSchemas) continue;
3390
+ const local = Object.entries(entry.nestedSchemas);
3391
+ if (local.length === 0) continue;
3392
+ const rename = /* @__PURE__ */ new Map();
3393
+ for (const [name] of local) rename.set(name, name);
3394
+ const textFor = (name) => {
3395
+ const raw = entry.nestedSchemas?.[name] ?? "";
3396
+ return applyRenames(raw, rename);
3397
+ };
3398
+ let changed = true;
3399
+ let guard = 0;
3400
+ while (changed && guard < local.length + 2) {
3401
+ changed = false;
3402
+ guard += 1;
3403
+ for (const [name] of local) {
3404
+ const finalName = rename.get(name) ?? name;
3405
+ const text = textFor(name);
3406
+ const existing = globalSchemas.get(finalName);
3407
+ if (existing === void 0) continue;
3408
+ if (existing === text) continue;
3409
+ let i = 2;
3410
+ let candidate = `${name}_${i}`;
3411
+ while (globalSchemas.has(candidate) && globalSchemas.get(candidate) !== textFor(name) || [...rename.values()].includes(candidate)) {
3412
+ i += 1;
3413
+ candidate = `${name}_${i}`;
3414
+ }
3415
+ rename.set(name, candidate);
3416
+ changed = true;
3417
+ }
3418
+ }
3419
+ for (const [name] of local) {
3420
+ const finalName = rename.get(name) ?? name;
3421
+ let text = textFor(name);
3422
+ if (isSelfReferential(finalName, text)) {
3423
+ text = "z.unknown() /* recursive type \u2014 not expanded */";
3424
+ }
3425
+ const existing = globalSchemas.get(finalName);
3426
+ if (existing === void 0) {
3427
+ globalSchemas.set(finalName, text);
3428
+ }
3429
+ }
3430
+ renamesByEntry.set(entry, rename);
3431
+ }
3432
+ return { globalSchemas, renamesByEntry };
3433
+ }
3434
+ function renderSchema(src, outDir, refAlias) {
3435
+ if (src.text) return src.text;
3436
+ if (src.ref) {
3437
+ const root = refRootIdentifier(src.ref.name);
3438
+ const alias = refAlias.get(`${src.ref.filePath}\0${root}`) ?? root;
3439
+ const member = src.ref.name.slice(root.length);
3440
+ return `${alias}${member}`;
3441
+ }
3442
+ return "z.unknown()";
3443
+ }
3444
+
3445
+ // src/emit/emit-index.ts
3446
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
3447
+ import { join as join7 } from "path";
3448
+ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
3449
+ await mkdir4(outDir, { recursive: true });
3450
+ const exports = ["export * from './pages.js';", "export * from './routes.js';"];
3451
+ if (hasContracts) {
3452
+ exports.push("export * from './api.js';");
3453
+ }
3454
+ if (hasForms) {
3455
+ exports.push("export * from './forms.js';");
3456
+ }
3457
+ const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
3458
+ "\n"
3459
+ );
3460
+ await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
3461
+ }
3462
+
3463
+ // src/emit/emit-pages.ts
3464
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
3465
+ import { join as join8, relative as relative5 } from "path";
3466
+ async function emitPages(pages, outDir, _options = {}) {
3467
+ await mkdir5(outDir, { recursive: true });
3468
+ const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
3469
+ const augBody = pages.map((p) => {
3470
+ const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
3471
+ const valueType = buildAugmentationType(p, outDir);
3472
+ return ` ${key}: ${valueType};`;
3473
+ }).join("\n");
3474
+ const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
3475
+ const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
3476
+ const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
3477
+
3478
+ export type InertiaPageName = ${pageNameUnion};
3479
+ ${propsHelper}
3480
+ declare module '@dudousxd/nestjs-inertia' {
3481
+ interface InertiaPages {
3482
+ ${augBody}
3483
+ }
3484
+ ${sharedPropsBlock}}
3485
+ `;
3486
+ await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
3487
+ }
3488
+ function buildSharedPropsBlock(sharedProps) {
3489
+ if (!sharedProps) return "";
3490
+ if (sharedProps.isImportRef) {
3491
+ return `
3492
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
3493
+ interface InertiaSharedProps extends ${sharedProps.typeString} {}
3494
+ `;
3495
+ }
3496
+ if (!sharedProps.properties || sharedProps.properties.length === 0) return "";
3497
+ const propsBody = sharedProps.properties.map((p) => ` ${p.name}: ${p.type};`).join("\n");
3498
+ return `
3499
+ interface InertiaSharedProps {
3500
+ ${propsBody}
3501
+ }
3502
+ `;
3503
+ }
3504
+ function buildAugmentationType(page, outDir) {
3505
+ let importPath = relative5(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
3506
+ if (!importPath.startsWith(".")) {
3507
+ importPath = `./${importPath}`;
3508
+ }
3509
+ return `Parameters<typeof import('${importPath}').default>[0]`;
3510
+ }
3511
+ function needsQuotes(name) {
3512
+ return !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
3513
+ }
3514
+
3515
+ // src/emit/emit-routes.ts
3516
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
3517
+ import { join as join9 } from "path";
3518
+ async function emitRoutes(routes, outDir) {
3519
+ await mkdir6(outDir, { recursive: true });
3520
+ const content = buildRoutesFile(routes);
3521
+ await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
3522
+ }
3523
+ function buildRoutesFile(routes) {
3524
+ if (routes.length === 0) {
3525
+ return buildEmpty();
3526
+ }
3527
+ const entries = routes.map((r) => ` ${JSON.stringify(r.name)}: ${JSON.stringify(r.path)},`).join("\n");
3528
+ const routeNameUnion = routes.map((r) => ` | ${JSON.stringify(r.name)}`).join("\n");
3529
+ const lines = [
3530
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
3531
+ "",
3532
+ "/** Map of route name \u2192 path pattern. */",
3533
+ "export const ROUTES = {",
3534
+ entries,
3535
+ "} as const;",
3536
+ "",
3537
+ "/** Union of all known route names. */",
3538
+ "export type RouteName =",
3539
+ `${routeNameUnion};`,
3540
+ "",
3541
+ "/**",
3542
+ " * Extracts path-parameter names from a route path string.",
3543
+ ' * `ExtractParams<"/users/:id">` \u2192 `"id"`',
3544
+ " */",
3545
+ "export type ExtractParams<Path extends string> =",
3546
+ " Path extends `${string}:${infer Param}/${infer Rest}`",
3547
+ " ? Param | ExtractParams<`/${Rest}`>",
3548
+ " : Path extends `${string}:${infer Param}`",
3549
+ " ? Param",
3550
+ " : never;",
3551
+ "",
3552
+ "/**",
3553
+ " * Mapped type: given a `RouteName`, returns an object with each path param as `string`.",
3554
+ ' * `RouteParams<"UsersController.show">` \u2192 `{ id: string }`',
3555
+ ' * `RouteParams<"UsersController.list">` \u2192 `Record<string, never>`',
3556
+ " */",
3557
+ "export type RouteParams<K extends RouteName> =",
3558
+ " ExtractParams<(typeof ROUTES)[K]> extends never",
3559
+ " ? Record<string, never>",
3560
+ " : { [P in ExtractParams<(typeof ROUTES)[K]>]: string };",
3561
+ "",
3562
+ "/**",
3563
+ " * Map of every route name to its resolved params object.",
3564
+ " * Use this for `InertiaRegistry` module augmentation.",
3565
+ " */",
3566
+ "export type RouteParamsMap = { [K in RouteName]: RouteParams<K> };",
3567
+ "",
3568
+ "/**",
3569
+ " * Build a URL from a named route, interpolating path params and appending query string.",
3570
+ " *",
3571
+ " * @example",
3572
+ " * route('UsersController.show', { id: '42' }) // \u2192 '/users/42'",
3573
+ " * route('UsersController.list') // \u2192 '/users'",
3574
+ " * route('users.list', undefined, { active: true }) // \u2192 '/api/users?active=true'",
3575
+ " */",
3576
+ "export function route<K extends RouteName>(",
3577
+ " name: K,",
3578
+ " ...args: ExtractParams<(typeof ROUTES)[K]> extends never",
3579
+ " ? [params?: undefined, query?: Record<string, unknown>]",
3580
+ " : [params: RouteParams<K>, query?: Record<string, unknown>]",
3581
+ "): string {",
3582
+ " const [params, query] = args as [Record<string, string> | undefined, Record<string, unknown> | undefined];",
3583
+ " const path: string | undefined = ROUTES[name as RouteName];",
3584
+ " if (path === undefined) {",
3585
+ " const available = Object.keys(ROUTES).join(', ');",
3586
+ " throw new Error(",
3587
+ ' `[nestjs-codegen] Route "${String(name)}" does not exist.\\n\\n` +',
3588
+ " `Available routes: ${available}\\n\\n` +",
3589
+ " `This usually means:\\n` +",
3590
+ " ` - The route name has a typo\\n` +",
3591
+ " ` - The controller hasn't been scanned by codegen yet (run: nestjs-codegen codegen)\\n` +",
3592
+ " ` - The @As() decorator changed the route name\\n`",
3593
+ " );",
3594
+ " }",
3595
+ " let resolvedPath: string = path;",
3596
+ " if (params) {",
3597
+ " resolvedPath = resolvedPath.replace(/:([^/]+)/g, (_, key) => {",
3598
+ " const val = params[key];",
3599
+ " if (val === undefined) throw new Error(`Missing route param: ${key}`);",
3600
+ " return encodeURIComponent(val);",
3601
+ " });",
3602
+ " }",
3603
+ " if (query && Object.keys(query).length > 0) {",
3604
+ " const qs = new URLSearchParams();",
3605
+ " for (const [k, v] of Object.entries(query)) {",
3606
+ " if (v !== undefined && v !== null) qs.append(k, String(v));",
3607
+ " }",
3608
+ " const qStr = qs.toString();",
3609
+ " if (qStr) {",
3610
+ " const sep = resolvedPath.includes('?') ? '&' : '?';",
3611
+ " resolvedPath = `${resolvedPath}${sep}${qStr}`;",
3612
+ " }",
3613
+ " }",
3614
+ " return resolvedPath;",
3615
+ "}",
3616
+ ""
3617
+ ];
3618
+ return lines.join("\n");
3619
+ }
3620
+ function buildEmpty() {
3621
+ return [
3622
+ "// Generated by @dudousxd/nestjs-codegen. Do not edit.",
3623
+ "",
3624
+ "export const ROUTES = {} as const;",
3625
+ "export type RouteName = never;",
3626
+ "export type ExtractParams<_Path extends string> = never;",
3627
+ "export type RouteParams<_K extends RouteName> = Record<string, never>;",
3628
+ 'export function route(_name: never, _params?: undefined, _query?: Record<string, unknown>): string { return ""; }',
3629
+ ""
3630
+ ].join("\n");
3631
+ }
3632
+
3633
+ // src/generate.ts
3634
+ async function generate(config, inputRoutes = []) {
3635
+ const extensions = config.extensions ?? [];
3636
+ let routes = inputRoutes;
3637
+ const ctx = createExtensionContext(config, () => routes);
3638
+ if (extensions.length > 0) {
3639
+ routes = await applyTransformRoutes(routes, extensions, ctx);
3640
+ }
3641
+ if (config.pages) {
3642
+ const pagesConfig = config.pages;
3643
+ const pages = await discoverPages({
3644
+ glob: pagesConfig.glob,
3645
+ cwd: config.codegen.cwd,
3646
+ propsExport: pagesConfig.propsExport,
3647
+ componentNameStrategy: pagesConfig.componentNameStrategy
3648
+ });
3649
+ let sharedProps = null;
3650
+ if (config.app?.moduleEntry) {
3651
+ try {
3652
+ const tsconfigPath = config.app.tsconfig ?? join10(config.codegen.cwd, "tsconfig.json");
3653
+ let project;
3654
+ try {
3655
+ project = new Project3({
3656
+ tsConfigFilePath: tsconfigPath,
3657
+ skipAddingFilesFromTsConfig: true,
3658
+ skipLoadingLibFiles: true,
3659
+ skipFileDependencyResolution: true
3660
+ });
3661
+ } catch {
3662
+ project = new Project3({
3663
+ skipAddingFilesFromTsConfig: true,
3664
+ skipLoadingLibFiles: true,
3665
+ skipFileDependencyResolution: true,
3666
+ compilerOptions: { allowJs: true, strict: false }
3667
+ });
3668
+ }
3669
+ sharedProps = discoverSharedProps(project, config.app.moduleEntry);
3670
+ } catch {
3671
+ }
3672
+ }
3673
+ await emitPages(pages, config.codegen.outDir, {
3674
+ propsExport: pagesConfig.propsExport,
3675
+ sharedProps
3676
+ });
3677
+ await emitCache(pages, config.codegen.outDir);
3678
+ }
3679
+ const hasRoutes = routes.length > 0;
3680
+ const hasContracts = routes.some((r) => r.contract);
3681
+ if (hasRoutes) {
3682
+ await emitRoutes(routes, config.codegen.outDir);
3683
+ }
3684
+ if (hasContracts) {
3685
+ await emitApi(routes, config.codegen.outDir, {
3686
+ ...config.fetcher?.importPath ? { fetcherImportPath: config.fetcher.importPath } : {},
3687
+ extensions,
3688
+ ctx
3689
+ });
3690
+ }
3691
+ const hasForms = await emitForms(routes, config.codegen.outDir, config.forms, config.validation);
3692
+ await emitIndex(config.codegen.outDir, hasContracts, hasForms);
3693
+ if (extensions.length > 0) {
3694
+ const extraFiles = await collectEmittedFiles(extensions, ctx);
3695
+ for (const file of extraFiles) {
3696
+ const dest = join10(config.codegen.outDir, file.path);
3697
+ await mkdir7(dirname3(dest), { recursive: true });
3698
+ await writeFile7(dest, file.contents, "utf8");
3699
+ }
3700
+ }
3701
+ }
3702
+
3703
+ // src/watch/lock-file.ts
3704
+ import { open } from "fs/promises";
3705
+ import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
3706
+ import { join as join11 } from "path";
3707
+ var LOCK_FILE = ".watcher.lock";
3708
+ function isProcessAlive(pid) {
3709
+ try {
3710
+ process.kill(pid, 0);
3711
+ return true;
3712
+ } catch {
3713
+ return false;
3714
+ }
3715
+ }
3716
+ async function acquireLock(outDir) {
3717
+ await mkdir8(outDir, { recursive: true });
3718
+ const lockPath = join11(outDir, LOCK_FILE);
3719
+ const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
3720
+ try {
3721
+ const fd = await open(lockPath, "wx");
3722
+ await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
3723
+ `, "utf8");
3724
+ await fd.close();
3725
+ } catch (err) {
3726
+ if (err.code === "EEXIST") {
3727
+ try {
3728
+ const raw = await readFile2(lockPath, "utf8");
3729
+ const existing = JSON.parse(raw);
3730
+ if (isProcessAlive(existing.pid)) return null;
3731
+ await unlink(lockPath);
3732
+ return acquireLock(outDir);
3733
+ } catch {
3734
+ return null;
3735
+ }
3736
+ }
3737
+ return null;
3738
+ }
3739
+ return {
3740
+ release: async () => {
3741
+ try {
3742
+ await unlink(lockPath);
3743
+ } catch {
3744
+ }
3745
+ }
3746
+ };
3747
+ }
3748
+
3749
+ // src/watch/watcher.ts
3750
+ var PAGES_DEBOUNCE_MS = 150;
3751
+ var NO_OP_WATCHER = { close: async () => {
3752
+ } };
3753
+ async function watch(config, onChange) {
3754
+ const lock = await acquireLock(config.codegen.outDir);
3755
+ if (lock === null) {
3756
+ let holderPid = "unknown";
3757
+ try {
3758
+ const raw = await readFile3(join12(config.codegen.outDir, ".watcher.lock"), "utf8");
3759
+ const data = JSON.parse(raw);
3760
+ if (data.pid !== void 0) holderPid = String(data.pid);
3761
+ } catch {
3762
+ }
3763
+ console.warn(
3764
+ `[nestjs-codegen] auto-watch skipped \u2014 another process (PID ${holderPid}) is already running the watcher in ${config.codegen.outDir}. Files will continue to regenerate from that process. To take over, stop the other watcher.`
3765
+ );
3766
+ return NO_OP_WATCHER;
3767
+ }
3768
+ try {
3769
+ const initialRoutes = await discoverContractsFast({
3770
+ cwd: config.codegen.cwd,
3771
+ glob: config.contracts.glob,
3772
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3773
+ });
3774
+ await generate(config, initialRoutes);
3775
+ } catch (err) {
3776
+ console.warn(
3777
+ `[nestjs-codegen] Initial route discovery failed, falling back to pages-only: ${err instanceof Error ? err.message : String(err)}`
3778
+ );
3779
+ try {
3780
+ await generate(config);
3781
+ } catch {
3782
+ }
3783
+ }
3784
+ let pagesDebounceTimer;
3785
+ const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
3786
+ const pagesWatcher = chokidar.watch(join12(config.codegen.cwd, pagesGlob), {
3787
+ ignoreInitial: true,
3788
+ persistent: true,
3789
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3790
+ });
3791
+ function schedulePagesRegenerate() {
3792
+ if (pagesDebounceTimer !== void 0) {
3793
+ clearTimeout(pagesDebounceTimer);
3794
+ }
3795
+ pagesDebounceTimer = setTimeout(async () => {
3796
+ pagesDebounceTimer = void 0;
3797
+ try {
3798
+ await generate(config);
3799
+ } catch (err) {
3800
+ console.error(
3801
+ "[nestjs-codegen] Pages generation failed:",
3802
+ err instanceof Error ? err.message : err
3803
+ );
3804
+ }
3805
+ onChange?.();
3806
+ }, PAGES_DEBOUNCE_MS);
3807
+ }
3808
+ pagesWatcher.on("add", schedulePagesRegenerate);
3809
+ pagesWatcher.on("change", schedulePagesRegenerate);
3810
+ pagesWatcher.on("unlink", schedulePagesRegenerate);
3811
+ let contractsDebounceTimer;
3812
+ const contractsWatcher = chokidar.watch(join12(config.codegen.cwd, config.contracts.glob), {
3813
+ ignoreInitial: true,
3814
+ persistent: true,
3815
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3816
+ });
3817
+ function scheduleContractsRegenerate() {
3818
+ if (contractsDebounceTimer !== void 0) {
3819
+ clearTimeout(contractsDebounceTimer);
3820
+ }
3821
+ contractsDebounceTimer = setTimeout(async () => {
3822
+ contractsDebounceTimer = void 0;
3823
+ try {
3824
+ const routes = await discoverContractsFast({
3825
+ cwd: config.codegen.cwd,
3826
+ glob: config.contracts.glob,
3827
+ ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
3828
+ });
3829
+ await generate(config, routes);
3830
+ } catch (err) {
3831
+ console.error(
3832
+ "[nestjs-codegen] Contracts generation failed:",
3833
+ err instanceof Error ? err.message : err
3834
+ );
3835
+ }
3836
+ onChange?.();
3837
+ }, config.contracts.debounceMs);
3838
+ }
3839
+ contractsWatcher.on("add", scheduleContractsRegenerate);
3840
+ contractsWatcher.on("change", scheduleContractsRegenerate);
3841
+ contractsWatcher.on("unlink", scheduleContractsRegenerate);
3842
+ const formsWatcher = chokidar.watch(join12(config.codegen.cwd, config.forms.watch), {
3843
+ ignoreInitial: true,
3844
+ persistent: true,
3845
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
3846
+ });
3847
+ formsWatcher.on("add", scheduleContractsRegenerate);
3848
+ formsWatcher.on("change", scheduleContractsRegenerate);
3849
+ formsWatcher.on("unlink", scheduleContractsRegenerate);
3850
+ return {
3851
+ close: async () => {
3852
+ if (pagesDebounceTimer !== void 0) {
3853
+ clearTimeout(pagesDebounceTimer);
3854
+ pagesDebounceTimer = void 0;
3855
+ }
3856
+ if (contractsDebounceTimer !== void 0) {
3857
+ clearTimeout(contractsDebounceTimer);
3858
+ contractsDebounceTimer = void 0;
3859
+ }
3860
+ await pagesWatcher.close();
3861
+ await contractsWatcher.close();
3862
+ await formsWatcher.close();
3863
+ await lock.release();
3864
+ }
3865
+ };
3866
+ }
3867
+
3868
+ // src/nest/module.ts
3869
+ var CODEGEN_MODULE_OPTIONS = Symbol("NESTJS_CODEGEN_MODULE_OPTIONS");
3870
+ function shouldRun(options, env) {
3871
+ if (options.enabled !== void 0) return options.enabled;
3872
+ return env !== "production";
3873
+ }
3874
+ var NestjsCodegenService = class {
3875
+ constructor(options) {
3876
+ this.options = options;
3877
+ }
3878
+ logger = new Logger("NestjsCodegen");
3879
+ watcher = null;
3880
+ async onApplicationBootstrap() {
3881
+ if (!shouldRun(this.options, process.env.NODE_ENV)) return;
3882
+ const { enabled: _enabled, cwd, ...userConfig } = this.options;
3883
+ try {
3884
+ const config = resolveConfig(userConfig, cwd ?? process.cwd());
3885
+ this.watcher = await watch(config);
3886
+ this.logger.log(`Watching ${config.contracts.glob} \u2192 ${config.codegen.outDir}`);
3887
+ } catch (err) {
3888
+ this.logger.warn(
3889
+ `Codegen watcher failed to start: ${err instanceof Error ? err.message : String(err)}`
3890
+ );
3891
+ }
3892
+ }
3893
+ async onModuleDestroy() {
3894
+ await this.watcher?.close();
3895
+ this.watcher = null;
3896
+ }
3897
+ };
3898
+ NestjsCodegenService = __decorateClass([
3899
+ Injectable(),
3900
+ __decorateParam(0, Inject(CODEGEN_MODULE_OPTIONS))
3901
+ ], NestjsCodegenService);
3902
+ var NestjsCodegenModule = class {
3903
+ static forRoot(options = {}) {
3904
+ return {
3905
+ module: NestjsCodegenModule,
3906
+ providers: [{ provide: CODEGEN_MODULE_OPTIONS, useValue: options }, NestjsCodegenService]
3907
+ };
3908
+ }
3909
+ };
3910
+ NestjsCodegenModule = __decorateClass([
3911
+ Module({})
3912
+ ], NestjsCodegenModule);
3913
+ export {
3914
+ CODEGEN_MODULE_OPTIONS,
3915
+ NestjsCodegenModule,
3916
+ NestjsCodegenService,
3917
+ shouldRun
3918
+ };
3919
+ //# sourceMappingURL=index.js.map