@dudousxd/nestjs-codegen 0.25.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -83,12 +83,33 @@ function validateUserConfig(userConfig) {
83
83
  "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
84
84
  );
85
85
  }
86
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
86
+ if (userConfig.pages && !isGlobInput(userConfig.pages.glob)) {
87
87
  throw new ConfigError(
88
- "Config validation failed: `pages.glob` must be a string when `pages` is set"
88
+ "Config validation failed: `pages.glob` must be a string or a non-empty array of strings when `pages` is set"
89
+ );
90
+ }
91
+ for (const [scope, scopeConfig] of Object.entries(userConfig.pages?.scopes ?? {})) {
92
+ if (scope === "default") {
93
+ throw new ConfigError(
94
+ 'Config validation failed: `pages.scopes` cannot declare "default" \u2014 the default app is `pages.glob` (InertiaModule.forRoot).'
95
+ );
96
+ }
97
+ if (!isGlobInput(scopeConfig?.glob)) {
98
+ throw new ConfigError(
99
+ `Config validation failed: \`pages.scopes.${scope}.glob\` must be a string or a non-empty array of strings`
100
+ );
101
+ }
102
+ }
103
+ if (userConfig.scopes && Object.keys(userConfig.scopes).length > 0) {
104
+ console.warn(
105
+ "[nestjs-codegen] The top-level `scopes` option has no effect and is deprecated. For several Inertia apps use `pages.scopes` (one entry per InertiaModule.forFeature scope); for several controller sets use one `contracts.glob` (e.g. a brace glob)."
89
106
  );
90
107
  }
91
108
  }
109
+ function isGlobInput(value) {
110
+ if (typeof value === "string") return true;
111
+ return Array.isArray(value) && value.length > 0 && value.every((glob) => typeof glob === "string");
112
+ }
92
113
  function applyDefaults(userConfig, cwd) {
93
114
  validateUserConfig(userConfig);
94
115
  const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
@@ -115,7 +136,17 @@ function applyDefaults(userConfig, cwd) {
115
136
  pages: userConfig.pages ? {
116
137
  glob: userConfig.pages.glob,
117
138
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
118
- componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext"
139
+ componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext",
140
+ scopes: Object.fromEntries(
141
+ Object.entries(userConfig.pages.scopes ?? {}).map(([scope, scopeConfig]) => [
142
+ scope,
143
+ {
144
+ glob: scopeConfig.glob,
145
+ prefix: scopeConfig.prefix ?? `${scope}/`,
146
+ componentNameStrategy: scopeConfig.componentNameStrategy ?? userConfig.pages?.componentNameStrategy ?? "relative-no-ext"
147
+ }
148
+ ])
149
+ )
119
150
  } : null,
120
151
  contracts: {
121
152
  glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
@@ -189,26 +220,84 @@ import { dirname as dirname4, join as join14 } from "path";
189
220
 
190
221
  // src/discovery/pages.ts
191
222
  import { readFile } from "fs/promises";
192
- import { join as join2, relative as relative2 } from "path";
223
+ import { join as join2, relative as relative2, sep as sep2 } from "path";
193
224
  import fg from "fast-glob";
194
225
  var NON_PAGE_FILE_RE = /\.(?:test|spec|stories|story)\.(?:tsx?|jsx?|vue|svelte)$/i;
226
+ var MAGIC_SEGMENT_RE = /[*?{}[\]()!]/;
227
+ function globStaticBase(glob) {
228
+ const segments = glob.split("/");
229
+ const staticSegments = [];
230
+ for (const segment of segments.slice(0, -1)) {
231
+ if (MAGIC_SEGMENT_RE.test(segment)) break;
232
+ staticSegments.push(segment);
233
+ }
234
+ return staticSegments.join("/");
235
+ }
195
236
  async function discoverPages(opts) {
196
- const allFiles = await fg(opts.glob, { cwd: opts.cwd, absolute: true });
197
- const files = allFiles.filter((f) => !NON_PAGE_FILE_RE.test(f));
198
- files.sort();
199
- const globStatic = opts.glob.split("*")[0]?.replace(/\/$/, "") ?? "";
200
- const pagesBase = join2(opts.cwd, globStatic);
237
+ const globs = typeof opts.glob === "string" ? [opts.glob] : opts.glob;
238
+ const prefix = opts.prefix ?? "";
239
+ const scope = opts.scope ?? "default";
240
+ const seenFiles = /* @__PURE__ */ new Set();
201
241
  const out = [];
202
- for (const file of files) {
203
- const rel = relative2(opts.cwd, file);
204
- const nameRel = relative2(pagesBase, file);
205
- const name = computeName(nameRel, opts.componentNameStrategy);
206
- const source = await readFile(file, "utf8");
207
- const propsSource = extractPropsSource(source, opts.propsExport);
208
- out.push({ name, absolutePath: file, relativePath: rel, propsSource });
242
+ for (const glob of globs) {
243
+ const files = (await fg(glob, { cwd: opts.cwd, absolute: true })).filter((f) => !NON_PAGE_FILE_RE.test(f) && !seenFiles.has(f)).sort();
244
+ const pagesBase = join2(opts.cwd, globStaticBase(glob));
245
+ for (const file of files) {
246
+ seenFiles.add(file);
247
+ const rel = relative2(opts.cwd, file);
248
+ const nameRel = relative2(pagesBase, file).split(sep2).join("/");
249
+ const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
250
+ const source = await readFile(file, "utf8");
251
+ const propsSource = extractPropsSource(source, opts.propsExport);
252
+ out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
253
+ }
254
+ }
255
+ assertUniqueNames(out);
256
+ return out;
257
+ }
258
+ async function discoverPageScopes(config) {
259
+ const pages = config.pages;
260
+ if (!pages) return [];
261
+ const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
262
+ const out = await discoverPages({
263
+ ...common,
264
+ glob: pages.glob,
265
+ componentNameStrategy: pages.componentNameStrategy
266
+ });
267
+ for (const [scope, scopeConfig] of Object.entries(pages.scopes ?? {})) {
268
+ out.push(
269
+ ...await discoverPages({
270
+ ...common,
271
+ glob: scopeConfig.glob,
272
+ componentNameStrategy: scopeConfig.componentNameStrategy,
273
+ prefix: scopeConfig.prefix,
274
+ scope
275
+ })
276
+ );
209
277
  }
278
+ assertUniqueNames(out);
210
279
  return out;
211
280
  }
281
+ function allPageGlobs(config) {
282
+ if (!config.pages) return [];
283
+ const asList = (glob) => typeof glob === "string" ? [glob] : glob;
284
+ return [
285
+ ...asList(config.pages.glob),
286
+ ...Object.values(config.pages.scopes ?? {}).flatMap((scope) => asList(scope.glob))
287
+ ];
288
+ }
289
+ function assertUniqueNames(pages) {
290
+ const byName = /* @__PURE__ */ new Map();
291
+ for (const page of pages) {
292
+ const previous = byName.get(page.name);
293
+ if (previous) {
294
+ throw new Error(
295
+ `Two Inertia pages resolve to the component name "${page.name}": ${previous.relativePath} (scope "${previous.scope}") and ${page.relativePath} (scope "${page.scope}"). Give one of them another name, directory or scope prefix.`
296
+ );
297
+ }
298
+ byName.set(page.name, page);
299
+ }
300
+ }
212
301
  function computeName(rel, strat) {
213
302
  if (typeof strat === "function") return strat(rel);
214
303
  const noExt = rel.replace(/\.(tsx?|vue|svelte)$/, "");
@@ -518,22 +607,22 @@ import { isAbsolute as isAbsolute2, join as join5, relative as relative4 } from
518
607
 
519
608
  // src/extension/registry.ts
520
609
  import { relative as relative3, resolve as resolve4 } from "path";
521
- import { Project as Project4 } from "ts-morph";
610
+ import { Project as Project5 } from "ts-morph";
522
611
 
523
612
  // src/discovery/contracts-fast.ts
524
613
  import { existsSync } from "fs";
525
614
  import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
526
615
  import fg2 from "fast-glob";
527
616
  import {
528
- Node as Node9,
529
- Project as Project3,
530
- ts
617
+ Node as Node11,
618
+ Project as Project4,
619
+ ts as ts3
531
620
  } from "ts-morph";
532
621
 
533
622
  // src/discovery/dto-type-resolver.ts
534
623
  import {
535
- Node as Node7,
536
- SyntaxKind as SyntaxKind3
624
+ Node as Node10,
625
+ SyntaxKind as SyntaxKind4
537
626
  } from "ts-morph";
538
627
 
539
628
  // src/discovery/dto-to-ir.ts
@@ -2071,6 +2160,674 @@ function extractFilterableEntityFields(filterClass, project, entityOverride, pre
2071
2160
  return fields;
2072
2161
  }
2073
2162
 
2163
+ // src/discovery/pipe-schema.ts
2164
+ import { Node as Node9 } from "ts-morph";
2165
+
2166
+ // src/discovery/schema-type-inference.ts
2167
+ import {
2168
+ ts as ts2
2169
+ } from "ts-morph";
2170
+
2171
+ // src/discovery/schema-type-program.ts
2172
+ import { Node as Node7, Project as Project3, ts } from "ts-morph";
2173
+ var PARAM_DECORATORS = /* @__PURE__ */ new Set(["Body", "Query", "Param"]);
2174
+ var SchemaTypeProgram = class {
2175
+ constructor(discovery) {
2176
+ this.discovery = discovery;
2177
+ const base = discovery.getCompilerOptions();
2178
+ this.options = {
2179
+ ...base,
2180
+ // A tsconfig-less consumer gets ES5 by default, whose lib has no `Map`/`Set`/
2181
+ // `Promise` for the schema libraries' declarations to lean on.
2182
+ target: base.target ?? ts.ScriptTarget.ES2022,
2183
+ // With neither set, resolve the way a bundler does: package `exports`
2184
+ // (`zod/v4`) and extensionless or `.js` relative imports all resolve.
2185
+ ...base.module === void 0 && base.moduleResolution === void 0 ? { module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler } : {},
2186
+ // Without it `.nullable()` and `.optional()` would erase to the bare type.
2187
+ strictNullChecks: true,
2188
+ // No ambient `@types/*` (a tsconfig's `types: ['node']`): a schema does not
2189
+ // need them, and they are a hundred-odd files of program to build.
2190
+ types: [],
2191
+ noEmit: true
2192
+ };
2193
+ this.project = new Project3({
2194
+ skipAddingFilesFromTsConfig: true,
2195
+ compilerOptions: this.options
2196
+ });
2197
+ for (const sf of discovery.getSourceFiles()) this.request(sf.getFilePath(), { schema: true });
2198
+ this.flush();
2199
+ }
2200
+ project;
2201
+ options;
2202
+ /** Parses a file that is not in discovery's Project, without adding it there. */
2203
+ scratch = new Project3({
2204
+ skipAddingFilesFromTsConfig: true,
2205
+ skipLoadingLibFiles: true,
2206
+ skipFileDependencyResolution: true
2207
+ });
2208
+ needs = /* @__PURE__ */ new Map();
2209
+ /** Pruned text per file, as last written into {@link project}. */
2210
+ written = /* @__PURE__ */ new Map();
2211
+ /**
2212
+ * The typed copy of a discovery file, with its schema positions kept. Adding one
2213
+ * that was not there yet (a contract file discovery reached lazily) rebuilds the
2214
+ * program once.
2215
+ */
2216
+ sourceFileFor(sf) {
2217
+ this.request(sf.getFilePath(), { schema: true });
2218
+ this.flush();
2219
+ return this.project.getSourceFile(sf.getFilePath());
2220
+ }
2221
+ // -------------------------------------------------------------------------
2222
+ queue = [];
2223
+ request(path, want) {
2224
+ let needs = this.needs.get(path);
2225
+ const isNew = !needs;
2226
+ needs ??= { schema: false, all: false, names: /* @__PURE__ */ new Set() };
2227
+ let grew = isNew;
2228
+ if (want.schema && !needs.schema) {
2229
+ needs.schema = true;
2230
+ grew = true;
2231
+ }
2232
+ if (want.all && !needs.all) {
2233
+ needs.all = true;
2234
+ grew = true;
2235
+ }
2236
+ for (const name of want.names ?? []) {
2237
+ if (!needs.names.has(name)) {
2238
+ needs.names.add(name);
2239
+ grew = true;
2240
+ }
2241
+ }
2242
+ this.needs.set(path, needs);
2243
+ if (grew) this.queue.push(path);
2244
+ }
2245
+ /** Run the worklist to a fixpoint, then write every changed file into the program. */
2246
+ flush() {
2247
+ const changed = /* @__PURE__ */ new Map();
2248
+ while (this.queue.length > 0) {
2249
+ const path = this.queue.pop();
2250
+ const original = this.original(path);
2251
+ if (!original) continue;
2252
+ const needs = this.needs.get(path);
2253
+ const text = this.prune(original, needs);
2254
+ if (this.written.get(path) !== text) changed.set(path, text);
2255
+ }
2256
+ for (const [path, text] of changed) {
2257
+ this.project.createSourceFile(path, text, { overwrite: true });
2258
+ this.written.set(path, text);
2259
+ }
2260
+ }
2261
+ /** The file as discovery sees it (its in-memory text), else as it is on disk. */
2262
+ original(path) {
2263
+ return this.discovery.getSourceFile(path) ?? this.scratch.getSourceFile(path) ?? this.scratch.addSourceFileAtPathIfExists(path);
2264
+ }
2265
+ /**
2266
+ * `sf`'s text with every import/re-export nothing in `needs` references blanked,
2267
+ * requesting from each kept one's target the names it brings in.
2268
+ */
2269
+ prune(sf, needs) {
2270
+ const locals = topLevelDeclarations(sf);
2271
+ const roots = [];
2272
+ if (needs.all) roots.push(...sf.getStatements());
2273
+ if (needs.schema) roots.push(...schemaPositions(sf));
2274
+ const reexported = /* @__PURE__ */ new Map();
2275
+ for (const exp of sf.getExportDeclarations()) {
2276
+ const spec = exp.getModuleSpecifierValue();
2277
+ if (spec === void 0) {
2278
+ for (const named of exp.getNamedExports()) {
2279
+ const exposed = (named.getAliasNode() ?? named.getNameNode()).getText();
2280
+ if (needs.all || needs.names.has(exposed)) roots.push(named.getNameNode());
2281
+ }
2282
+ continue;
2283
+ }
2284
+ if (exp.isNamespaceExport() || exp.getNamedExports().length === 0) continue;
2285
+ for (const named of exp.getNamedExports()) {
2286
+ const exposed = (named.getAliasNode() ?? named.getNameNode()).getText();
2287
+ const list = reexported.get(exposed) ?? [];
2288
+ list.push({ from: spec, name: named.getNameNode().getText() });
2289
+ reexported.set(exposed, list);
2290
+ }
2291
+ }
2292
+ const unresolvedNames = [];
2293
+ for (const name of needs.names) {
2294
+ const decls = name === "default" ? defaultExport(sf) : locals.get(name) ?? [];
2295
+ if (decls.length > 0) roots.push(...decls);
2296
+ else if (!reexported.has(name)) unresolvedNames.push(name);
2297
+ }
2298
+ const referenced = /* @__PURE__ */ new Set();
2299
+ const queue = [...roots];
2300
+ const seen = new Set(queue);
2301
+ while (queue.length > 0) {
2302
+ const node = queue.pop();
2303
+ const ids = Node7.isIdentifier(node) ? [node] : node.getDescendantsOfKind(ts.SyntaxKind.Identifier);
2304
+ for (const id of ids) {
2305
+ const name = id.getText();
2306
+ if (referenced.has(name)) continue;
2307
+ referenced.add(name);
2308
+ for (const local of locals.get(name) ?? []) {
2309
+ if (seen.has(local)) continue;
2310
+ seen.add(local);
2311
+ queue.push(local);
2312
+ }
2313
+ }
2314
+ }
2315
+ const chars = sf.getFullText().split("");
2316
+ const blank = (node) => {
2317
+ for (let i = node.getStart(); i < node.getEnd(); i++) {
2318
+ if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
2319
+ }
2320
+ };
2321
+ for (const decl of sf.getImportDeclarations()) {
2322
+ const target = this.resolve(decl.getModuleSpecifierValue(), sf);
2323
+ const namespace = decl.getNamespaceImport()?.getText();
2324
+ const def = decl.getDefaultImport()?.getText();
2325
+ const named = decl.getNamedImports().map((n) => ({
2326
+ local: (n.getAliasNode() ?? n.getNameNode()).getText(),
2327
+ imported: n.getName()
2328
+ })).filter((n) => referenced.has(n.local));
2329
+ const wantsNamespace = namespace !== void 0 && referenced.has(namespace);
2330
+ const wantsDefault = def !== void 0 && referenced.has(def);
2331
+ if (!wantsNamespace && !wantsDefault && named.length === 0) {
2332
+ blank(decl);
2333
+ continue;
2334
+ }
2335
+ if (target) {
2336
+ this.request(target, {
2337
+ all: wantsNamespace,
2338
+ names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2339
+ });
2340
+ }
2341
+ }
2342
+ for (const exp of sf.getExportDeclarations()) {
2343
+ const spec = exp.getModuleSpecifierValue();
2344
+ if (spec === void 0) continue;
2345
+ const target = this.resolve(spec, sf);
2346
+ if (exp.isNamespaceExport() || exp.getNamedExports().length === 0) {
2347
+ if (!needs.all && unresolvedNames.length === 0) {
2348
+ blank(exp);
2349
+ } else if (target) {
2350
+ this.request(target, { all: needs.all, names: unresolvedNames });
2351
+ }
2352
+ continue;
2353
+ }
2354
+ const wanted = exp.getNamedExports().filter(
2355
+ (n) => needs.all || needs.names.has((n.getAliasNode() ?? n.getNameNode()).getText())
2356
+ ).map((n) => n.getNameNode().getText());
2357
+ if (wanted.length === 0) blank(exp);
2358
+ else if (target) this.request(target, { names: wanted });
2359
+ }
2360
+ return chars.join("");
2361
+ }
2362
+ /**
2363
+ * The project source file a module specifier resolves to, or undefined for a
2364
+ * package (read unpruned by the program itself) or an unresolvable one.
2365
+ */
2366
+ resolve(specifier, from) {
2367
+ const resolved = ts.resolveModuleName(
2368
+ specifier,
2369
+ from.getFilePath(),
2370
+ this.options,
2371
+ ts.sys
2372
+ ).resolvedModule;
2373
+ if (!resolved || resolved.isExternalLibraryImport) return void 0;
2374
+ const file = resolved.resolvedFileName;
2375
+ if (file.endsWith(".d.ts") || !/\.(m|c)?tsx?$/.test(file)) return void 0;
2376
+ return file;
2377
+ }
2378
+ };
2379
+ function schemaPositions(sf) {
2380
+ const roots = [];
2381
+ sf.forEachDescendant((node) => {
2382
+ if (Node7.isDecorator(node)) {
2383
+ const name = node.getName();
2384
+ if (name === "ApplyContract") {
2385
+ roots.push(...node.getArguments());
2386
+ } else if (PARAM_DECORATORS.has(name)) {
2387
+ for (const arg of node.getArguments()) {
2388
+ if (Node7.isNewExpression(arg) || Node7.isCallExpression(arg)) {
2389
+ roots.push(...arg.getArguments());
2390
+ }
2391
+ }
2392
+ }
2393
+ } else if (Node7.isCallExpression(node)) {
2394
+ const callee = node.getExpression();
2395
+ const name = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : void 0;
2396
+ if (name === "defineContract") roots.push(...node.getArguments());
2397
+ }
2398
+ });
2399
+ return roots;
2400
+ }
2401
+ function topLevelDeclarations(sf) {
2402
+ const byName = /* @__PURE__ */ new Map();
2403
+ const add = (name, node) => {
2404
+ if (!name) return;
2405
+ const list = byName.get(name);
2406
+ if (list) list.push(node);
2407
+ else byName.set(name, [node]);
2408
+ };
2409
+ for (const decl of sf.getVariableDeclarations()) add(decl.getName(), decl);
2410
+ for (const decl of sf.getFunctions()) add(decl.getName(), decl);
2411
+ for (const decl of sf.getClasses()) add(decl.getName(), decl);
2412
+ for (const decl of sf.getEnums()) add(decl.getName(), decl);
2413
+ for (const decl of sf.getInterfaces()) add(decl.getName(), decl);
2414
+ for (const decl of sf.getTypeAliases()) add(decl.getName(), decl);
2415
+ for (const decl of sf.getModules()) add(decl.getName(), decl);
2416
+ return byName;
2417
+ }
2418
+ function defaultExport(sf) {
2419
+ const nodes = [];
2420
+ for (const assignment of sf.getExportAssignments()) nodes.push(assignment);
2421
+ for (const fn of sf.getFunctions()) if (fn.isDefaultExport()) nodes.push(fn);
2422
+ for (const cls of sf.getClasses()) if (cls.isDefaultExport()) nodes.push(cls);
2423
+ return nodes;
2424
+ }
2425
+
2426
+ // src/discovery/schema-type-inference.ts
2427
+ var programs = /* @__PURE__ */ new WeakMap();
2428
+ function programFor(discovery) {
2429
+ let program = programs.get(discovery);
2430
+ if (!program) {
2431
+ program = new SchemaTypeProgram(discovery);
2432
+ programs.set(discovery, program);
2433
+ }
2434
+ return program;
2435
+ }
2436
+ function clearSchemaTypeProject(discovery) {
2437
+ programs.delete(discovery);
2438
+ }
2439
+ function counterpart(typedSf, node) {
2440
+ const start = node.getStart();
2441
+ const end = node.getEnd();
2442
+ const kind = node.getKind();
2443
+ let candidate = typedSf.getDescendantAtPos(start);
2444
+ while (candidate) {
2445
+ if (candidate.getStart() === start && candidate.getEnd() === end && candidate.getKind() === kind) {
2446
+ return candidate;
2447
+ }
2448
+ if (candidate.getStart() < start || candidate.getEnd() > end) return void 0;
2449
+ candidate = candidate.getParent();
2450
+ }
2451
+ return void 0;
2452
+ }
2453
+ function propType(type, name, at) {
2454
+ return type.getProperty(name)?.getTypeAtLocation(at);
2455
+ }
2456
+ function isAnyOrUnknown(type) {
2457
+ return type.isAny() || type.isUnknown();
2458
+ }
2459
+ function inferSchema(expr, discovery) {
2460
+ try {
2461
+ const typedSf = programFor(discovery).sourceFileFor(expr.getSourceFile());
2462
+ const at = typedSf && counterpart(typedSf, expr);
2463
+ if (!at) return { kind: "unresolved" };
2464
+ const schemaType = at.getType();
2465
+ if (isAnyOrUnknown(schemaType)) return { kind: "unresolved" };
2466
+ const standard = propType(schemaType, "~standard", at);
2467
+ const stdTypes = standard ? propType(standard, "types", at)?.getNonNullableType() : void 0;
2468
+ const stdInput = stdTypes ? propType(stdTypes, "input", at) : void 0;
2469
+ const stdOutput = stdTypes ? propType(stdTypes, "output", at) : void 0;
2470
+ if (stdInput && stdOutput) {
2471
+ return { kind: "schema", types: { input: stdInput, output: stdOutput, at } };
2472
+ }
2473
+ const zodInput = propType(schemaType, "_input", at);
2474
+ const zodOutput = propType(schemaType, "_output", at);
2475
+ if (zodInput && zodOutput) {
2476
+ return { kind: "schema", types: { input: zodInput, output: zodOutput, at } };
2477
+ }
2478
+ return { kind: "not-schema" };
2479
+ } catch (err) {
2480
+ dbg("schema inference failed:", err);
2481
+ return { kind: "unresolved" };
2482
+ }
2483
+ }
2484
+ var GLOBAL_NAMED_TYPES = /* @__PURE__ */ new Set([
2485
+ "Date",
2486
+ "File",
2487
+ "Blob",
2488
+ "FormData",
2489
+ "URL",
2490
+ "RegExp",
2491
+ "Map",
2492
+ "Set",
2493
+ "ArrayBuffer",
2494
+ "Uint8Array"
2495
+ ]);
2496
+ var MAX_DEPTH = 12;
2497
+ function objectKey(name) {
2498
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2499
+ }
2500
+ function isOptionalSymbol(sym) {
2501
+ return (sym.getFlags() & ts2.SymbolFlags.Optional) !== 0;
2502
+ }
2503
+ function isSymbolKeyed(sym) {
2504
+ return sym.getName().startsWith("__@");
2505
+ }
2506
+ function orderedProperties(type) {
2507
+ const position = (sym) => {
2508
+ const decl = sym.getDeclarations()[0];
2509
+ return decl ? [decl.getSourceFile().getFilePath(), decl.getStart()] : void 0;
2510
+ };
2511
+ return type.getProperties().map((sym, index) => ({ sym, index, at: position(sym) })).sort((a, b) => {
2512
+ if (a.at && b.at) {
2513
+ if (a.at[0] !== b.at[0]) return a.at[0] < b.at[0] ? -1 : 1;
2514
+ if (a.at[1] !== b.at[1]) return a.at[1] - b.at[1];
2515
+ } else if (a.at || b.at) {
2516
+ return a.at ? -1 : 1;
2517
+ }
2518
+ return a.index - b.index;
2519
+ }).map((entry) => entry.sym);
2520
+ }
2521
+ function isFromDeclarationFile(sym) {
2522
+ const decl = sym?.getDeclarations()[0];
2523
+ return !!decl && decl.getSourceFile().isDeclarationFile();
2524
+ }
2525
+ function render(type, state, paired) {
2526
+ if (state.depth > MAX_DEPTH) return "unknown";
2527
+ if (isAnyOrUnknown(type)) {
2528
+ return paired && !isAnyOrUnknown(paired) ? render(paired, state) : "unknown";
2529
+ }
2530
+ if (type.isNever()) return "never";
2531
+ if (type.isString() || type.isTemplateLiteral()) return "string";
2532
+ if (type.isNumber()) return "number";
2533
+ if (type.isBoolean()) return "boolean";
2534
+ if (type.isBigInt()) return "bigint";
2535
+ if (type.isNull()) return "null";
2536
+ if (type.isUndefined() || type.isVoid()) return "undefined";
2537
+ if (type.isBooleanLiteral()) return type.getText();
2538
+ if (type.isStringLiteral() || type.isNumberLiteral() || type.isEnumLiteral()) {
2539
+ const value = type.getLiteralValue();
2540
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
2541
+ }
2542
+ if (type.isBigIntLiteral()) return type.getText();
2543
+ if (type.isTypeParameter()) return "unknown";
2544
+ if (type.isUnion()) return renderUnion(type, state, paired);
2545
+ if (type.isIntersection()) return renderIntersection(type, state);
2546
+ const next = { ...state, depth: state.depth + 1 };
2547
+ if (type.isArray() || type.isReadonlyArray()) {
2548
+ const element = type.getArrayElementType() ?? type.getTypeArguments()[0];
2549
+ const pairedElement = paired?.getArrayElementType() ?? paired?.getTypeArguments()[0];
2550
+ return element ? `Array<${render(element, next, pairedElement)}>` : "Array<unknown>";
2551
+ }
2552
+ if (type.isTuple()) {
2553
+ const elements = type.getTupleElements();
2554
+ const pairedElements = paired?.isTuple() ? paired.getTupleElements() : [];
2555
+ return `[${elements.map((e, i) => render(e, next, pairedElements[i])).join(", ")}]`;
2556
+ }
2557
+ if (type.isObject()) return renderObject(type, next, paired);
2558
+ return "unknown";
2559
+ }
2560
+ function renderUnion(type, state, paired) {
2561
+ const members = type.getUnionTypes();
2562
+ const hasTrue = members.some((m) => m.isBooleanLiteral() && m.getText() === "true");
2563
+ const hasFalse = members.some((m) => m.isBooleanLiteral() && m.getText() === "false");
2564
+ const pairedNonNull = paired?.getNonNullableType();
2565
+ const arms = [];
2566
+ let sawBoolean = false;
2567
+ let hasNull = false;
2568
+ let hasUndefined = false;
2569
+ for (const member of members) {
2570
+ if (member.isNull()) {
2571
+ hasNull = true;
2572
+ continue;
2573
+ }
2574
+ if (member.isUndefined()) {
2575
+ hasUndefined = true;
2576
+ continue;
2577
+ }
2578
+ if (member.isBooleanLiteral() && hasTrue && hasFalse) {
2579
+ if (!sawBoolean) arms.push("boolean");
2580
+ sawBoolean = true;
2581
+ continue;
2582
+ }
2583
+ arms.push(render(member, state, members.length === 1 ? paired : pairedNonNull));
2584
+ }
2585
+ if (hasNull) arms.push("null");
2586
+ if (hasUndefined) arms.push("undefined");
2587
+ const unique = [...new Set(arms)];
2588
+ if (unique.includes("unknown")) return "unknown";
2589
+ return unique.join(" | ");
2590
+ }
2591
+ function renderIntersection(type, state) {
2592
+ const members = type.getIntersectionTypes();
2593
+ const primitive = members.find((m) => !m.isObject());
2594
+ if (primitive) return render(primitive, state);
2595
+ return renderObject(type, { ...state, depth: state.depth + 1 }, void 0);
2596
+ }
2597
+ function renderObject(type, state, paired) {
2598
+ const symbol = type.getSymbol() ?? type.getAliasSymbol();
2599
+ const name = symbol?.getName();
2600
+ if (name && GLOBAL_NAMED_TYPES.has(name) && isFromDeclarationFile(symbol)) {
2601
+ const args = type.getTypeArguments();
2602
+ return args.length ? `${name}<${args.map((a) => render(a, state)).join(", ")}>` : name;
2603
+ }
2604
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
2605
+ return "unknown";
2606
+ }
2607
+ if (type.isClassOrInterface() && isFromDeclarationFile(symbol)) return "unknown";
2608
+ if (state.stack.has(type.compilerType)) return "unknown";
2609
+ const stack = new Set(state.stack).add(type.compilerType);
2610
+ const inner = { ...state, stack };
2611
+ const fields = [];
2612
+ for (const prop of orderedProperties(type)) {
2613
+ if (isSymbolKeyed(prop)) continue;
2614
+ const propName = prop.getName();
2615
+ const propT = prop.getTypeAtLocation(state.at);
2616
+ if (propT.getCallSignatures().length > 0 && !propT.isUnion()) continue;
2617
+ const pairedProp = paired?.getProperty(propName)?.getTypeAtLocation(state.at);
2618
+ const optional = isOptionalSymbol(prop) ? "?" : "";
2619
+ fields.push(`${objectKey(propName)}${optional}: ${render(propT, inner, pairedProp)}`);
2620
+ }
2621
+ const stringIndex = type.getStringIndexType();
2622
+ if (stringIndex) {
2623
+ const pairedIndex = paired?.getStringIndexType();
2624
+ const value = render(stringIndex, inner, pairedIndex);
2625
+ if (fields.length === 0) return `Record<string, ${value}>`;
2626
+ fields.push(`[key: string]: ${value}`);
2627
+ }
2628
+ return fields.length ? `{ ${fields.join("; ")} }` : "{}";
2629
+ }
2630
+ function stateAt(at) {
2631
+ return { at, stack: /* @__PURE__ */ new Set(), depth: 0 };
2632
+ }
2633
+ function requestTypeOf(types) {
2634
+ return render(types.input, stateAt(types.at), types.output);
2635
+ }
2636
+ function responseTypeOf(types) {
2637
+ return render(types.output, stateAt(types.at));
2638
+ }
2639
+ function requestFieldsOf(types) {
2640
+ const { input, output, at } = types;
2641
+ const objectInput = input.getNonNullableType();
2642
+ if (!objectInput.isObject() || objectInput.isArray() || objectInput.isTuple()) return null;
2643
+ const state = stateAt(at);
2644
+ return orderedProperties(objectInput).filter((prop) => !isSymbolKeyed(prop)).map((prop) => ({
2645
+ name: prop.getName(),
2646
+ type: render(
2647
+ prop.getTypeAtLocation(at),
2648
+ state,
2649
+ output.getProperty(prop.getName())?.getTypeAtLocation(at)
2650
+ ),
2651
+ optional: isOptionalSymbol(prop)
2652
+ }));
2653
+ }
2654
+ function acceptsUndefined(types) {
2655
+ const { input, output } = types;
2656
+ const accepted = isAnyOrUnknown(input) ? output : input;
2657
+ if (isAnyOrUnknown(accepted) || accepted.isUndefined()) return true;
2658
+ return accepted.isUnion() && accepted.getUnionTypes().some((t) => t.isUndefined());
2659
+ }
2660
+
2661
+ // src/discovery/zod-ast-to-ts.ts
2662
+ import { Node as Node8, SyntaxKind as SyntaxKind3 } from "ts-morph";
2663
+ function zodAstToTs(node) {
2664
+ if (!Node8.isCallExpression(node)) return "unknown";
2665
+ const expr = node.getExpression();
2666
+ if (Node8.isPropertyAccessExpression(expr)) {
2667
+ const methodName = expr.getName();
2668
+ const receiver = expr.getExpression();
2669
+ if (methodName === "optional") {
2670
+ return `${zodAstToTs(receiver)} | undefined`;
2671
+ }
2672
+ if (methodName === "nullable") {
2673
+ return `${zodAstToTs(receiver)} | null`;
2674
+ }
2675
+ const args = node.getArguments();
2676
+ switch (methodName) {
2677
+ case "string":
2678
+ return "string";
2679
+ case "number":
2680
+ return "number";
2681
+ case "boolean":
2682
+ return "boolean";
2683
+ case "unknown":
2684
+ return "unknown";
2685
+ case "any":
2686
+ return "unknown";
2687
+ case "literal": {
2688
+ const lit = args[0];
2689
+ if (!lit) return "unknown";
2690
+ if (Node8.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2691
+ if (Node8.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2692
+ if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
2693
+ if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
2694
+ return "unknown";
2695
+ }
2696
+ case "enum": {
2697
+ const arrArg = args[0];
2698
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2699
+ const members = arrArg.getElements().map(
2700
+ (el) => Node8.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2701
+ );
2702
+ return members.join(" | ");
2703
+ }
2704
+ case "array": {
2705
+ const inner = args[0];
2706
+ if (!inner) return "unknown";
2707
+ return `Array<${zodAstToTs(inner)}>`;
2708
+ }
2709
+ case "object": {
2710
+ const objArg = args[0];
2711
+ if (!objArg || !Node8.isObjectLiteralExpression(objArg)) return "unknown";
2712
+ const lines = [];
2713
+ for (const prop of objArg.getProperties()) {
2714
+ if (!Node8.isPropertyAssignment(prop)) continue;
2715
+ const key = prop.getName();
2716
+ const valNode = prop.getInitializer();
2717
+ if (!valNode) continue;
2718
+ const tsType = zodAstToTs(valNode);
2719
+ const isOpt = isOptionalChain(valNode);
2720
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
2721
+ }
2722
+ return `{ ${lines.join("; ")} }`;
2723
+ }
2724
+ case "union": {
2725
+ const arrArg = args[0];
2726
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2727
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
2728
+ }
2729
+ case "record": {
2730
+ const valArg = args.length === 1 ? args[0] : args[1];
2731
+ if (!valArg) return "unknown";
2732
+ return `Record<string, ${zodAstToTs(valArg)}>`;
2733
+ }
2734
+ case "tuple": {
2735
+ const arrArg = args[0];
2736
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2737
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
2738
+ }
2739
+ default:
2740
+ return "unknown";
2741
+ }
2742
+ }
2743
+ return "unknown";
2744
+ }
2745
+ function isOptionalChain(node) {
2746
+ if (!Node8.isCallExpression(node)) return false;
2747
+ const expr = node.getExpression();
2748
+ return Node8.isPropertyAccessExpression(expr) && expr.getName() === "optional";
2749
+ }
2750
+ function memberType(val, position, project) {
2751
+ if (project) {
2752
+ const inferred = inferSchema(val, project);
2753
+ if (inferred.kind === "schema") {
2754
+ return position === "request" ? requestTypeOf(inferred.types) : responseTypeOf(inferred.types);
2755
+ }
2756
+ }
2757
+ return zodAstToTs(val);
2758
+ }
2759
+ function parseDefineContractCall(callExpr, project) {
2760
+ if (!Node8.isCallExpression(callExpr)) return null;
2761
+ const callee = callExpr.getExpression();
2762
+ const calleeName = Node8.isIdentifier(callee) ? callee.getText() : Node8.isPropertyAccessExpression(callee) ? callee.getName() : "";
2763
+ if (calleeName !== "defineContract") return null;
2764
+ const args = callExpr.getArguments();
2765
+ const optsArg = args[0];
2766
+ if (!optsArg || !Node8.isObjectLiteralExpression(optsArg)) return null;
2767
+ let query = null;
2768
+ let body = null;
2769
+ let response = "unknown";
2770
+ let error = null;
2771
+ let bodyZodText = null;
2772
+ let queryZodText = null;
2773
+ for (const prop of optsArg.getProperties()) {
2774
+ if (!Node8.isPropertyAssignment(prop)) continue;
2775
+ const propName = prop.getName();
2776
+ const val = prop.getInitializer();
2777
+ if (!val) continue;
2778
+ if (propName === "query") {
2779
+ query = memberType(val, "request", project);
2780
+ queryZodText = val.getText();
2781
+ } else if (propName === "body") {
2782
+ body = memberType(val, "request", project);
2783
+ bodyZodText = val.getText();
2784
+ } else if (propName === "response") {
2785
+ response = memberType(val, "response", project);
2786
+ } else if (propName === "error") {
2787
+ error = memberType(val, "response", project);
2788
+ }
2789
+ }
2790
+ return { query, body, response, error, bodyZodText, queryZodText };
2791
+ }
2792
+
2793
+ // src/discovery/pipe-schema.ts
2794
+ function pipeSchemaOf(decorator, project) {
2795
+ for (const pipe of decorator.getArguments()) {
2796
+ if (!Node9.isNewExpression(pipe) && !Node9.isCallExpression(pipe)) continue;
2797
+ for (const arg of pipe.getArguments()) {
2798
+ if (!couldBeSchema(arg)) continue;
2799
+ const inferred = inferSchema(arg, project);
2800
+ if (inferred.kind === "schema") {
2801
+ return {
2802
+ type: requestTypeOf(inferred.types),
2803
+ optional: acceptsUndefined(inferred.types),
2804
+ fields: requestFieldsOf(inferred.types)
2805
+ };
2806
+ }
2807
+ if (inferred.kind === "unresolved") {
2808
+ const fallback = syntacticZodType(arg, project);
2809
+ if (fallback) {
2810
+ return { type: fallback, optional: fallback.endsWith("| undefined"), fields: null };
2811
+ }
2812
+ }
2813
+ }
2814
+ }
2815
+ return null;
2816
+ }
2817
+ function couldBeSchema(arg) {
2818
+ return Node9.isIdentifier(arg) || Node9.isPropertyAccessExpression(arg) || Node9.isCallExpression(arg) || Node9.isAsExpression(arg) || Node9.isSatisfiesExpression(arg) || Node9.isParenthesizedExpression(arg);
2819
+ }
2820
+ function syntacticZodType(arg, project) {
2821
+ let expr = arg;
2822
+ if (Node9.isIdentifier(arg)) {
2823
+ const resolved = resolveImportedVariable(arg.getText(), arg.getSourceFile(), project);
2824
+ expr = resolved?.decl.getInitializer();
2825
+ }
2826
+ if (!expr || !Node9.isCallExpression(expr)) return null;
2827
+ const type = zodAstToTs(expr);
2828
+ return type === "unknown" ? null : type;
2829
+ }
2830
+
2074
2831
  // src/discovery/dto-type-resolver.ts
2075
2832
  var WRAPPER_TYPES = {
2076
2833
  // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
@@ -2102,22 +2859,22 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2102
2859
  ]);
2103
2860
  function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2104
2861
  if (depth <= 0) return "unknown";
2105
- if (Node7.isArrayTypeNode(typeNode)) {
2862
+ if (Node10.isArrayTypeNode(typeNode)) {
2106
2863
  const elementType = typeNode.getElementTypeNode();
2107
2864
  return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
2108
2865
  }
2109
- if (Node7.isUnionTypeNode(typeNode)) {
2866
+ if (Node10.isUnionTypeNode(typeNode)) {
2110
2867
  return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
2111
2868
  }
2112
- if (Node7.isIntersectionTypeNode(typeNode)) {
2869
+ if (Node10.isIntersectionTypeNode(typeNode)) {
2113
2870
  return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
2114
2871
  }
2115
- if (Node7.isParenthesizedTypeNode(typeNode)) {
2872
+ if (Node10.isParenthesizedTypeNode(typeNode)) {
2116
2873
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
2117
2874
  }
2118
- if (Node7.isTypeReference(typeNode)) {
2875
+ if (Node10.isTypeReference(typeNode)) {
2119
2876
  const typeName = typeNode.getTypeName();
2120
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2877
+ const name = Node10.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2121
2878
  const bound = subst.get(name);
2122
2879
  if (bound !== void 0) return bound;
2123
2880
  if (name === "string" || name === "number" || name === "boolean") return name;
@@ -2140,13 +2897,13 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /
2140
2897
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
2141
2898
  return "unknown";
2142
2899
  }
2143
- if (Node7.isTypeLiteral(typeNode)) {
2900
+ if (Node10.isTypeLiteral(typeNode)) {
2144
2901
  const members = [];
2145
2902
  for (const member of typeNode.getMembers()) {
2146
- if (Node7.isPropertySignature(member)) {
2903
+ if (Node10.isPropertySignature(member)) {
2147
2904
  const memberTypeNode = member.getTypeNode();
2148
- const memberType = memberTypeNode ? resolveTypeNodeToString(memberTypeNode, sourceFile, project, depth, subst) : "unknown";
2149
- members.push(`${member.getName()}${member.hasQuestionToken() ? "?" : ""}: ${memberType}`);
2905
+ const memberType2 = memberTypeNode ? resolveTypeNodeToString(memberTypeNode, sourceFile, project, depth, subst) : "unknown";
2906
+ members.push(`${member.getName()}${member.hasQuestionToken() ? "?" : ""}: ${memberType2}`);
2150
2907
  } else {
2151
2908
  members.push(member.getText());
2152
2909
  }
@@ -2154,11 +2911,11 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /
2154
2911
  return members.length > 0 ? `{ ${members.join("; ")} }` : "{}";
2155
2912
  }
2156
2913
  const kind = typeNode.getKind();
2157
- if (kind === SyntaxKind3.StringKeyword) return "string";
2158
- if (kind === SyntaxKind3.NumberKeyword) return "number";
2159
- if (kind === SyntaxKind3.BooleanKeyword) return "boolean";
2160
- if (kind === SyntaxKind3.UnknownKeyword) return "unknown";
2161
- if (kind === SyntaxKind3.AnyKeyword) return "unknown";
2914
+ if (kind === SyntaxKind4.StringKeyword) return "string";
2915
+ if (kind === SyntaxKind4.NumberKeyword) return "number";
2916
+ if (kind === SyntaxKind4.BooleanKeyword) return "boolean";
2917
+ if (kind === SyntaxKind4.UnknownKeyword) return "unknown";
2918
+ if (kind === SyntaxKind4.AnyKeyword) return "unknown";
2162
2919
  return typeNode.getText();
2163
2920
  }
2164
2921
  function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
@@ -2206,20 +2963,24 @@ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE_
2206
2963
  const propName = prop.getName();
2207
2964
  const isOptional = prop.hasQuestionToken();
2208
2965
  const propTypeNode = prop.getTypeNode();
2209
- let propType = "unknown";
2966
+ let propType2 = "unknown";
2210
2967
  if (propTypeNode) {
2211
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2968
+ propType2 = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2212
2969
  }
2213
- lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
2970
+ lines.push(`${propName}${isOptional ? "?" : ""}: ${propType2}`);
2214
2971
  }
2215
2972
  return `{ ${lines.join("; ")} }`;
2216
2973
  }
2974
+ function namedField(decorator) {
2975
+ const first = decorator.getArguments()[0];
2976
+ return first && Node10.isStringLiteral(first) ? first.getLiteralValue() : null;
2977
+ }
2217
2978
  function extractBodyType(method, sourceFile, project) {
2218
2979
  for (const param of method.getParameters()) {
2219
2980
  const bodyDecorator = param.getDecorators().find((d) => d.getName() === "Body");
2220
- if (!bodyDecorator) continue;
2221
- const bodyArgs = bodyDecorator.getArguments();
2222
- if (bodyArgs.length > 0) continue;
2981
+ if (!bodyDecorator || namedField(bodyDecorator) !== null) continue;
2982
+ const piped = pipeSchemaOf(bodyDecorator, project);
2983
+ if (piped) return piped.type;
2223
2984
  const typeNode = param.getTypeNode();
2224
2985
  if (typeNode) {
2225
2986
  return resolveTypeNodeToString(typeNode, sourceFile, project, 3);
@@ -2227,11 +2988,16 @@ function extractBodyType(method, sourceFile, project) {
2227
2988
  }
2228
2989
  return null;
2229
2990
  }
2991
+ function hasPipeSchema(param, decoratorName, project) {
2992
+ const decorator = param.getDecorators().find((d) => d.getName() === decoratorName);
2993
+ return !!decorator && pipeSchemaOf(decorator, project) !== null;
2994
+ }
2230
2995
  function extractQueryType(method, sourceFile, project) {
2231
2996
  for (const param of method.getParameters()) {
2232
2997
  const queryDecorator = param.getDecorators().find((d) => d.getName() === "Query");
2233
- if (!queryDecorator) continue;
2234
- if (queryDecorator.getArguments().length > 0) continue;
2998
+ if (!queryDecorator || namedField(queryDecorator) !== null) continue;
2999
+ const piped = pipeSchemaOf(queryDecorator, project);
3000
+ if (piped) return piped.type;
2235
3001
  const typeNode = param.getTypeNode();
2236
3002
  if (typeNode) {
2237
3003
  return resolveTypeNodeToString(typeNode, sourceFile, project, 3);
@@ -2241,10 +3007,13 @@ function extractQueryType(method, sourceFile, project) {
2241
3007
  for (const param of method.getParameters()) {
2242
3008
  const queryDecorator = param.getDecorators().find((d) => d.getName() === "Query");
2243
3009
  if (!queryDecorator) continue;
2244
- const queryArgs = queryDecorator.getArguments();
2245
- const nameArg = queryArgs[0];
2246
- if (!nameArg || !Node7.isStringLiteral(nameArg)) continue;
2247
- const queryName = nameArg.getLiteralValue();
3010
+ const queryName = namedField(queryDecorator);
3011
+ if (queryName === null) continue;
3012
+ const piped = pipeSchemaOf(queryDecorator, project);
3013
+ if (piped) {
3014
+ entries.push(`${queryName}${piped.optional ? "?" : ""}: ${piped.type}`);
3015
+ continue;
3016
+ }
2248
3017
  const typeNode = param.getTypeNode();
2249
3018
  const queryType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
2250
3019
  entries.push(`${queryName}${isParamOptional(param) ? "?" : ""}: ${queryType}`);
@@ -2254,8 +3023,8 @@ function extractQueryType(method, sourceFile, project) {
2254
3023
  function isParamOptional(param) {
2255
3024
  if (param.hasQuestionToken() || param.hasInitializer()) return true;
2256
3025
  const typeNode = param.getTypeNode();
2257
- if (typeNode && Node7.isUnionTypeNode(typeNode)) {
2258
- return typeNode.getTypeNodes().some((t) => t.getKind() === SyntaxKind3.UndefinedKeyword);
3026
+ if (typeNode && Node10.isUnionTypeNode(typeNode)) {
3027
+ return typeNode.getTypeNodes().some((t) => t.getKind() === SyntaxKind4.UndefinedKeyword);
2259
3028
  }
2260
3029
  return false;
2261
3030
  }
@@ -2267,7 +3036,7 @@ function extractParamsType(method, sourceFile, project) {
2267
3036
  const paramArgs = paramDecorator.getArguments();
2268
3037
  if (paramArgs.length === 0) continue;
2269
3038
  const nameArg = paramArgs[0];
2270
- if (!Node7.isStringLiteral(nameArg)) continue;
3039
+ if (!Node10.isStringLiteral(nameArg)) continue;
2271
3040
  const paramName = nameArg.getLiteralValue();
2272
3041
  const typeNode = param.getTypeNode();
2273
3042
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -2275,6 +3044,22 @@ function extractParamsType(method, sourceFile, project) {
2275
3044
  }
2276
3045
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
2277
3046
  }
3047
+ function extractPipedParamTypes(method, project) {
3048
+ const types = {};
3049
+ for (const param of method.getParameters()) {
3050
+ const paramDecorator = param.getDecorators().find((d) => d.getName() === "Param");
3051
+ if (!paramDecorator) continue;
3052
+ const piped = pipeSchemaOf(paramDecorator, project);
3053
+ if (!piped) continue;
3054
+ const name = namedField(paramDecorator);
3055
+ if (name !== null) {
3056
+ types[name] = piped.type;
3057
+ } else {
3058
+ for (const field of piped.fields ?? []) types[field.name] = field.type;
3059
+ }
3060
+ }
3061
+ return Object.keys(types).length > 0 ? types : null;
3062
+ }
2278
3063
  function extractUploadedFiles(method) {
2279
3064
  const FILE = "File | Blob";
2280
3065
  const entries = [];
@@ -2288,28 +3073,28 @@ function extractUploadedFiles(method) {
2288
3073
  for (const decorator of method.getDecorators()) {
2289
3074
  if (decorator.getName() !== "UseInterceptors") continue;
2290
3075
  for (const arg of decorator.getArguments()) {
2291
- if (!Node7.isCallExpression(arg)) continue;
3076
+ if (!Node10.isCallExpression(arg)) continue;
2292
3077
  const interceptor = arg.getExpression().getText();
2293
3078
  const callArgs = arg.getArguments();
2294
3079
  const firstArg2 = callArgs[0];
2295
3080
  if (interceptor === "FileInterceptor") {
2296
- if (firstArg2 && Node7.isStringLiteral(firstArg2)) {
3081
+ if (firstArg2 && Node10.isStringLiteral(firstArg2)) {
2297
3082
  entries.push(`${firstArg2.getLiteralValue()}: ${FILE}`);
2298
3083
  multipart = true;
2299
3084
  }
2300
3085
  } else if (interceptor === "FilesInterceptor") {
2301
- if (firstArg2 && Node7.isStringLiteral(firstArg2)) {
3086
+ if (firstArg2 && Node10.isStringLiteral(firstArg2)) {
2302
3087
  entries.push(`${firstArg2.getLiteralValue()}: Array<${FILE}>`);
2303
3088
  multipart = true;
2304
3089
  }
2305
3090
  } else if (interceptor === "FileFieldsInterceptor") {
2306
- if (firstArg2 && Node7.isArrayLiteralExpression(firstArg2)) {
3091
+ if (firstArg2 && Node10.isArrayLiteralExpression(firstArg2)) {
2307
3092
  for (const el of firstArg2.getElements()) {
2308
- if (!Node7.isObjectLiteralExpression(el)) continue;
3093
+ if (!Node10.isObjectLiteralExpression(el)) continue;
2309
3094
  const nameProp = el.getProperty("name");
2310
- if (nameProp && Node7.isPropertyAssignment(nameProp)) {
3095
+ if (nameProp && Node10.isPropertyAssignment(nameProp)) {
2311
3096
  const init = nameProp.getInitializer();
2312
- if (init && Node7.isStringLiteral(init)) {
3097
+ if (init && Node10.isStringLiteral(init)) {
2313
3098
  entries.push(`${init.getLiteralValue()}: Array<${FILE}>`);
2314
3099
  }
2315
3100
  }
@@ -2329,13 +3114,13 @@ function extractResponseType(method, sourceFile, project) {
2329
3114
  if (apiResponseDecorator) {
2330
3115
  const args = apiResponseDecorator.getArguments();
2331
3116
  const optsArg = args[0];
2332
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
3117
+ if (optsArg && Node10.isObjectLiteralExpression(optsArg)) {
2333
3118
  for (const prop of optsArg.getProperties()) {
2334
- if (!Node7.isPropertyAssignment(prop)) continue;
3119
+ if (!Node10.isPropertyAssignment(prop)) continue;
2335
3120
  if (prop.getName() !== "type") continue;
2336
3121
  const val = prop.getInitializer();
2337
3122
  if (!val) continue;
2338
- if (Node7.isArrayLiteralExpression(val)) {
3123
+ if (Node10.isArrayLiteralExpression(val)) {
2339
3124
  const elements = val.getElements();
2340
3125
  const firstEl = elements[0];
2341
3126
  if (elements.length > 0 && firstEl !== void 0) {
@@ -2356,24 +3141,24 @@ function extractResponseType(method, sourceFile, project) {
2356
3141
  }
2357
3142
  function apiResponseStatus(decorator) {
2358
3143
  const optsArg = decorator.getArguments()[0];
2359
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3144
+ if (!optsArg || !Node10.isObjectLiteralExpression(optsArg)) return null;
2360
3145
  for (const prop of optsArg.getProperties()) {
2361
- if (!Node7.isPropertyAssignment(prop)) continue;
3146
+ if (!Node10.isPropertyAssignment(prop)) continue;
2362
3147
  if (prop.getName() !== "status") continue;
2363
3148
  const val = prop.getInitializer();
2364
- if (val && Node7.isNumericLiteral(val)) return Number(val.getLiteralValue());
3149
+ if (val && Node10.isNumericLiteral(val)) return Number(val.getLiteralValue());
2365
3150
  }
2366
3151
  return null;
2367
3152
  }
2368
3153
  function apiResponseTypeNode(decorator) {
2369
3154
  const optsArg = decorator.getArguments()[0];
2370
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3155
+ if (!optsArg || !Node10.isObjectLiteralExpression(optsArg)) return null;
2371
3156
  for (const prop of optsArg.getProperties()) {
2372
- if (!Node7.isPropertyAssignment(prop)) continue;
3157
+ if (!Node10.isPropertyAssignment(prop)) continue;
2373
3158
  if (prop.getName() !== "type") continue;
2374
3159
  const val = prop.getInitializer();
2375
3160
  if (!val) return null;
2376
- if (Node7.isArrayLiteralExpression(val)) {
3161
+ if (Node10.isArrayLiteralExpression(val)) {
2377
3162
  const first = val.getElements()[0];
2378
3163
  return first ? { node: first, isArray: true } : null;
2379
3164
  }
@@ -2391,7 +3176,7 @@ function extractErrorType(method, sourceFile, project) {
2391
3176
  const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
2392
3177
  const type = typeInfo.isArray ? `Array<${inner}>` : inner;
2393
3178
  let ref = null;
2394
- if (Node7.isIdentifier(typeInfo.node)) {
3179
+ if (Node10.isIdentifier(typeInfo.node)) {
2395
3180
  const name = typeInfo.node.getText();
2396
3181
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
2397
3182
  if (localDecl?.isExported()) {
@@ -2408,7 +3193,7 @@ function extractErrorType(method, sourceFile, project) {
2408
3193
  return null;
2409
3194
  }
2410
3195
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
2411
- if (!Node7.isIdentifier(node)) return "unknown";
3196
+ if (!Node10.isIdentifier(node)) return "unknown";
2412
3197
  const name = node.getText();
2413
3198
  const resolved = findType(name, sourceFile, project);
2414
3199
  if (resolved) {
@@ -2428,9 +3213,9 @@ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLik
2428
3213
  var BINARY_RESPONSE_TYPES = /* @__PURE__ */ new Set(["StreamableFile", "Buffer"]);
2429
3214
  function detectBinaryResponse(method) {
2430
3215
  const node = unwrapNamedContainer(method.getReturnTypeNode(), /* @__PURE__ */ new Set(["Promise"]));
2431
- if (!node || !Node7.isTypeReference(node)) return false;
3216
+ if (!node || !Node10.isTypeReference(node)) return false;
2432
3217
  const typeName = node.getTypeName();
2433
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : "";
3218
+ const name = Node10.isIdentifier(typeName) ? typeName.getText() : "";
2434
3219
  return BINARY_RESPONSE_TYPES.has(name);
2435
3220
  }
2436
3221
  function detectStreamElement(method) {
@@ -2445,9 +3230,9 @@ function detectStreamElement(method) {
2445
3230
  return null;
2446
3231
  }
2447
3232
  function streamContainerElement(node) {
2448
- if (!node || !Node7.isTypeReference(node)) return null;
3233
+ if (!node || !Node10.isTypeReference(node)) return null;
2449
3234
  const typeName = node.getTypeName();
2450
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : "";
3235
+ const name = Node10.isIdentifier(typeName) ? typeName.getText() : "";
2451
3236
  if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
2452
3237
  return node.getTypeArguments()[0] ?? null;
2453
3238
  }
@@ -2457,9 +3242,9 @@ function hasAsQueryDecorator(method) {
2457
3242
  return method.getDecorators().some((d) => d.getName() === "AsQuery");
2458
3243
  }
2459
3244
  function unwrapNamedContainer(node, names) {
2460
- if (!node || !Node7.isTypeReference(node)) return node;
3245
+ if (!node || !Node10.isTypeReference(node)) return node;
2461
3246
  const typeName = node.getTypeName();
2462
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : "";
3247
+ const name = Node10.isIdentifier(typeName) ? typeName.getText() : "";
2463
3248
  if (names.has(name)) {
2464
3249
  return node.getTypeArguments()[0] ?? node;
2465
3250
  }
@@ -2480,19 +3265,20 @@ function extractDtoContract(method, sourceFile, project, mixin, controllerClass)
2480
3265
  body = body ?? bodyType;
2481
3266
  }
2482
3267
  const paramsType = extractParamsType(method, sourceFile, project);
3268
+ const paramTypes = extractPipedParamTypes(method, project);
2483
3269
  const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
2484
3270
  const errorInfo = extractErrorType(method, sourceFile, project);
2485
- if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart && !binaryResponse && !asQuery) {
3271
+ if (body === null && query === null && paramsType === null && paramTypes === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart && !binaryResponse && !asQuery) {
2486
3272
  return null;
2487
3273
  }
2488
3274
  let bodyRef = null;
2489
3275
  let queryRef = null;
2490
3276
  let responseRef = null;
2491
3277
  for (const param of method.getParameters()) {
2492
- if (param.getDecorators().some((d) => d.getName() === "Body") && param.getTypeNode()) {
3278
+ if (param.getDecorators().some((d) => d.getName() === "Body") && param.getTypeNode() && !hasPipeSchema(param, "Body", project)) {
2493
3279
  bodyRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2494
3280
  }
2495
- if (param.getDecorators().some((d) => d.getName() === "Query") && param.getTypeNode()) {
3281
+ if (param.getDecorators().some((d) => d.getName() === "Query") && param.getTypeNode() && !hasPipeSchema(param, "Query", project)) {
2496
3282
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2497
3283
  }
2498
3284
  }
@@ -2505,11 +3291,11 @@ function extractDtoContract(method, sourceFile, project, mixin, controllerClass)
2505
3291
  if (apiResp) {
2506
3292
  const args = apiResp.getArguments();
2507
3293
  const optsArg = args[0];
2508
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
3294
+ if (optsArg && Node10.isObjectLiteralExpression(optsArg)) {
2509
3295
  for (const prop of optsArg.getProperties()) {
2510
- if (Node7.isPropertyAssignment(prop) && prop.getName() === "type") {
3296
+ if (Node10.isPropertyAssignment(prop) && prop.getName() === "type") {
2511
3297
  const val = prop.getInitializer();
2512
- if (val && Node7.isIdentifier(val)) {
3298
+ if (val && Node10.isIdentifier(val)) {
2513
3299
  const name = val.getText();
2514
3300
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
2515
3301
  if (localDecl?.isExported()) {
@@ -2545,6 +3331,7 @@ function extractDtoContract(method, sourceFile, project, mixin, controllerClass)
2545
3331
  response,
2546
3332
  error: errorInfo?.type ?? null,
2547
3333
  params: paramsType,
3334
+ paramTypes,
2548
3335
  queryRef,
2549
3336
  bodyRef,
2550
3337
  responseRef,
@@ -2565,6 +3352,7 @@ function extractDtoContract(method, sourceFile, project, mixin, controllerClass)
2565
3352
  function resolveParamClass(method, decoratorName, sourceFile, project) {
2566
3353
  for (const param of method.getParameters()) {
2567
3354
  if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
3355
+ if (hasPipeSchema(param, decoratorName, project)) continue;
2568
3356
  const typeNode = param.getTypeNode();
2569
3357
  if (!typeNode) continue;
2570
3358
  const text = typeNode.getText().replace(/\[\]$/, "");
@@ -2577,129 +3365,6 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
2577
3365
  return null;
2578
3366
  }
2579
3367
 
2580
- // src/discovery/zod-ast-to-ts.ts
2581
- import { Node as Node8, SyntaxKind as SyntaxKind4 } from "ts-morph";
2582
- function zodAstToTs(node) {
2583
- if (!Node8.isCallExpression(node)) return "unknown";
2584
- const expr = node.getExpression();
2585
- if (Node8.isPropertyAccessExpression(expr)) {
2586
- const methodName = expr.getName();
2587
- const receiver = expr.getExpression();
2588
- if (methodName === "optional") {
2589
- return `${zodAstToTs(receiver)} | undefined`;
2590
- }
2591
- if (methodName === "nullable") {
2592
- return `${zodAstToTs(receiver)} | null`;
2593
- }
2594
- const args = node.getArguments();
2595
- switch (methodName) {
2596
- case "string":
2597
- return "string";
2598
- case "number":
2599
- return "number";
2600
- case "boolean":
2601
- return "boolean";
2602
- case "unknown":
2603
- return "unknown";
2604
- case "any":
2605
- return "unknown";
2606
- case "literal": {
2607
- const lit = args[0];
2608
- if (!lit) return "unknown";
2609
- if (Node8.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2610
- if (Node8.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2611
- if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
2612
- if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
2613
- return "unknown";
2614
- }
2615
- case "enum": {
2616
- const arrArg = args[0];
2617
- if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2618
- const members = arrArg.getElements().map(
2619
- (el) => Node8.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2620
- );
2621
- return members.join(" | ");
2622
- }
2623
- case "array": {
2624
- const inner = args[0];
2625
- if (!inner) return "unknown";
2626
- return `Array<${zodAstToTs(inner)}>`;
2627
- }
2628
- case "object": {
2629
- const objArg = args[0];
2630
- if (!objArg || !Node8.isObjectLiteralExpression(objArg)) return "unknown";
2631
- const lines = [];
2632
- for (const prop of objArg.getProperties()) {
2633
- if (!Node8.isPropertyAssignment(prop)) continue;
2634
- const key = prop.getName();
2635
- const valNode = prop.getInitializer();
2636
- if (!valNode) continue;
2637
- const tsType = zodAstToTs(valNode);
2638
- const isOpt = isOptionalChain(valNode);
2639
- lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
2640
- }
2641
- return `{ ${lines.join("; ")} }`;
2642
- }
2643
- case "union": {
2644
- const arrArg = args[0];
2645
- if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2646
- return arrArg.getElements().map(zodAstToTs).join(" | ");
2647
- }
2648
- case "record": {
2649
- const valArg = args.length === 1 ? args[0] : args[1];
2650
- if (!valArg) return "unknown";
2651
- return `Record<string, ${zodAstToTs(valArg)}>`;
2652
- }
2653
- case "tuple": {
2654
- const arrArg = args[0];
2655
- if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2656
- return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
2657
- }
2658
- default:
2659
- return "unknown";
2660
- }
2661
- }
2662
- return "unknown";
2663
- }
2664
- function isOptionalChain(node) {
2665
- if (!Node8.isCallExpression(node)) return false;
2666
- const expr = node.getExpression();
2667
- return Node8.isPropertyAccessExpression(expr) && expr.getName() === "optional";
2668
- }
2669
- function parseDefineContractCall(callExpr) {
2670
- if (!Node8.isCallExpression(callExpr)) return null;
2671
- const callee = callExpr.getExpression();
2672
- const calleeName = Node8.isIdentifier(callee) ? callee.getText() : Node8.isPropertyAccessExpression(callee) ? callee.getName() : "";
2673
- if (calleeName !== "defineContract") return null;
2674
- const args = callExpr.getArguments();
2675
- const optsArg = args[0];
2676
- if (!optsArg || !Node8.isObjectLiteralExpression(optsArg)) return null;
2677
- let query = null;
2678
- let body = null;
2679
- let response = "unknown";
2680
- let error = null;
2681
- let bodyZodText = null;
2682
- let queryZodText = null;
2683
- for (const prop of optsArg.getProperties()) {
2684
- if (!Node8.isPropertyAssignment(prop)) continue;
2685
- const propName = prop.getName();
2686
- const val = prop.getInitializer();
2687
- if (!val) continue;
2688
- if (propName === "query") {
2689
- query = zodAstToTs(val);
2690
- queryZodText = val.getText();
2691
- } else if (propName === "body") {
2692
- body = zodAstToTs(val);
2693
- bodyZodText = val.getText();
2694
- } else if (propName === "response") {
2695
- response = zodAstToTs(val);
2696
- } else if (propName === "error") {
2697
- error = zodAstToTs(val);
2698
- }
2699
- }
2700
- return { query, body, response, error, bodyZodText, queryZodText };
2701
- }
2702
-
2703
3368
  // src/discovery/contracts-fast.ts
2704
3369
  async function discoverContractsFast(opts) {
2705
3370
  const { cwd, glob, tsconfig } = opts;
@@ -2712,7 +3377,9 @@ async function discoverContractsFast(opts) {
2712
3377
  }
2713
3378
  bindDiscoveryContext(project, cwd, tsconfigPath, loaded);
2714
3379
  clearMixinTypeProject();
2715
- return extractAllRoutes(project);
3380
+ const routes = extractAllRoutes(project);
3381
+ clearSchemaTypeProject(project);
3382
+ return routes;
2716
3383
  }
2717
3384
  function resolveTsconfigPath(cwd, tsconfig) {
2718
3385
  return tsconfig ? resolve3(tsconfig) : join4(cwd, "tsconfig.json");
@@ -2724,25 +3391,25 @@ var NO_TSCONFIG_OPTIONS = {
2724
3391
  };
2725
3392
  function loadDiscoveryTsconfig(tsconfigPath) {
2726
3393
  if (!existsSync(tsconfigPath)) return { options: null, files: [] };
2727
- const read = ts.readConfigFile(tsconfigPath, (path) => ts.sys.readFile(path));
3394
+ const read = ts3.readConfigFile(tsconfigPath, (path) => ts3.sys.readFile(path));
2728
3395
  if (read.error) {
2729
3396
  return {
2730
3397
  options: null,
2731
3398
  files: [tsconfigPath],
2732
- error: ts.flattenDiagnosticMessageText(read.error.messageText, " ")
3399
+ error: ts3.flattenDiagnosticMessageText(read.error.messageText, " ")
2733
3400
  };
2734
3401
  }
2735
3402
  const host = {
2736
- useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
3403
+ useCaseSensitiveFileNames: ts3.sys.useCaseSensitiveFileNames,
2737
3404
  // The one line that keeps a tsconfig from being a filesystem walk.
2738
3405
  readDirectory: () => [],
2739
- fileExists: (path) => ts.sys.fileExists(path),
3406
+ fileExists: (path) => ts3.sys.fileExists(path),
2740
3407
  // `extends` is resolved through the host, so a consumer whose `paths` live
2741
3408
  // in a base tsconfig keeps working.
2742
- readFile: (path) => ts.sys.readFile(path)
3409
+ readFile: (path) => ts3.sys.readFile(path)
2743
3410
  };
2744
- const sourceFile = ts.readJsonConfigFile(tsconfigPath, (path) => ts.sys.readFile(path));
2745
- const parsed = ts.parseJsonSourceFileConfigFileContent(
3411
+ const sourceFile = ts3.readJsonConfigFile(tsconfigPath, (path) => ts3.sys.readFile(path));
3412
+ const parsed = ts3.parseJsonSourceFileConfigFileContent(
2746
3413
  sourceFile,
2747
3414
  host,
2748
3415
  dirname2(tsconfigPath),
@@ -2765,7 +3432,7 @@ function createDiscoveryProject(tsconfigPath, tsconfig = loadDiscoveryTsconfig(t
2765
3432
  `[nestjs-codegen/fast] Could not load ${tsconfigPath}: ${tsconfig.error} \u2014 continuing WITHOUT its compiler options, so path alias imports (e.g. '@/...') will not resolve and controllers extending an alias-imported factory will contribute NO routes to the generated client.`
2766
3433
  );
2767
3434
  }
2768
- return new Project3({
3435
+ return new Project4({
2769
3436
  compilerOptions: tsconfig.options ?? { ...NO_TSCONFIG_OPTIONS },
2770
3437
  skipAddingFilesFromTsConfig: true,
2771
3438
  skipLoadingLibFiles: true,
@@ -2877,15 +3544,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
2877
3544
  clearTypeResolutionCaches(this.project);
2878
3545
  clearEnumCache(this.project);
2879
3546
  clearMixinTypeProject();
3547
+ clearSchemaTypeProject(this.project);
2880
3548
  return extractRoutesFrom(this.project, this.controllerPaths);
2881
3549
  }
2882
3550
  };
2883
3551
  function decoratorStringArg(decoratorExpr) {
2884
3552
  if (!decoratorExpr) return void 0;
2885
- if (Node9.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
2886
- if (Node9.isArrayLiteralExpression(decoratorExpr)) {
3553
+ if (Node11.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3554
+ if (Node11.isArrayLiteralExpression(decoratorExpr)) {
2887
3555
  const first = decoratorExpr.getElements()[0];
2888
- if (first && Node9.isStringLiteral(first)) return first.getLiteralValue();
3556
+ if (first && Node11.isStringLiteral(first)) return first.getLiteralValue();
2889
3557
  }
2890
3558
  return void 0;
2891
3559
  }
@@ -3006,9 +3674,9 @@ function extractContractRoute(args) {
3006
3674
  let contractDef = null;
3007
3675
  let bodyZodRef = null;
3008
3676
  let queryZodRef = null;
3009
- if (Node9.isCallExpression(firstDecoratorArg)) {
3010
- contractDef = parseDefineContractCall(firstDecoratorArg);
3011
- } else if (Node9.isIdentifier(firstDecoratorArg)) {
3677
+ if (Node11.isCallExpression(firstDecoratorArg)) {
3678
+ contractDef = parseDefineContractCall(firstDecoratorArg, project);
3679
+ } else if (Node11.isIdentifier(firstDecoratorArg)) {
3012
3680
  const identName = firstDecoratorArg.getText();
3013
3681
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
3014
3682
  if (!resolvedVar) {
@@ -3020,7 +3688,7 @@ function extractContractRoute(args) {
3020
3688
  const { decl: varDecl, file: declFile } = resolvedVar;
3021
3689
  const initializer = varDecl.getInitializer();
3022
3690
  if (!initializer) return null;
3023
- contractDef = parseDefineContractCall(initializer);
3691
+ contractDef = parseDefineContractCall(initializer, project);
3024
3692
  if (contractDef && varDecl.isExported()) {
3025
3693
  const filePath = declFile.getFilePath();
3026
3694
  if (contractDef.body !== null) {
@@ -3091,6 +3759,7 @@ function extractDtoRoute(args) {
3091
3759
  body: dtoContract?.body ?? null,
3092
3760
  response: mixinResponse ?? dtoContract?.response ?? "unknown",
3093
3761
  error: dtoContract?.error ?? null,
3762
+ paramTypes: dtoContract?.paramTypes ?? null,
3094
3763
  queryRef: dtoContract?.queryRef ?? null,
3095
3764
  bodyRef: dtoContract?.bodyRef ?? null,
3096
3765
  responseRef: dtoContract?.responseRef ?? null,
@@ -3213,7 +3882,7 @@ function createExtensionContext(config, getRoutes, trackedInputs) {
3213
3882
  },
3214
3883
  project() {
3215
3884
  if (!project) {
3216
- project = new Project4({
3885
+ project = new Project5({
3217
3886
  skipAddingFilesFromTsConfig: true,
3218
3887
  skipLoadingLibFiles: true,
3219
3888
  skipFileDependencyResolution: true,
@@ -3331,10 +4000,11 @@ function insertIntoTree(tree, segments, leaf, fullName) {
3331
4000
  insertIntoTree(branch.children, rest, leaf, fullName);
3332
4001
  }
3333
4002
  }
3334
- function buildParamsType(params) {
4003
+ function buildParamsType(params, paramTypes) {
3335
4004
  const pathParams = params.filter((p) => p.source === "path");
3336
4005
  if (pathParams.length === 0) return "never";
3337
- return `{ ${pathParams.map((p) => `${p.name}: string`).join("; ")} }`;
4006
+ const typeOf = (name) => paramTypes && Object.hasOwn(paramTypes, name) && paramTypes[name] || "string";
4007
+ return `{ ${pathParams.map((p) => `${p.name}: ${typeOf(p.name)}`).join("; ")} }`;
3338
4008
  }
3339
4009
  function hasPathParams(params) {
3340
4010
  return params.some((p) => p.source === "path");
@@ -3428,7 +4098,7 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
3428
4098
  }
3429
4099
  const response = buildResponseType(c, outDir, serialization);
3430
4100
  const error = buildErrorType(c);
3431
- const params = buildParamsType(c.params);
4101
+ const params = buildParamsType(c.params, c.contractSource.paramTypes);
3432
4102
  const safeMethod = JSON.stringify(method);
3433
4103
  const safeUrl = JSON.stringify(c.path);
3434
4104
  const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
@@ -3516,28 +4186,28 @@ function renderFetcherRequest(req, binaryResponse) {
3516
4186
  }
3517
4187
  function emitReqHelper() {
3518
4188
  return [
3519
- "/** Awaitable request handle. `await api.x.y({...})` runs the fetch; extensions add query/mutation helpers. */",
3520
- "type __Req<R> = {",
3521
- " then<T1 = R, T2 = never>(",
4189
+ "/** An awaitable request handle: a lazy, memoized `Promise` that also carries `fetch()` and the members extensions add. */",
4190
+ "export type RequestHandle<R, M = {}> = Promise<R> & { fetch(): Promise<R> } & M;",
4191
+ "class __Request<R> extends Promise<R> {",
4192
+ " static override get [Symbol.species](): PromiseConstructor {",
4193
+ " return Promise;",
4194
+ " }",
4195
+ " private __run: () => Promise<R>;",
4196
+ " private __p: Promise<R> | undefined;",
4197
+ " constructor(run: () => Promise<R>) {",
4198
+ " super(() => {});",
4199
+ " this.__run = run;",
4200
+ " }",
4201
+ " override then<T1 = R, T2 = never>(",
3522
4202
  " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
3523
4203
  " onrejected?: ((reason: unknown) => T2 | PromiseLike<T2>) | null,",
3524
- " ): Promise<T1 | T2>;",
3525
- " catch<T = never>(onrejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<R | T>;",
3526
- " finally(onfinally?: (() => void) | null): Promise<R>;",
3527
- " fetch(): Promise<R>;",
3528
- "};",
3529
- "function __req<R>(run: () => Promise<R>): __Req<R> {",
3530
- " let __p: Promise<R> | undefined;",
3531
- " const __promise = () => {",
3532
- " __p ??= run();",
3533
- " return __p;",
3534
- " };",
3535
- " return {",
3536
- " then: (onfulfilled, onrejected) => __promise().then(onfulfilled, onrejected),",
3537
- " catch: (onrejected) => __promise().catch(onrejected),",
3538
- " finally: (onfinally) => __promise().finally(onfinally),",
3539
- " fetch: run,",
3540
- " };",
4204
+ " ): Promise<T1 | T2> {",
4205
+ " this.__p ??= this.__run();",
4206
+ " return this.__p.then(onfulfilled, onrejected);",
4207
+ " }",
4208
+ "}",
4209
+ "function __req<R, M extends object = {}>(run: () => Promise<R>, members?: M): RequestHandle<R, M> {",
4210
+ " return Object.assign(new __Request<R>(run), { fetch: run }, members) as RequestHandle<R, M>;",
3541
4211
  "}",
3542
4212
  ""
3543
4213
  ];
@@ -3555,18 +4225,26 @@ function emitFilterFieldGuard() {
3555
4225
  ];
3556
4226
  }
3557
4227
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
3558
- const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
3559
- lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
4228
+ const memberLines = [];
3560
4229
  if (req.filterFieldsExpr) {
3561
- lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4230
+ memberLines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
3562
4231
  }
3563
4232
  if (streamExpr) {
3564
- lines.push(`${pad} stream: () => ${streamExpr},`);
4233
+ memberLines.push(`${pad} stream: () => ${streamExpr},`);
3565
4234
  }
3566
4235
  for (const [name, value] of Object.entries(members)) {
3567
- lines.push(`${pad} ${name}: ${value},`);
3568
- }
3569
- lines.push(`${pad}}),`);
4236
+ memberLines.push(`${pad} ${name}: ${value},`);
4237
+ }
4238
+ const body = [requestExpr, ...memberLines].join("\n");
4239
+ const param = /\binput\b/.test(body) ? `input?: ${req.inputType}` : "";
4240
+ const lines = [`${pad}${objKey}: (${param}) =>`];
4241
+ if (memberLines.length === 0) {
4242
+ lines.push(`${pad} __req<${req.responseType}>(() => ${requestExpr}),`);
4243
+ return lines;
4244
+ }
4245
+ lines.push(`${pad} __req(() => ${requestExpr}, {`);
4246
+ lines.push(...memberLines);
4247
+ lines.push(`${pad} }),`);
3570
4248
  return lines;
3571
4249
  }
3572
4250
  function renderStreamExpr(req) {
@@ -3697,6 +4375,26 @@ var EMPTY_PATH_NAMESPACE = [
3697
4375
  "}",
3698
4376
  ""
3699
4377
  ];
4378
+ function isClientCallable(route) {
4379
+ return route.method.toUpperCase() !== "ALL";
4380
+ }
4381
+ var ROUTES_IMPORT_PLACEHOLDER = "/* @@routes-import@@ */";
4382
+ var ROUTES_EXPORTS = [
4383
+ // Called, never mentioned in prose: match the call so a comment saying "route" doesn't count.
4384
+ { name: "route", typeOnly: false },
4385
+ { name: "ROUTES", typeOnly: false },
4386
+ { name: "RouteName", typeOnly: true },
4387
+ { name: "ExtractParams", typeOnly: true },
4388
+ { name: "RouteParams", typeOnly: true }
4389
+ ];
4390
+ function finishRoutesImport(lines) {
4391
+ const body = lines.filter((l) => l !== ROUTES_IMPORT_PLACEHOLDER).join("\n");
4392
+ const used = ROUTES_EXPORTS.filter(
4393
+ ({ name }) => new RegExp(name === "route" ? "\\broute\\(" : `\\b${name}\\b`).test(body)
4394
+ ).map(({ name, typeOnly }) => typeOnly ? `type ${name}` : name);
4395
+ const importLine = used.length ? `import { ${used.join(", ")} } from './routes.js';` : null;
4396
+ return lines.flatMap((l) => l === ROUTES_IMPORT_PLACEHOLDER ? importLine ? [importLine] : [] : [l]).join("\n");
4397
+ }
3700
4398
  function buildApiFile(routes, outDir, opts = {}) {
3701
4399
  const fetcherImportPath = opts.fetcherImportPath;
3702
4400
  const serialization = opts.serialization ?? "json";
@@ -3704,7 +4402,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3704
4402
  const { layer } = resolveApiSlots(extensions);
3705
4403
  const memberExts = extensions.filter((e) => e.apiMembers);
3706
4404
  const headerExts = extensions.filter((e) => e.apiHeader);
3707
- const contracted = routes.filter((r) => r.contract);
4405
+ const contracted = routes.filter((r) => r.contract && isClientCallable(r));
3708
4406
  const ctx = opts.ctx ?? {
3709
4407
  cwd: outDir ?? "",
3710
4408
  outDir: outDir ?? "",
@@ -3756,9 +4454,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3756
4454
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
3757
4455
  }
3758
4456
  lines.push(...extImports);
3759
- lines.push(
3760
- "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
3761
- );
4457
+ lines.push(ROUTES_IMPORT_PLACEHOLDER);
3762
4458
  const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
3763
4459
  lines.push(`import type { Fetcher } from '${runtimeImport}';`);
3764
4460
  if (serialization === "json" && contracted.length > 0) {
@@ -3809,7 +4505,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3809
4505
  lines.push(...statements, "");
3810
4506
  }
3811
4507
  }
3812
- return lines.join("\n");
4508
+ return finishRoutesImport(lines);
3813
4509
  }
3814
4510
  const tree = /* @__PURE__ */ new Map();
3815
4511
  for (const r of contracted) {
@@ -3862,7 +4558,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3862
4558
  lines.push(...statements, "");
3863
4559
  }
3864
4560
  }
3865
- return lines.join("\n");
4561
+ return finishRoutesImport(lines);
3866
4562
  }
3867
4563
 
3868
4564
  // src/emit/emit-cache.ts
@@ -3875,6 +4571,7 @@ async function emitCache(pages, outDir) {
3875
4571
  const s = await stat(p.absolutePath);
3876
4572
  return {
3877
4573
  name: p.name,
4574
+ scope: p.scope ?? "default",
3878
4575
  relativePath: p.relativePath,
3879
4576
  mtime: s.mtime.toISOString()
3880
4577
  };
@@ -4622,6 +5319,7 @@ function buildOpenApiSpec(routes, opts = {}) {
4622
5319
  const paths = {};
4623
5320
  for (const route of routes) {
4624
5321
  if (!route.contract) continue;
5322
+ if (route.method.toUpperCase() === "ALL") continue;
4625
5323
  const oaPath = toOpenApiPath(route.path);
4626
5324
  const method = route.method.toLowerCase();
4627
5325
  let pathItem = paths[oaPath];
@@ -4665,10 +5363,11 @@ async function emitPages(pages, outDir, _options = {}) {
4665
5363
  }).join("\n");
4666
5364
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
4667
5365
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
5366
+ const scopesBlock = buildScopesBlock(pages);
4668
5367
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
4669
5368
 
4670
5369
  export type InertiaPageName = ${pageNameUnion};
4671
- ${propsHelper}
5370
+ ${propsHelper}${scopesBlock}
4672
5371
  declare module '@dudousxd/nestjs-inertia' {
4673
5372
  interface InertiaPages {
4674
5373
  ${augBody}
@@ -4677,6 +5376,31 @@ ${sharedPropsBlock}}
4677
5376
  `;
4678
5377
  await writeFile7(join11(outDir, "pages.d.ts"), content, "utf8");
4679
5378
  }
5379
+ function buildScopesBlock(pages) {
5380
+ const byScope = /* @__PURE__ */ new Map();
5381
+ for (const page of pages) {
5382
+ const scope = page.scope ?? "default";
5383
+ const names = byScope.get(scope) ?? [];
5384
+ names.push(JSON.stringify(page.name));
5385
+ byScope.set(scope, names);
5386
+ }
5387
+ if (![...byScope.keys()].some((scope) => scope !== "default")) return "";
5388
+ if (!byScope.has("default")) byScope.set("default", []);
5389
+ const scopes = ["default", ...[...byScope.keys()].filter((s) => s !== "default")];
5390
+ const body = scopes.map((scope) => {
5391
+ const key = needsQuotes(scope) ? JSON.stringify(scope) : scope;
5392
+ const names = byScope.get(scope) ?? [];
5393
+ return ` ${key}: ${names.length ? names.join(" | ") : "never"};`;
5394
+ }).join("\n");
5395
+ return `
5396
+ /** The page names of each Inertia app: \`default\` (forRoot) and every \`pages.scopes\` entry. */
5397
+ export interface InertiaScopePages {
5398
+ ${body}
5399
+ }
5400
+
5401
+ export type InertiaScopePageName<S extends keyof InertiaScopePages> = InertiaScopePages[S];
5402
+ `;
5403
+ }
4680
5404
  function buildSharedPropsBlock(sharedProps) {
4681
5405
  if (!sharedProps) return "";
4682
5406
  if (sharedProps.isImportRef) {
@@ -4884,8 +5608,7 @@ function diffConfigKeyHashes(previous, current) {
4884
5608
  return [...keys].filter((key) => previous[key] !== current[key]).sort();
4885
5609
  }
4886
5610
  async function discoverInputFiles(config) {
4887
- const globs = [config.contracts.glob, config.forms.watch];
4888
- if (config.pages) globs.push(config.pages.glob);
5611
+ const globs = [config.contracts.glob, config.forms.watch, ...allPageGlobs(config)];
4889
5612
  const cwd = config.codegen.cwd;
4890
5613
  const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
4891
5614
  return [...new Set(matched)].sort();
@@ -5016,12 +5739,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5016
5739
  }
5017
5740
  if (config.pages) {
5018
5741
  const pagesConfig = config.pages;
5019
- const pages = await discoverPages({
5020
- glob: pagesConfig.glob,
5021
- cwd: config.codegen.cwd,
5022
- propsExport: pagesConfig.propsExport,
5023
- componentNameStrategy: pagesConfig.componentNameStrategy
5024
- });
5742
+ const pages = await discoverPageScopes(config);
5025
5743
  const sharedProps = discoverSharedPropsFromConfig(config);
5026
5744
  await emitPages(pages, config.codegen.outDir, {
5027
5745
  propsExport: pagesConfig.propsExport,
@@ -5196,12 +5914,17 @@ async function watch(config, onChange, options = {}) {
5196
5914
  await runInitialPass();
5197
5915
  }
5198
5916
  let pagesDebounceTimer;
5199
- const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
5200
- const pagesWatcher = chokidar.watch(join16(config.codegen.cwd, pagesGlob), {
5201
- ignoreInitial: true,
5202
- persistent: true,
5203
- awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5204
- });
5917
+ const pageGlobs = allPageGlobs(config);
5918
+ const pagesWatcher = chokidar.watch(
5919
+ (pageGlobs.length ? pageGlobs : [".nestjs-codegen-no-pages"]).map(
5920
+ (glob) => join16(config.codegen.cwd, glob)
5921
+ ),
5922
+ {
5923
+ ignoreInitial: true,
5924
+ persistent: true,
5925
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5926
+ }
5927
+ );
5205
5928
  function schedulePagesRegenerate() {
5206
5929
  if (pagesDebounceTimer !== void 0) {
5207
5930
  clearTimeout(pagesDebounceTimer);
@@ -5281,7 +6004,7 @@ async function watch(config, onChange, options = {}) {
5281
6004
  }
5282
6005
 
5283
6006
  // src/index.ts
5284
- var VERSION = "0.25.2";
6007
+ var VERSION = "0.27.0";
5285
6008
 
5286
6009
  // src/cli/codegen.ts
5287
6010
  async function runCodegen(opts = {}) {