@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/index.js CHANGED
@@ -88,12 +88,33 @@ function validateUserConfig(userConfig) {
88
88
  "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
89
89
  );
90
90
  }
91
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
91
+ if (userConfig.pages && !isGlobInput(userConfig.pages.glob)) {
92
92
  throw new ConfigError(
93
- "Config validation failed: `pages.glob` must be a string when `pages` is set"
93
+ "Config validation failed: `pages.glob` must be a string or a non-empty array of strings when `pages` is set"
94
+ );
95
+ }
96
+ for (const [scope, scopeConfig] of Object.entries(userConfig.pages?.scopes ?? {})) {
97
+ if (scope === "default") {
98
+ throw new ConfigError(
99
+ 'Config validation failed: `pages.scopes` cannot declare "default" \u2014 the default app is `pages.glob` (InertiaModule.forRoot).'
100
+ );
101
+ }
102
+ if (!isGlobInput(scopeConfig?.glob)) {
103
+ throw new ConfigError(
104
+ `Config validation failed: \`pages.scopes.${scope}.glob\` must be a string or a non-empty array of strings`
105
+ );
106
+ }
107
+ }
108
+ if (userConfig.scopes && Object.keys(userConfig.scopes).length > 0) {
109
+ console.warn(
110
+ "[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)."
94
111
  );
95
112
  }
96
113
  }
114
+ function isGlobInput(value) {
115
+ if (typeof value === "string") return true;
116
+ return Array.isArray(value) && value.length > 0 && value.every((glob) => typeof glob === "string");
117
+ }
97
118
  function applyDefaults(userConfig, cwd) {
98
119
  validateUserConfig(userConfig);
99
120
  const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
@@ -120,7 +141,17 @@ function applyDefaults(userConfig, cwd) {
120
141
  pages: userConfig.pages ? {
121
142
  glob: userConfig.pages.glob,
122
143
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
123
- componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext"
144
+ componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext",
145
+ scopes: Object.fromEntries(
146
+ Object.entries(userConfig.pages.scopes ?? {}).map(([scope, scopeConfig]) => [
147
+ scope,
148
+ {
149
+ glob: scopeConfig.glob,
150
+ prefix: scopeConfig.prefix ?? `${scope}/`,
151
+ componentNameStrategy: scopeConfig.componentNameStrategy ?? userConfig.pages?.componentNameStrategy ?? "relative-no-ext"
152
+ }
153
+ ])
154
+ )
124
155
  } : null,
125
156
  contracts: {
126
157
  glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
@@ -194,26 +225,84 @@ import { dirname as dirname4, join as join14 } from "path";
194
225
 
195
226
  // src/discovery/pages.ts
196
227
  import { readFile } from "fs/promises";
197
- import { join as join2, relative as relative2 } from "path";
228
+ import { join as join2, relative as relative2, sep as sep2 } from "path";
198
229
  import fg from "fast-glob";
199
230
  var NON_PAGE_FILE_RE = /\.(?:test|spec|stories|story)\.(?:tsx?|jsx?|vue|svelte)$/i;
231
+ var MAGIC_SEGMENT_RE = /[*?{}[\]()!]/;
232
+ function globStaticBase(glob) {
233
+ const segments = glob.split("/");
234
+ const staticSegments = [];
235
+ for (const segment of segments.slice(0, -1)) {
236
+ if (MAGIC_SEGMENT_RE.test(segment)) break;
237
+ staticSegments.push(segment);
238
+ }
239
+ return staticSegments.join("/");
240
+ }
200
241
  async function discoverPages(opts) {
201
- const allFiles = await fg(opts.glob, { cwd: opts.cwd, absolute: true });
202
- const files = allFiles.filter((f) => !NON_PAGE_FILE_RE.test(f));
203
- files.sort();
204
- const globStatic = opts.glob.split("*")[0]?.replace(/\/$/, "") ?? "";
205
- const pagesBase = join2(opts.cwd, globStatic);
242
+ const globs = typeof opts.glob === "string" ? [opts.glob] : opts.glob;
243
+ const prefix = opts.prefix ?? "";
244
+ const scope = opts.scope ?? "default";
245
+ const seenFiles = /* @__PURE__ */ new Set();
206
246
  const out = [];
207
- for (const file of files) {
208
- const rel = relative2(opts.cwd, file);
209
- const nameRel = relative2(pagesBase, file);
210
- const name = computeName(nameRel, opts.componentNameStrategy);
211
- const source = await readFile(file, "utf8");
212
- const propsSource = extractPropsSource(source, opts.propsExport);
213
- out.push({ name, absolutePath: file, relativePath: rel, propsSource });
247
+ for (const glob of globs) {
248
+ const files = (await fg(glob, { cwd: opts.cwd, absolute: true })).filter((f) => !NON_PAGE_FILE_RE.test(f) && !seenFiles.has(f)).sort();
249
+ const pagesBase = join2(opts.cwd, globStaticBase(glob));
250
+ for (const file of files) {
251
+ seenFiles.add(file);
252
+ const rel = relative2(opts.cwd, file);
253
+ const nameRel = relative2(pagesBase, file).split(sep2).join("/");
254
+ const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
255
+ const source = await readFile(file, "utf8");
256
+ const propsSource = extractPropsSource(source, opts.propsExport);
257
+ out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
258
+ }
259
+ }
260
+ assertUniqueNames(out);
261
+ return out;
262
+ }
263
+ async function discoverPageScopes(config) {
264
+ const pages = config.pages;
265
+ if (!pages) return [];
266
+ const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
267
+ const out = await discoverPages({
268
+ ...common,
269
+ glob: pages.glob,
270
+ componentNameStrategy: pages.componentNameStrategy
271
+ });
272
+ for (const [scope, scopeConfig] of Object.entries(pages.scopes ?? {})) {
273
+ out.push(
274
+ ...await discoverPages({
275
+ ...common,
276
+ glob: scopeConfig.glob,
277
+ componentNameStrategy: scopeConfig.componentNameStrategy,
278
+ prefix: scopeConfig.prefix,
279
+ scope
280
+ })
281
+ );
214
282
  }
283
+ assertUniqueNames(out);
215
284
  return out;
216
285
  }
286
+ function allPageGlobs(config) {
287
+ if (!config.pages) return [];
288
+ const asList = (glob) => typeof glob === "string" ? [glob] : glob;
289
+ return [
290
+ ...asList(config.pages.glob),
291
+ ...Object.values(config.pages.scopes ?? {}).flatMap((scope) => asList(scope.glob))
292
+ ];
293
+ }
294
+ function assertUniqueNames(pages) {
295
+ const byName = /* @__PURE__ */ new Map();
296
+ for (const page of pages) {
297
+ const previous = byName.get(page.name);
298
+ if (previous) {
299
+ throw new Error(
300
+ `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.`
301
+ );
302
+ }
303
+ byName.set(page.name, page);
304
+ }
305
+ }
217
306
  function computeName(rel, strat) {
218
307
  if (typeof strat === "function") return strat(rel);
219
308
  const noExt = rel.replace(/\.(tsx?|vue|svelte)$/, "");
@@ -523,22 +612,22 @@ import { isAbsolute as isAbsolute2, join as join5, relative as relative4 } from
523
612
 
524
613
  // src/extension/registry.ts
525
614
  import { relative as relative3, resolve as resolve4 } from "path";
526
- import { Project as Project4 } from "ts-morph";
615
+ import { Project as Project5 } from "ts-morph";
527
616
 
528
617
  // src/discovery/contracts-fast.ts
529
618
  import { existsSync } from "fs";
530
619
  import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
531
620
  import fg2 from "fast-glob";
532
621
  import {
533
- Node as Node9,
534
- Project as Project3,
535
- ts
622
+ Node as Node11,
623
+ Project as Project4,
624
+ ts as ts3
536
625
  } from "ts-morph";
537
626
 
538
627
  // src/discovery/dto-type-resolver.ts
539
628
  import {
540
- Node as Node7,
541
- SyntaxKind as SyntaxKind3
629
+ Node as Node10,
630
+ SyntaxKind as SyntaxKind4
542
631
  } from "ts-morph";
543
632
 
544
633
  // src/discovery/dto-to-ir.ts
@@ -2076,6 +2165,674 @@ function extractFilterableEntityFields(filterClass, project, entityOverride, pre
2076
2165
  return fields;
2077
2166
  }
2078
2167
 
2168
+ // src/discovery/pipe-schema.ts
2169
+ import { Node as Node9 } from "ts-morph";
2170
+
2171
+ // src/discovery/schema-type-inference.ts
2172
+ import {
2173
+ ts as ts2
2174
+ } from "ts-morph";
2175
+
2176
+ // src/discovery/schema-type-program.ts
2177
+ import { Node as Node7, Project as Project3, ts } from "ts-morph";
2178
+ var PARAM_DECORATORS = /* @__PURE__ */ new Set(["Body", "Query", "Param"]);
2179
+ var SchemaTypeProgram = class {
2180
+ constructor(discovery) {
2181
+ this.discovery = discovery;
2182
+ const base = discovery.getCompilerOptions();
2183
+ this.options = {
2184
+ ...base,
2185
+ // A tsconfig-less consumer gets ES5 by default, whose lib has no `Map`/`Set`/
2186
+ // `Promise` for the schema libraries' declarations to lean on.
2187
+ target: base.target ?? ts.ScriptTarget.ES2022,
2188
+ // With neither set, resolve the way a bundler does: package `exports`
2189
+ // (`zod/v4`) and extensionless or `.js` relative imports all resolve.
2190
+ ...base.module === void 0 && base.moduleResolution === void 0 ? { module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler } : {},
2191
+ // Without it `.nullable()` and `.optional()` would erase to the bare type.
2192
+ strictNullChecks: true,
2193
+ // No ambient `@types/*` (a tsconfig's `types: ['node']`): a schema does not
2194
+ // need them, and they are a hundred-odd files of program to build.
2195
+ types: [],
2196
+ noEmit: true
2197
+ };
2198
+ this.project = new Project3({
2199
+ skipAddingFilesFromTsConfig: true,
2200
+ compilerOptions: this.options
2201
+ });
2202
+ for (const sf of discovery.getSourceFiles()) this.request(sf.getFilePath(), { schema: true });
2203
+ this.flush();
2204
+ }
2205
+ project;
2206
+ options;
2207
+ /** Parses a file that is not in discovery's Project, without adding it there. */
2208
+ scratch = new Project3({
2209
+ skipAddingFilesFromTsConfig: true,
2210
+ skipLoadingLibFiles: true,
2211
+ skipFileDependencyResolution: true
2212
+ });
2213
+ needs = /* @__PURE__ */ new Map();
2214
+ /** Pruned text per file, as last written into {@link project}. */
2215
+ written = /* @__PURE__ */ new Map();
2216
+ /**
2217
+ * The typed copy of a discovery file, with its schema positions kept. Adding one
2218
+ * that was not there yet (a contract file discovery reached lazily) rebuilds the
2219
+ * program once.
2220
+ */
2221
+ sourceFileFor(sf) {
2222
+ this.request(sf.getFilePath(), { schema: true });
2223
+ this.flush();
2224
+ return this.project.getSourceFile(sf.getFilePath());
2225
+ }
2226
+ // -------------------------------------------------------------------------
2227
+ queue = [];
2228
+ request(path, want) {
2229
+ let needs = this.needs.get(path);
2230
+ const isNew = !needs;
2231
+ needs ??= { schema: false, all: false, names: /* @__PURE__ */ new Set() };
2232
+ let grew = isNew;
2233
+ if (want.schema && !needs.schema) {
2234
+ needs.schema = true;
2235
+ grew = true;
2236
+ }
2237
+ if (want.all && !needs.all) {
2238
+ needs.all = true;
2239
+ grew = true;
2240
+ }
2241
+ for (const name of want.names ?? []) {
2242
+ if (!needs.names.has(name)) {
2243
+ needs.names.add(name);
2244
+ grew = true;
2245
+ }
2246
+ }
2247
+ this.needs.set(path, needs);
2248
+ if (grew) this.queue.push(path);
2249
+ }
2250
+ /** Run the worklist to a fixpoint, then write every changed file into the program. */
2251
+ flush() {
2252
+ const changed = /* @__PURE__ */ new Map();
2253
+ while (this.queue.length > 0) {
2254
+ const path = this.queue.pop();
2255
+ const original = this.original(path);
2256
+ if (!original) continue;
2257
+ const needs = this.needs.get(path);
2258
+ const text = this.prune(original, needs);
2259
+ if (this.written.get(path) !== text) changed.set(path, text);
2260
+ }
2261
+ for (const [path, text] of changed) {
2262
+ this.project.createSourceFile(path, text, { overwrite: true });
2263
+ this.written.set(path, text);
2264
+ }
2265
+ }
2266
+ /** The file as discovery sees it (its in-memory text), else as it is on disk. */
2267
+ original(path) {
2268
+ return this.discovery.getSourceFile(path) ?? this.scratch.getSourceFile(path) ?? this.scratch.addSourceFileAtPathIfExists(path);
2269
+ }
2270
+ /**
2271
+ * `sf`'s text with every import/re-export nothing in `needs` references blanked,
2272
+ * requesting from each kept one's target the names it brings in.
2273
+ */
2274
+ prune(sf, needs) {
2275
+ const locals = topLevelDeclarations(sf);
2276
+ const roots = [];
2277
+ if (needs.all) roots.push(...sf.getStatements());
2278
+ if (needs.schema) roots.push(...schemaPositions(sf));
2279
+ const reexported = /* @__PURE__ */ new Map();
2280
+ for (const exp of sf.getExportDeclarations()) {
2281
+ const spec = exp.getModuleSpecifierValue();
2282
+ if (spec === void 0) {
2283
+ for (const named of exp.getNamedExports()) {
2284
+ const exposed = (named.getAliasNode() ?? named.getNameNode()).getText();
2285
+ if (needs.all || needs.names.has(exposed)) roots.push(named.getNameNode());
2286
+ }
2287
+ continue;
2288
+ }
2289
+ if (exp.isNamespaceExport() || exp.getNamedExports().length === 0) continue;
2290
+ for (const named of exp.getNamedExports()) {
2291
+ const exposed = (named.getAliasNode() ?? named.getNameNode()).getText();
2292
+ const list = reexported.get(exposed) ?? [];
2293
+ list.push({ from: spec, name: named.getNameNode().getText() });
2294
+ reexported.set(exposed, list);
2295
+ }
2296
+ }
2297
+ const unresolvedNames = [];
2298
+ for (const name of needs.names) {
2299
+ const decls = name === "default" ? defaultExport(sf) : locals.get(name) ?? [];
2300
+ if (decls.length > 0) roots.push(...decls);
2301
+ else if (!reexported.has(name)) unresolvedNames.push(name);
2302
+ }
2303
+ const referenced = /* @__PURE__ */ new Set();
2304
+ const queue = [...roots];
2305
+ const seen = new Set(queue);
2306
+ while (queue.length > 0) {
2307
+ const node = queue.pop();
2308
+ const ids = Node7.isIdentifier(node) ? [node] : node.getDescendantsOfKind(ts.SyntaxKind.Identifier);
2309
+ for (const id of ids) {
2310
+ const name = id.getText();
2311
+ if (referenced.has(name)) continue;
2312
+ referenced.add(name);
2313
+ for (const local of locals.get(name) ?? []) {
2314
+ if (seen.has(local)) continue;
2315
+ seen.add(local);
2316
+ queue.push(local);
2317
+ }
2318
+ }
2319
+ }
2320
+ const chars = sf.getFullText().split("");
2321
+ const blank = (node) => {
2322
+ for (let i = node.getStart(); i < node.getEnd(); i++) {
2323
+ if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
2324
+ }
2325
+ };
2326
+ for (const decl of sf.getImportDeclarations()) {
2327
+ const target = this.resolve(decl.getModuleSpecifierValue(), sf);
2328
+ const namespace = decl.getNamespaceImport()?.getText();
2329
+ const def = decl.getDefaultImport()?.getText();
2330
+ const named = decl.getNamedImports().map((n) => ({
2331
+ local: (n.getAliasNode() ?? n.getNameNode()).getText(),
2332
+ imported: n.getName()
2333
+ })).filter((n) => referenced.has(n.local));
2334
+ const wantsNamespace = namespace !== void 0 && referenced.has(namespace);
2335
+ const wantsDefault = def !== void 0 && referenced.has(def);
2336
+ if (!wantsNamespace && !wantsDefault && named.length === 0) {
2337
+ blank(decl);
2338
+ continue;
2339
+ }
2340
+ if (target) {
2341
+ this.request(target, {
2342
+ all: wantsNamespace,
2343
+ names: [...wantsDefault ? ["default"] : [], ...named.map((n) => n.imported)]
2344
+ });
2345
+ }
2346
+ }
2347
+ for (const exp of sf.getExportDeclarations()) {
2348
+ const spec = exp.getModuleSpecifierValue();
2349
+ if (spec === void 0) continue;
2350
+ const target = this.resolve(spec, sf);
2351
+ if (exp.isNamespaceExport() || exp.getNamedExports().length === 0) {
2352
+ if (!needs.all && unresolvedNames.length === 0) {
2353
+ blank(exp);
2354
+ } else if (target) {
2355
+ this.request(target, { all: needs.all, names: unresolvedNames });
2356
+ }
2357
+ continue;
2358
+ }
2359
+ const wanted = exp.getNamedExports().filter(
2360
+ (n) => needs.all || needs.names.has((n.getAliasNode() ?? n.getNameNode()).getText())
2361
+ ).map((n) => n.getNameNode().getText());
2362
+ if (wanted.length === 0) blank(exp);
2363
+ else if (target) this.request(target, { names: wanted });
2364
+ }
2365
+ return chars.join("");
2366
+ }
2367
+ /**
2368
+ * The project source file a module specifier resolves to, or undefined for a
2369
+ * package (read unpruned by the program itself) or an unresolvable one.
2370
+ */
2371
+ resolve(specifier, from) {
2372
+ const resolved = ts.resolveModuleName(
2373
+ specifier,
2374
+ from.getFilePath(),
2375
+ this.options,
2376
+ ts.sys
2377
+ ).resolvedModule;
2378
+ if (!resolved || resolved.isExternalLibraryImport) return void 0;
2379
+ const file = resolved.resolvedFileName;
2380
+ if (file.endsWith(".d.ts") || !/\.(m|c)?tsx?$/.test(file)) return void 0;
2381
+ return file;
2382
+ }
2383
+ };
2384
+ function schemaPositions(sf) {
2385
+ const roots = [];
2386
+ sf.forEachDescendant((node) => {
2387
+ if (Node7.isDecorator(node)) {
2388
+ const name = node.getName();
2389
+ if (name === "ApplyContract") {
2390
+ roots.push(...node.getArguments());
2391
+ } else if (PARAM_DECORATORS.has(name)) {
2392
+ for (const arg of node.getArguments()) {
2393
+ if (Node7.isNewExpression(arg) || Node7.isCallExpression(arg)) {
2394
+ roots.push(...arg.getArguments());
2395
+ }
2396
+ }
2397
+ }
2398
+ } else if (Node7.isCallExpression(node)) {
2399
+ const callee = node.getExpression();
2400
+ const name = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : void 0;
2401
+ if (name === "defineContract") roots.push(...node.getArguments());
2402
+ }
2403
+ });
2404
+ return roots;
2405
+ }
2406
+ function topLevelDeclarations(sf) {
2407
+ const byName = /* @__PURE__ */ new Map();
2408
+ const add = (name, node) => {
2409
+ if (!name) return;
2410
+ const list = byName.get(name);
2411
+ if (list) list.push(node);
2412
+ else byName.set(name, [node]);
2413
+ };
2414
+ for (const decl of sf.getVariableDeclarations()) add(decl.getName(), decl);
2415
+ for (const decl of sf.getFunctions()) add(decl.getName(), decl);
2416
+ for (const decl of sf.getClasses()) add(decl.getName(), decl);
2417
+ for (const decl of sf.getEnums()) add(decl.getName(), decl);
2418
+ for (const decl of sf.getInterfaces()) add(decl.getName(), decl);
2419
+ for (const decl of sf.getTypeAliases()) add(decl.getName(), decl);
2420
+ for (const decl of sf.getModules()) add(decl.getName(), decl);
2421
+ return byName;
2422
+ }
2423
+ function defaultExport(sf) {
2424
+ const nodes = [];
2425
+ for (const assignment of sf.getExportAssignments()) nodes.push(assignment);
2426
+ for (const fn of sf.getFunctions()) if (fn.isDefaultExport()) nodes.push(fn);
2427
+ for (const cls of sf.getClasses()) if (cls.isDefaultExport()) nodes.push(cls);
2428
+ return nodes;
2429
+ }
2430
+
2431
+ // src/discovery/schema-type-inference.ts
2432
+ var programs = /* @__PURE__ */ new WeakMap();
2433
+ function programFor(discovery) {
2434
+ let program = programs.get(discovery);
2435
+ if (!program) {
2436
+ program = new SchemaTypeProgram(discovery);
2437
+ programs.set(discovery, program);
2438
+ }
2439
+ return program;
2440
+ }
2441
+ function clearSchemaTypeProject(discovery) {
2442
+ programs.delete(discovery);
2443
+ }
2444
+ function counterpart(typedSf, node) {
2445
+ const start = node.getStart();
2446
+ const end = node.getEnd();
2447
+ const kind = node.getKind();
2448
+ let candidate = typedSf.getDescendantAtPos(start);
2449
+ while (candidate) {
2450
+ if (candidate.getStart() === start && candidate.getEnd() === end && candidate.getKind() === kind) {
2451
+ return candidate;
2452
+ }
2453
+ if (candidate.getStart() < start || candidate.getEnd() > end) return void 0;
2454
+ candidate = candidate.getParent();
2455
+ }
2456
+ return void 0;
2457
+ }
2458
+ function propType(type, name, at) {
2459
+ return type.getProperty(name)?.getTypeAtLocation(at);
2460
+ }
2461
+ function isAnyOrUnknown(type) {
2462
+ return type.isAny() || type.isUnknown();
2463
+ }
2464
+ function inferSchema(expr, discovery) {
2465
+ try {
2466
+ const typedSf = programFor(discovery).sourceFileFor(expr.getSourceFile());
2467
+ const at = typedSf && counterpart(typedSf, expr);
2468
+ if (!at) return { kind: "unresolved" };
2469
+ const schemaType = at.getType();
2470
+ if (isAnyOrUnknown(schemaType)) return { kind: "unresolved" };
2471
+ const standard = propType(schemaType, "~standard", at);
2472
+ const stdTypes = standard ? propType(standard, "types", at)?.getNonNullableType() : void 0;
2473
+ const stdInput = stdTypes ? propType(stdTypes, "input", at) : void 0;
2474
+ const stdOutput = stdTypes ? propType(stdTypes, "output", at) : void 0;
2475
+ if (stdInput && stdOutput) {
2476
+ return { kind: "schema", types: { input: stdInput, output: stdOutput, at } };
2477
+ }
2478
+ const zodInput = propType(schemaType, "_input", at);
2479
+ const zodOutput = propType(schemaType, "_output", at);
2480
+ if (zodInput && zodOutput) {
2481
+ return { kind: "schema", types: { input: zodInput, output: zodOutput, at } };
2482
+ }
2483
+ return { kind: "not-schema" };
2484
+ } catch (err) {
2485
+ dbg("schema inference failed:", err);
2486
+ return { kind: "unresolved" };
2487
+ }
2488
+ }
2489
+ var GLOBAL_NAMED_TYPES = /* @__PURE__ */ new Set([
2490
+ "Date",
2491
+ "File",
2492
+ "Blob",
2493
+ "FormData",
2494
+ "URL",
2495
+ "RegExp",
2496
+ "Map",
2497
+ "Set",
2498
+ "ArrayBuffer",
2499
+ "Uint8Array"
2500
+ ]);
2501
+ var MAX_DEPTH = 12;
2502
+ function objectKey(name) {
2503
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2504
+ }
2505
+ function isOptionalSymbol(sym) {
2506
+ return (sym.getFlags() & ts2.SymbolFlags.Optional) !== 0;
2507
+ }
2508
+ function isSymbolKeyed(sym) {
2509
+ return sym.getName().startsWith("__@");
2510
+ }
2511
+ function orderedProperties(type) {
2512
+ const position = (sym) => {
2513
+ const decl = sym.getDeclarations()[0];
2514
+ return decl ? [decl.getSourceFile().getFilePath(), decl.getStart()] : void 0;
2515
+ };
2516
+ return type.getProperties().map((sym, index) => ({ sym, index, at: position(sym) })).sort((a, b) => {
2517
+ if (a.at && b.at) {
2518
+ if (a.at[0] !== b.at[0]) return a.at[0] < b.at[0] ? -1 : 1;
2519
+ if (a.at[1] !== b.at[1]) return a.at[1] - b.at[1];
2520
+ } else if (a.at || b.at) {
2521
+ return a.at ? -1 : 1;
2522
+ }
2523
+ return a.index - b.index;
2524
+ }).map((entry) => entry.sym);
2525
+ }
2526
+ function isFromDeclarationFile(sym) {
2527
+ const decl = sym?.getDeclarations()[0];
2528
+ return !!decl && decl.getSourceFile().isDeclarationFile();
2529
+ }
2530
+ function render(type, state, paired) {
2531
+ if (state.depth > MAX_DEPTH) return "unknown";
2532
+ if (isAnyOrUnknown(type)) {
2533
+ return paired && !isAnyOrUnknown(paired) ? render(paired, state) : "unknown";
2534
+ }
2535
+ if (type.isNever()) return "never";
2536
+ if (type.isString() || type.isTemplateLiteral()) return "string";
2537
+ if (type.isNumber()) return "number";
2538
+ if (type.isBoolean()) return "boolean";
2539
+ if (type.isBigInt()) return "bigint";
2540
+ if (type.isNull()) return "null";
2541
+ if (type.isUndefined() || type.isVoid()) return "undefined";
2542
+ if (type.isBooleanLiteral()) return type.getText();
2543
+ if (type.isStringLiteral() || type.isNumberLiteral() || type.isEnumLiteral()) {
2544
+ const value = type.getLiteralValue();
2545
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
2546
+ }
2547
+ if (type.isBigIntLiteral()) return type.getText();
2548
+ if (type.isTypeParameter()) return "unknown";
2549
+ if (type.isUnion()) return renderUnion(type, state, paired);
2550
+ if (type.isIntersection()) return renderIntersection(type, state);
2551
+ const next = { ...state, depth: state.depth + 1 };
2552
+ if (type.isArray() || type.isReadonlyArray()) {
2553
+ const element = type.getArrayElementType() ?? type.getTypeArguments()[0];
2554
+ const pairedElement = paired?.getArrayElementType() ?? paired?.getTypeArguments()[0];
2555
+ return element ? `Array<${render(element, next, pairedElement)}>` : "Array<unknown>";
2556
+ }
2557
+ if (type.isTuple()) {
2558
+ const elements = type.getTupleElements();
2559
+ const pairedElements = paired?.isTuple() ? paired.getTupleElements() : [];
2560
+ return `[${elements.map((e, i) => render(e, next, pairedElements[i])).join(", ")}]`;
2561
+ }
2562
+ if (type.isObject()) return renderObject(type, next, paired);
2563
+ return "unknown";
2564
+ }
2565
+ function renderUnion(type, state, paired) {
2566
+ const members = type.getUnionTypes();
2567
+ const hasTrue = members.some((m) => m.isBooleanLiteral() && m.getText() === "true");
2568
+ const hasFalse = members.some((m) => m.isBooleanLiteral() && m.getText() === "false");
2569
+ const pairedNonNull = paired?.getNonNullableType();
2570
+ const arms = [];
2571
+ let sawBoolean = false;
2572
+ let hasNull = false;
2573
+ let hasUndefined = false;
2574
+ for (const member of members) {
2575
+ if (member.isNull()) {
2576
+ hasNull = true;
2577
+ continue;
2578
+ }
2579
+ if (member.isUndefined()) {
2580
+ hasUndefined = true;
2581
+ continue;
2582
+ }
2583
+ if (member.isBooleanLiteral() && hasTrue && hasFalse) {
2584
+ if (!sawBoolean) arms.push("boolean");
2585
+ sawBoolean = true;
2586
+ continue;
2587
+ }
2588
+ arms.push(render(member, state, members.length === 1 ? paired : pairedNonNull));
2589
+ }
2590
+ if (hasNull) arms.push("null");
2591
+ if (hasUndefined) arms.push("undefined");
2592
+ const unique = [...new Set(arms)];
2593
+ if (unique.includes("unknown")) return "unknown";
2594
+ return unique.join(" | ");
2595
+ }
2596
+ function renderIntersection(type, state) {
2597
+ const members = type.getIntersectionTypes();
2598
+ const primitive = members.find((m) => !m.isObject());
2599
+ if (primitive) return render(primitive, state);
2600
+ return renderObject(type, { ...state, depth: state.depth + 1 }, void 0);
2601
+ }
2602
+ function renderObject(type, state, paired) {
2603
+ const symbol = type.getSymbol() ?? type.getAliasSymbol();
2604
+ const name = symbol?.getName();
2605
+ if (name && GLOBAL_NAMED_TYPES.has(name) && isFromDeclarationFile(symbol)) {
2606
+ const args = type.getTypeArguments();
2607
+ return args.length ? `${name}<${args.map((a) => render(a, state)).join(", ")}>` : name;
2608
+ }
2609
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
2610
+ return "unknown";
2611
+ }
2612
+ if (type.isClassOrInterface() && isFromDeclarationFile(symbol)) return "unknown";
2613
+ if (state.stack.has(type.compilerType)) return "unknown";
2614
+ const stack = new Set(state.stack).add(type.compilerType);
2615
+ const inner = { ...state, stack };
2616
+ const fields = [];
2617
+ for (const prop of orderedProperties(type)) {
2618
+ if (isSymbolKeyed(prop)) continue;
2619
+ const propName = prop.getName();
2620
+ const propT = prop.getTypeAtLocation(state.at);
2621
+ if (propT.getCallSignatures().length > 0 && !propT.isUnion()) continue;
2622
+ const pairedProp = paired?.getProperty(propName)?.getTypeAtLocation(state.at);
2623
+ const optional = isOptionalSymbol(prop) ? "?" : "";
2624
+ fields.push(`${objectKey(propName)}${optional}: ${render(propT, inner, pairedProp)}`);
2625
+ }
2626
+ const stringIndex = type.getStringIndexType();
2627
+ if (stringIndex) {
2628
+ const pairedIndex = paired?.getStringIndexType();
2629
+ const value = render(stringIndex, inner, pairedIndex);
2630
+ if (fields.length === 0) return `Record<string, ${value}>`;
2631
+ fields.push(`[key: string]: ${value}`);
2632
+ }
2633
+ return fields.length ? `{ ${fields.join("; ")} }` : "{}";
2634
+ }
2635
+ function stateAt(at) {
2636
+ return { at, stack: /* @__PURE__ */ new Set(), depth: 0 };
2637
+ }
2638
+ function requestTypeOf(types) {
2639
+ return render(types.input, stateAt(types.at), types.output);
2640
+ }
2641
+ function responseTypeOf(types) {
2642
+ return render(types.output, stateAt(types.at));
2643
+ }
2644
+ function requestFieldsOf(types) {
2645
+ const { input, output, at } = types;
2646
+ const objectInput = input.getNonNullableType();
2647
+ if (!objectInput.isObject() || objectInput.isArray() || objectInput.isTuple()) return null;
2648
+ const state = stateAt(at);
2649
+ return orderedProperties(objectInput).filter((prop) => !isSymbolKeyed(prop)).map((prop) => ({
2650
+ name: prop.getName(),
2651
+ type: render(
2652
+ prop.getTypeAtLocation(at),
2653
+ state,
2654
+ output.getProperty(prop.getName())?.getTypeAtLocation(at)
2655
+ ),
2656
+ optional: isOptionalSymbol(prop)
2657
+ }));
2658
+ }
2659
+ function acceptsUndefined(types) {
2660
+ const { input, output } = types;
2661
+ const accepted = isAnyOrUnknown(input) ? output : input;
2662
+ if (isAnyOrUnknown(accepted) || accepted.isUndefined()) return true;
2663
+ return accepted.isUnion() && accepted.getUnionTypes().some((t) => t.isUndefined());
2664
+ }
2665
+
2666
+ // src/discovery/zod-ast-to-ts.ts
2667
+ import { Node as Node8, SyntaxKind as SyntaxKind3 } from "ts-morph";
2668
+ function zodAstToTs(node) {
2669
+ if (!Node8.isCallExpression(node)) return "unknown";
2670
+ const expr = node.getExpression();
2671
+ if (Node8.isPropertyAccessExpression(expr)) {
2672
+ const methodName = expr.getName();
2673
+ const receiver = expr.getExpression();
2674
+ if (methodName === "optional") {
2675
+ return `${zodAstToTs(receiver)} | undefined`;
2676
+ }
2677
+ if (methodName === "nullable") {
2678
+ return `${zodAstToTs(receiver)} | null`;
2679
+ }
2680
+ const args = node.getArguments();
2681
+ switch (methodName) {
2682
+ case "string":
2683
+ return "string";
2684
+ case "number":
2685
+ return "number";
2686
+ case "boolean":
2687
+ return "boolean";
2688
+ case "unknown":
2689
+ return "unknown";
2690
+ case "any":
2691
+ return "unknown";
2692
+ case "literal": {
2693
+ const lit = args[0];
2694
+ if (!lit) return "unknown";
2695
+ if (Node8.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2696
+ if (Node8.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2697
+ if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
2698
+ if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
2699
+ return "unknown";
2700
+ }
2701
+ case "enum": {
2702
+ const arrArg = args[0];
2703
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2704
+ const members = arrArg.getElements().map(
2705
+ (el) => Node8.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2706
+ );
2707
+ return members.join(" | ");
2708
+ }
2709
+ case "array": {
2710
+ const inner = args[0];
2711
+ if (!inner) return "unknown";
2712
+ return `Array<${zodAstToTs(inner)}>`;
2713
+ }
2714
+ case "object": {
2715
+ const objArg = args[0];
2716
+ if (!objArg || !Node8.isObjectLiteralExpression(objArg)) return "unknown";
2717
+ const lines = [];
2718
+ for (const prop of objArg.getProperties()) {
2719
+ if (!Node8.isPropertyAssignment(prop)) continue;
2720
+ const key = prop.getName();
2721
+ const valNode = prop.getInitializer();
2722
+ if (!valNode) continue;
2723
+ const tsType = zodAstToTs(valNode);
2724
+ const isOpt = isOptionalChain(valNode);
2725
+ lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
2726
+ }
2727
+ return `{ ${lines.join("; ")} }`;
2728
+ }
2729
+ case "union": {
2730
+ const arrArg = args[0];
2731
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2732
+ return arrArg.getElements().map(zodAstToTs).join(" | ");
2733
+ }
2734
+ case "record": {
2735
+ const valArg = args.length === 1 ? args[0] : args[1];
2736
+ if (!valArg) return "unknown";
2737
+ return `Record<string, ${zodAstToTs(valArg)}>`;
2738
+ }
2739
+ case "tuple": {
2740
+ const arrArg = args[0];
2741
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2742
+ return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
2743
+ }
2744
+ default:
2745
+ return "unknown";
2746
+ }
2747
+ }
2748
+ return "unknown";
2749
+ }
2750
+ function isOptionalChain(node) {
2751
+ if (!Node8.isCallExpression(node)) return false;
2752
+ const expr = node.getExpression();
2753
+ return Node8.isPropertyAccessExpression(expr) && expr.getName() === "optional";
2754
+ }
2755
+ function memberType(val, position, project) {
2756
+ if (project) {
2757
+ const inferred = inferSchema(val, project);
2758
+ if (inferred.kind === "schema") {
2759
+ return position === "request" ? requestTypeOf(inferred.types) : responseTypeOf(inferred.types);
2760
+ }
2761
+ }
2762
+ return zodAstToTs(val);
2763
+ }
2764
+ function parseDefineContractCall(callExpr, project) {
2765
+ if (!Node8.isCallExpression(callExpr)) return null;
2766
+ const callee = callExpr.getExpression();
2767
+ const calleeName = Node8.isIdentifier(callee) ? callee.getText() : Node8.isPropertyAccessExpression(callee) ? callee.getName() : "";
2768
+ if (calleeName !== "defineContract") return null;
2769
+ const args = callExpr.getArguments();
2770
+ const optsArg = args[0];
2771
+ if (!optsArg || !Node8.isObjectLiteralExpression(optsArg)) return null;
2772
+ let query = null;
2773
+ let body = null;
2774
+ let response = "unknown";
2775
+ let error = null;
2776
+ let bodyZodText = null;
2777
+ let queryZodText = null;
2778
+ for (const prop of optsArg.getProperties()) {
2779
+ if (!Node8.isPropertyAssignment(prop)) continue;
2780
+ const propName = prop.getName();
2781
+ const val = prop.getInitializer();
2782
+ if (!val) continue;
2783
+ if (propName === "query") {
2784
+ query = memberType(val, "request", project);
2785
+ queryZodText = val.getText();
2786
+ } else if (propName === "body") {
2787
+ body = memberType(val, "request", project);
2788
+ bodyZodText = val.getText();
2789
+ } else if (propName === "response") {
2790
+ response = memberType(val, "response", project);
2791
+ } else if (propName === "error") {
2792
+ error = memberType(val, "response", project);
2793
+ }
2794
+ }
2795
+ return { query, body, response, error, bodyZodText, queryZodText };
2796
+ }
2797
+
2798
+ // src/discovery/pipe-schema.ts
2799
+ function pipeSchemaOf(decorator, project) {
2800
+ for (const pipe of decorator.getArguments()) {
2801
+ if (!Node9.isNewExpression(pipe) && !Node9.isCallExpression(pipe)) continue;
2802
+ for (const arg of pipe.getArguments()) {
2803
+ if (!couldBeSchema(arg)) continue;
2804
+ const inferred = inferSchema(arg, project);
2805
+ if (inferred.kind === "schema") {
2806
+ return {
2807
+ type: requestTypeOf(inferred.types),
2808
+ optional: acceptsUndefined(inferred.types),
2809
+ fields: requestFieldsOf(inferred.types)
2810
+ };
2811
+ }
2812
+ if (inferred.kind === "unresolved") {
2813
+ const fallback = syntacticZodType(arg, project);
2814
+ if (fallback) {
2815
+ return { type: fallback, optional: fallback.endsWith("| undefined"), fields: null };
2816
+ }
2817
+ }
2818
+ }
2819
+ }
2820
+ return null;
2821
+ }
2822
+ function couldBeSchema(arg) {
2823
+ return Node9.isIdentifier(arg) || Node9.isPropertyAccessExpression(arg) || Node9.isCallExpression(arg) || Node9.isAsExpression(arg) || Node9.isSatisfiesExpression(arg) || Node9.isParenthesizedExpression(arg);
2824
+ }
2825
+ function syntacticZodType(arg, project) {
2826
+ let expr = arg;
2827
+ if (Node9.isIdentifier(arg)) {
2828
+ const resolved = resolveImportedVariable(arg.getText(), arg.getSourceFile(), project);
2829
+ expr = resolved?.decl.getInitializer();
2830
+ }
2831
+ if (!expr || !Node9.isCallExpression(expr)) return null;
2832
+ const type = zodAstToTs(expr);
2833
+ return type === "unknown" ? null : type;
2834
+ }
2835
+
2079
2836
  // src/discovery/dto-type-resolver.ts
2080
2837
  var WRAPPER_TYPES = {
2081
2838
  // MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
@@ -2107,22 +2864,22 @@ var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
2107
2864
  ]);
2108
2865
  function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /* @__PURE__ */ new Map()) {
2109
2866
  if (depth <= 0) return "unknown";
2110
- if (Node7.isArrayTypeNode(typeNode)) {
2867
+ if (Node10.isArrayTypeNode(typeNode)) {
2111
2868
  const elementType = typeNode.getElementTypeNode();
2112
2869
  return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth, subst)}>`;
2113
2870
  }
2114
- if (Node7.isUnionTypeNode(typeNode)) {
2871
+ if (Node10.isUnionTypeNode(typeNode)) {
2115
2872
  return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" | ");
2116
2873
  }
2117
- if (Node7.isIntersectionTypeNode(typeNode)) {
2874
+ if (Node10.isIntersectionTypeNode(typeNode)) {
2118
2875
  return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth, subst)).join(" & ");
2119
2876
  }
2120
- if (Node7.isParenthesizedTypeNode(typeNode)) {
2877
+ if (Node10.isParenthesizedTypeNode(typeNode)) {
2121
2878
  return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth, subst)})`;
2122
2879
  }
2123
- if (Node7.isTypeReference(typeNode)) {
2880
+ if (Node10.isTypeReference(typeNode)) {
2124
2881
  const typeName = typeNode.getTypeName();
2125
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2882
+ const name = Node10.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
2126
2883
  const bound = subst.get(name);
2127
2884
  if (bound !== void 0) return bound;
2128
2885
  if (name === "string" || name === "number" || name === "boolean") return name;
@@ -2145,13 +2902,13 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /
2145
2902
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
2146
2903
  return "unknown";
2147
2904
  }
2148
- if (Node7.isTypeLiteral(typeNode)) {
2905
+ if (Node10.isTypeLiteral(typeNode)) {
2149
2906
  const members = [];
2150
2907
  for (const member of typeNode.getMembers()) {
2151
- if (Node7.isPropertySignature(member)) {
2908
+ if (Node10.isPropertySignature(member)) {
2152
2909
  const memberTypeNode = member.getTypeNode();
2153
- const memberType = memberTypeNode ? resolveTypeNodeToString(memberTypeNode, sourceFile, project, depth, subst) : "unknown";
2154
- members.push(`${member.getName()}${member.hasQuestionToken() ? "?" : ""}: ${memberType}`);
2910
+ const memberType2 = memberTypeNode ? resolveTypeNodeToString(memberTypeNode, sourceFile, project, depth, subst) : "unknown";
2911
+ members.push(`${member.getName()}${member.hasQuestionToken() ? "?" : ""}: ${memberType2}`);
2155
2912
  } else {
2156
2913
  members.push(member.getText());
2157
2914
  }
@@ -2159,11 +2916,11 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /
2159
2916
  return members.length > 0 ? `{ ${members.join("; ")} }` : "{}";
2160
2917
  }
2161
2918
  const kind = typeNode.getKind();
2162
- if (kind === SyntaxKind3.StringKeyword) return "string";
2163
- if (kind === SyntaxKind3.NumberKeyword) return "number";
2164
- if (kind === SyntaxKind3.BooleanKeyword) return "boolean";
2165
- if (kind === SyntaxKind3.UnknownKeyword) return "unknown";
2166
- if (kind === SyntaxKind3.AnyKeyword) return "unknown";
2919
+ if (kind === SyntaxKind4.StringKeyword) return "string";
2920
+ if (kind === SyntaxKind4.NumberKeyword) return "number";
2921
+ if (kind === SyntaxKind4.BooleanKeyword) return "boolean";
2922
+ if (kind === SyntaxKind4.UnknownKeyword) return "unknown";
2923
+ if (kind === SyntaxKind4.AnyKeyword) return "unknown";
2167
2924
  return typeNode.getText();
2168
2925
  }
2169
2926
  function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode, subst = /* @__PURE__ */ new Map()) {
@@ -2211,20 +2968,24 @@ function resolvePropertied(decl, sourceFile, project, depth, subst = /* @__PURE_
2211
2968
  const propName = prop.getName();
2212
2969
  const isOptional = prop.hasQuestionToken();
2213
2970
  const propTypeNode = prop.getTypeNode();
2214
- let propType = "unknown";
2971
+ let propType2 = "unknown";
2215
2972
  if (propTypeNode) {
2216
- propType = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2973
+ propType2 = resolveTypeNodeToString(propTypeNode, sourceFile, project, depth, subst);
2217
2974
  }
2218
- lines.push(`${propName}${isOptional ? "?" : ""}: ${propType}`);
2975
+ lines.push(`${propName}${isOptional ? "?" : ""}: ${propType2}`);
2219
2976
  }
2220
2977
  return `{ ${lines.join("; ")} }`;
2221
2978
  }
2979
+ function namedField(decorator) {
2980
+ const first = decorator.getArguments()[0];
2981
+ return first && Node10.isStringLiteral(first) ? first.getLiteralValue() : null;
2982
+ }
2222
2983
  function extractBodyType(method, sourceFile, project) {
2223
2984
  for (const param of method.getParameters()) {
2224
2985
  const bodyDecorator = param.getDecorators().find((d) => d.getName() === "Body");
2225
- if (!bodyDecorator) continue;
2226
- const bodyArgs = bodyDecorator.getArguments();
2227
- if (bodyArgs.length > 0) continue;
2986
+ if (!bodyDecorator || namedField(bodyDecorator) !== null) continue;
2987
+ const piped = pipeSchemaOf(bodyDecorator, project);
2988
+ if (piped) return piped.type;
2228
2989
  const typeNode = param.getTypeNode();
2229
2990
  if (typeNode) {
2230
2991
  return resolveTypeNodeToString(typeNode, sourceFile, project, 3);
@@ -2232,11 +2993,16 @@ function extractBodyType(method, sourceFile, project) {
2232
2993
  }
2233
2994
  return null;
2234
2995
  }
2996
+ function hasPipeSchema(param, decoratorName, project) {
2997
+ const decorator = param.getDecorators().find((d) => d.getName() === decoratorName);
2998
+ return !!decorator && pipeSchemaOf(decorator, project) !== null;
2999
+ }
2235
3000
  function extractQueryType(method, sourceFile, project) {
2236
3001
  for (const param of method.getParameters()) {
2237
3002
  const queryDecorator = param.getDecorators().find((d) => d.getName() === "Query");
2238
- if (!queryDecorator) continue;
2239
- if (queryDecorator.getArguments().length > 0) continue;
3003
+ if (!queryDecorator || namedField(queryDecorator) !== null) continue;
3004
+ const piped = pipeSchemaOf(queryDecorator, project);
3005
+ if (piped) return piped.type;
2240
3006
  const typeNode = param.getTypeNode();
2241
3007
  if (typeNode) {
2242
3008
  return resolveTypeNodeToString(typeNode, sourceFile, project, 3);
@@ -2246,10 +3012,13 @@ function extractQueryType(method, sourceFile, project) {
2246
3012
  for (const param of method.getParameters()) {
2247
3013
  const queryDecorator = param.getDecorators().find((d) => d.getName() === "Query");
2248
3014
  if (!queryDecorator) continue;
2249
- const queryArgs = queryDecorator.getArguments();
2250
- const nameArg = queryArgs[0];
2251
- if (!nameArg || !Node7.isStringLiteral(nameArg)) continue;
2252
- const queryName = nameArg.getLiteralValue();
3015
+ const queryName = namedField(queryDecorator);
3016
+ if (queryName === null) continue;
3017
+ const piped = pipeSchemaOf(queryDecorator, project);
3018
+ if (piped) {
3019
+ entries.push(`${queryName}${piped.optional ? "?" : ""}: ${piped.type}`);
3020
+ continue;
3021
+ }
2253
3022
  const typeNode = param.getTypeNode();
2254
3023
  const queryType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
2255
3024
  entries.push(`${queryName}${isParamOptional(param) ? "?" : ""}: ${queryType}`);
@@ -2259,8 +3028,8 @@ function extractQueryType(method, sourceFile, project) {
2259
3028
  function isParamOptional(param) {
2260
3029
  if (param.hasQuestionToken() || param.hasInitializer()) return true;
2261
3030
  const typeNode = param.getTypeNode();
2262
- if (typeNode && Node7.isUnionTypeNode(typeNode)) {
2263
- return typeNode.getTypeNodes().some((t) => t.getKind() === SyntaxKind3.UndefinedKeyword);
3031
+ if (typeNode && Node10.isUnionTypeNode(typeNode)) {
3032
+ return typeNode.getTypeNodes().some((t) => t.getKind() === SyntaxKind4.UndefinedKeyword);
2264
3033
  }
2265
3034
  return false;
2266
3035
  }
@@ -2272,7 +3041,7 @@ function extractParamsType(method, sourceFile, project) {
2272
3041
  const paramArgs = paramDecorator.getArguments();
2273
3042
  if (paramArgs.length === 0) continue;
2274
3043
  const nameArg = paramArgs[0];
2275
- if (!Node7.isStringLiteral(nameArg)) continue;
3044
+ if (!Node10.isStringLiteral(nameArg)) continue;
2276
3045
  const paramName = nameArg.getLiteralValue();
2277
3046
  const typeNode = param.getTypeNode();
2278
3047
  const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
@@ -2280,6 +3049,22 @@ function extractParamsType(method, sourceFile, project) {
2280
3049
  }
2281
3050
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
2282
3051
  }
3052
+ function extractPipedParamTypes(method, project) {
3053
+ const types = {};
3054
+ for (const param of method.getParameters()) {
3055
+ const paramDecorator = param.getDecorators().find((d) => d.getName() === "Param");
3056
+ if (!paramDecorator) continue;
3057
+ const piped = pipeSchemaOf(paramDecorator, project);
3058
+ if (!piped) continue;
3059
+ const name = namedField(paramDecorator);
3060
+ if (name !== null) {
3061
+ types[name] = piped.type;
3062
+ } else {
3063
+ for (const field of piped.fields ?? []) types[field.name] = field.type;
3064
+ }
3065
+ }
3066
+ return Object.keys(types).length > 0 ? types : null;
3067
+ }
2283
3068
  function extractUploadedFiles(method) {
2284
3069
  const FILE = "File | Blob";
2285
3070
  const entries = [];
@@ -2293,28 +3078,28 @@ function extractUploadedFiles(method) {
2293
3078
  for (const decorator of method.getDecorators()) {
2294
3079
  if (decorator.getName() !== "UseInterceptors") continue;
2295
3080
  for (const arg of decorator.getArguments()) {
2296
- if (!Node7.isCallExpression(arg)) continue;
3081
+ if (!Node10.isCallExpression(arg)) continue;
2297
3082
  const interceptor = arg.getExpression().getText();
2298
3083
  const callArgs = arg.getArguments();
2299
3084
  const firstArg2 = callArgs[0];
2300
3085
  if (interceptor === "FileInterceptor") {
2301
- if (firstArg2 && Node7.isStringLiteral(firstArg2)) {
3086
+ if (firstArg2 && Node10.isStringLiteral(firstArg2)) {
2302
3087
  entries.push(`${firstArg2.getLiteralValue()}: ${FILE}`);
2303
3088
  multipart = true;
2304
3089
  }
2305
3090
  } else if (interceptor === "FilesInterceptor") {
2306
- if (firstArg2 && Node7.isStringLiteral(firstArg2)) {
3091
+ if (firstArg2 && Node10.isStringLiteral(firstArg2)) {
2307
3092
  entries.push(`${firstArg2.getLiteralValue()}: Array<${FILE}>`);
2308
3093
  multipart = true;
2309
3094
  }
2310
3095
  } else if (interceptor === "FileFieldsInterceptor") {
2311
- if (firstArg2 && Node7.isArrayLiteralExpression(firstArg2)) {
3096
+ if (firstArg2 && Node10.isArrayLiteralExpression(firstArg2)) {
2312
3097
  for (const el of firstArg2.getElements()) {
2313
- if (!Node7.isObjectLiteralExpression(el)) continue;
3098
+ if (!Node10.isObjectLiteralExpression(el)) continue;
2314
3099
  const nameProp = el.getProperty("name");
2315
- if (nameProp && Node7.isPropertyAssignment(nameProp)) {
3100
+ if (nameProp && Node10.isPropertyAssignment(nameProp)) {
2316
3101
  const init = nameProp.getInitializer();
2317
- if (init && Node7.isStringLiteral(init)) {
3102
+ if (init && Node10.isStringLiteral(init)) {
2318
3103
  entries.push(`${init.getLiteralValue()}: Array<${FILE}>`);
2319
3104
  }
2320
3105
  }
@@ -2334,13 +3119,13 @@ function extractResponseType(method, sourceFile, project) {
2334
3119
  if (apiResponseDecorator) {
2335
3120
  const args = apiResponseDecorator.getArguments();
2336
3121
  const optsArg = args[0];
2337
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
3122
+ if (optsArg && Node10.isObjectLiteralExpression(optsArg)) {
2338
3123
  for (const prop of optsArg.getProperties()) {
2339
- if (!Node7.isPropertyAssignment(prop)) continue;
3124
+ if (!Node10.isPropertyAssignment(prop)) continue;
2340
3125
  if (prop.getName() !== "type") continue;
2341
3126
  const val = prop.getInitializer();
2342
3127
  if (!val) continue;
2343
- if (Node7.isArrayLiteralExpression(val)) {
3128
+ if (Node10.isArrayLiteralExpression(val)) {
2344
3129
  const elements = val.getElements();
2345
3130
  const firstEl = elements[0];
2346
3131
  if (elements.length > 0 && firstEl !== void 0) {
@@ -2361,24 +3146,24 @@ function extractResponseType(method, sourceFile, project) {
2361
3146
  }
2362
3147
  function apiResponseStatus(decorator) {
2363
3148
  const optsArg = decorator.getArguments()[0];
2364
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3149
+ if (!optsArg || !Node10.isObjectLiteralExpression(optsArg)) return null;
2365
3150
  for (const prop of optsArg.getProperties()) {
2366
- if (!Node7.isPropertyAssignment(prop)) continue;
3151
+ if (!Node10.isPropertyAssignment(prop)) continue;
2367
3152
  if (prop.getName() !== "status") continue;
2368
3153
  const val = prop.getInitializer();
2369
- if (val && Node7.isNumericLiteral(val)) return Number(val.getLiteralValue());
3154
+ if (val && Node10.isNumericLiteral(val)) return Number(val.getLiteralValue());
2370
3155
  }
2371
3156
  return null;
2372
3157
  }
2373
3158
  function apiResponseTypeNode(decorator) {
2374
3159
  const optsArg = decorator.getArguments()[0];
2375
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
3160
+ if (!optsArg || !Node10.isObjectLiteralExpression(optsArg)) return null;
2376
3161
  for (const prop of optsArg.getProperties()) {
2377
- if (!Node7.isPropertyAssignment(prop)) continue;
3162
+ if (!Node10.isPropertyAssignment(prop)) continue;
2378
3163
  if (prop.getName() !== "type") continue;
2379
3164
  const val = prop.getInitializer();
2380
3165
  if (!val) return null;
2381
- if (Node7.isArrayLiteralExpression(val)) {
3166
+ if (Node10.isArrayLiteralExpression(val)) {
2382
3167
  const first = val.getElements()[0];
2383
3168
  return first ? { node: first, isArray: true } : null;
2384
3169
  }
@@ -2396,7 +3181,7 @@ function extractErrorType(method, sourceFile, project) {
2396
3181
  const inner = resolveIdentifierToClassType(typeInfo.node, sourceFile, project, 3);
2397
3182
  const type = typeInfo.isArray ? `Array<${inner}>` : inner;
2398
3183
  let ref = null;
2399
- if (Node7.isIdentifier(typeInfo.node)) {
3184
+ if (Node10.isIdentifier(typeInfo.node)) {
2400
3185
  const name = typeInfo.node.getText();
2401
3186
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
2402
3187
  if (localDecl?.isExported()) {
@@ -2413,7 +3198,7 @@ function extractErrorType(method, sourceFile, project) {
2413
3198
  return null;
2414
3199
  }
2415
3200
  function resolveIdentifierToClassType(node, sourceFile, project, depth) {
2416
- if (!Node7.isIdentifier(node)) return "unknown";
3201
+ if (!Node10.isIdentifier(node)) return "unknown";
2417
3202
  const name = node.getText();
2418
3203
  const resolved = findType(name, sourceFile, project);
2419
3204
  if (resolved) {
@@ -2433,9 +3218,9 @@ var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLik
2433
3218
  var BINARY_RESPONSE_TYPES = /* @__PURE__ */ new Set(["StreamableFile", "Buffer"]);
2434
3219
  function detectBinaryResponse(method) {
2435
3220
  const node = unwrapNamedContainer(method.getReturnTypeNode(), /* @__PURE__ */ new Set(["Promise"]));
2436
- if (!node || !Node7.isTypeReference(node)) return false;
3221
+ if (!node || !Node10.isTypeReference(node)) return false;
2437
3222
  const typeName = node.getTypeName();
2438
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : "";
3223
+ const name = Node10.isIdentifier(typeName) ? typeName.getText() : "";
2439
3224
  return BINARY_RESPONSE_TYPES.has(name);
2440
3225
  }
2441
3226
  function detectStreamElement(method) {
@@ -2450,9 +3235,9 @@ function detectStreamElement(method) {
2450
3235
  return null;
2451
3236
  }
2452
3237
  function streamContainerElement(node) {
2453
- if (!node || !Node7.isTypeReference(node)) return null;
3238
+ if (!node || !Node10.isTypeReference(node)) return null;
2454
3239
  const typeName = node.getTypeName();
2455
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : "";
3240
+ const name = Node10.isIdentifier(typeName) ? typeName.getText() : "";
2456
3241
  if (STREAM_CONTAINERS.has(name) || STREAM_CONTAINERS_GENERATOR.has(name)) {
2457
3242
  return node.getTypeArguments()[0] ?? null;
2458
3243
  }
@@ -2462,9 +3247,9 @@ function hasAsQueryDecorator(method) {
2462
3247
  return method.getDecorators().some((d) => d.getName() === "AsQuery");
2463
3248
  }
2464
3249
  function unwrapNamedContainer(node, names) {
2465
- if (!node || !Node7.isTypeReference(node)) return node;
3250
+ if (!node || !Node10.isTypeReference(node)) return node;
2466
3251
  const typeName = node.getTypeName();
2467
- const name = Node7.isIdentifier(typeName) ? typeName.getText() : "";
3252
+ const name = Node10.isIdentifier(typeName) ? typeName.getText() : "";
2468
3253
  if (names.has(name)) {
2469
3254
  return node.getTypeArguments()[0] ?? node;
2470
3255
  }
@@ -2485,19 +3270,20 @@ function extractDtoContract(method, sourceFile, project, mixin, controllerClass)
2485
3270
  body = body ?? bodyType;
2486
3271
  }
2487
3272
  const paramsType = extractParamsType(method, sourceFile, project);
3273
+ const paramTypes = extractPipedParamTypes(method, project);
2488
3274
  const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
2489
3275
  const errorInfo = extractErrorType(method, sourceFile, project);
2490
- if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart && !binaryResponse && !asQuery) {
3276
+ if (body === null && query === null && paramsType === null && paramTypes === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart && !binaryResponse && !asQuery) {
2491
3277
  return null;
2492
3278
  }
2493
3279
  let bodyRef = null;
2494
3280
  let queryRef = null;
2495
3281
  let responseRef = null;
2496
3282
  for (const param of method.getParameters()) {
2497
- if (param.getDecorators().some((d) => d.getName() === "Body") && param.getTypeNode()) {
3283
+ if (param.getDecorators().some((d) => d.getName() === "Body") && param.getTypeNode() && !hasPipeSchema(param, "Body", project)) {
2498
3284
  bodyRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2499
3285
  }
2500
- if (param.getDecorators().some((d) => d.getName() === "Query") && param.getTypeNode()) {
3286
+ if (param.getDecorators().some((d) => d.getName() === "Query") && param.getTypeNode() && !hasPipeSchema(param, "Query", project)) {
2501
3287
  queryRef = resolveBodyQueryResponseRef(param.getTypeNode(), sourceFile, project);
2502
3288
  }
2503
3289
  }
@@ -2510,11 +3296,11 @@ function extractDtoContract(method, sourceFile, project, mixin, controllerClass)
2510
3296
  if (apiResp) {
2511
3297
  const args = apiResp.getArguments();
2512
3298
  const optsArg = args[0];
2513
- if (optsArg && Node7.isObjectLiteralExpression(optsArg)) {
3299
+ if (optsArg && Node10.isObjectLiteralExpression(optsArg)) {
2514
3300
  for (const prop of optsArg.getProperties()) {
2515
- if (Node7.isPropertyAssignment(prop) && prop.getName() === "type") {
3301
+ if (Node10.isPropertyAssignment(prop) && prop.getName() === "type") {
2516
3302
  const val = prop.getInitializer();
2517
- if (val && Node7.isIdentifier(val)) {
3303
+ if (val && Node10.isIdentifier(val)) {
2518
3304
  const name = val.getText();
2519
3305
  const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
2520
3306
  if (localDecl?.isExported()) {
@@ -2550,6 +3336,7 @@ function extractDtoContract(method, sourceFile, project, mixin, controllerClass)
2550
3336
  response,
2551
3337
  error: errorInfo?.type ?? null,
2552
3338
  params: paramsType,
3339
+ paramTypes,
2553
3340
  queryRef,
2554
3341
  bodyRef,
2555
3342
  responseRef,
@@ -2570,6 +3357,7 @@ function extractDtoContract(method, sourceFile, project, mixin, controllerClass)
2570
3357
  function resolveParamClass(method, decoratorName, sourceFile, project) {
2571
3358
  for (const param of method.getParameters()) {
2572
3359
  if (!param.getDecorators().some((d) => d.getName() === decoratorName)) continue;
3360
+ if (hasPipeSchema(param, decoratorName, project)) continue;
2573
3361
  const typeNode = param.getTypeNode();
2574
3362
  if (!typeNode) continue;
2575
3363
  const text = typeNode.getText().replace(/\[\]$/, "");
@@ -2582,129 +3370,6 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
2582
3370
  return null;
2583
3371
  }
2584
3372
 
2585
- // src/discovery/zod-ast-to-ts.ts
2586
- import { Node as Node8, SyntaxKind as SyntaxKind4 } from "ts-morph";
2587
- function zodAstToTs(node) {
2588
- if (!Node8.isCallExpression(node)) return "unknown";
2589
- const expr = node.getExpression();
2590
- if (Node8.isPropertyAccessExpression(expr)) {
2591
- const methodName = expr.getName();
2592
- const receiver = expr.getExpression();
2593
- if (methodName === "optional") {
2594
- return `${zodAstToTs(receiver)} | undefined`;
2595
- }
2596
- if (methodName === "nullable") {
2597
- return `${zodAstToTs(receiver)} | null`;
2598
- }
2599
- const args = node.getArguments();
2600
- switch (methodName) {
2601
- case "string":
2602
- return "string";
2603
- case "number":
2604
- return "number";
2605
- case "boolean":
2606
- return "boolean";
2607
- case "unknown":
2608
- return "unknown";
2609
- case "any":
2610
- return "unknown";
2611
- case "literal": {
2612
- const lit = args[0];
2613
- if (!lit) return "unknown";
2614
- if (Node8.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
2615
- if (Node8.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
2616
- if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
2617
- if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
2618
- return "unknown";
2619
- }
2620
- case "enum": {
2621
- const arrArg = args[0];
2622
- if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2623
- const members = arrArg.getElements().map(
2624
- (el) => Node8.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
2625
- );
2626
- return members.join(" | ");
2627
- }
2628
- case "array": {
2629
- const inner = args[0];
2630
- if (!inner) return "unknown";
2631
- return `Array<${zodAstToTs(inner)}>`;
2632
- }
2633
- case "object": {
2634
- const objArg = args[0];
2635
- if (!objArg || !Node8.isObjectLiteralExpression(objArg)) return "unknown";
2636
- const lines = [];
2637
- for (const prop of objArg.getProperties()) {
2638
- if (!Node8.isPropertyAssignment(prop)) continue;
2639
- const key = prop.getName();
2640
- const valNode = prop.getInitializer();
2641
- if (!valNode) continue;
2642
- const tsType = zodAstToTs(valNode);
2643
- const isOpt = isOptionalChain(valNode);
2644
- lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
2645
- }
2646
- return `{ ${lines.join("; ")} }`;
2647
- }
2648
- case "union": {
2649
- const arrArg = args[0];
2650
- if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2651
- return arrArg.getElements().map(zodAstToTs).join(" | ");
2652
- }
2653
- case "record": {
2654
- const valArg = args.length === 1 ? args[0] : args[1];
2655
- if (!valArg) return "unknown";
2656
- return `Record<string, ${zodAstToTs(valArg)}>`;
2657
- }
2658
- case "tuple": {
2659
- const arrArg = args[0];
2660
- if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
2661
- return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
2662
- }
2663
- default:
2664
- return "unknown";
2665
- }
2666
- }
2667
- return "unknown";
2668
- }
2669
- function isOptionalChain(node) {
2670
- if (!Node8.isCallExpression(node)) return false;
2671
- const expr = node.getExpression();
2672
- return Node8.isPropertyAccessExpression(expr) && expr.getName() === "optional";
2673
- }
2674
- function parseDefineContractCall(callExpr) {
2675
- if (!Node8.isCallExpression(callExpr)) return null;
2676
- const callee = callExpr.getExpression();
2677
- const calleeName = Node8.isIdentifier(callee) ? callee.getText() : Node8.isPropertyAccessExpression(callee) ? callee.getName() : "";
2678
- if (calleeName !== "defineContract") return null;
2679
- const args = callExpr.getArguments();
2680
- const optsArg = args[0];
2681
- if (!optsArg || !Node8.isObjectLiteralExpression(optsArg)) return null;
2682
- let query = null;
2683
- let body = null;
2684
- let response = "unknown";
2685
- let error = null;
2686
- let bodyZodText = null;
2687
- let queryZodText = null;
2688
- for (const prop of optsArg.getProperties()) {
2689
- if (!Node8.isPropertyAssignment(prop)) continue;
2690
- const propName = prop.getName();
2691
- const val = prop.getInitializer();
2692
- if (!val) continue;
2693
- if (propName === "query") {
2694
- query = zodAstToTs(val);
2695
- queryZodText = val.getText();
2696
- } else if (propName === "body") {
2697
- body = zodAstToTs(val);
2698
- bodyZodText = val.getText();
2699
- } else if (propName === "response") {
2700
- response = zodAstToTs(val);
2701
- } else if (propName === "error") {
2702
- error = zodAstToTs(val);
2703
- }
2704
- }
2705
- return { query, body, response, error, bodyZodText, queryZodText };
2706
- }
2707
-
2708
3373
  // src/discovery/contracts-fast.ts
2709
3374
  async function discoverContractsFast(opts) {
2710
3375
  const { cwd, glob, tsconfig } = opts;
@@ -2717,7 +3382,9 @@ async function discoverContractsFast(opts) {
2717
3382
  }
2718
3383
  bindDiscoveryContext(project, cwd, tsconfigPath, loaded);
2719
3384
  clearMixinTypeProject();
2720
- return extractAllRoutes(project);
3385
+ const routes = extractAllRoutes(project);
3386
+ clearSchemaTypeProject(project);
3387
+ return routes;
2721
3388
  }
2722
3389
  function resolveTsconfigPath(cwd, tsconfig) {
2723
3390
  return tsconfig ? resolve3(tsconfig) : join4(cwd, "tsconfig.json");
@@ -2729,25 +3396,25 @@ var NO_TSCONFIG_OPTIONS = {
2729
3396
  };
2730
3397
  function loadDiscoveryTsconfig(tsconfigPath) {
2731
3398
  if (!existsSync(tsconfigPath)) return { options: null, files: [] };
2732
- const read = ts.readConfigFile(tsconfigPath, (path) => ts.sys.readFile(path));
3399
+ const read = ts3.readConfigFile(tsconfigPath, (path) => ts3.sys.readFile(path));
2733
3400
  if (read.error) {
2734
3401
  return {
2735
3402
  options: null,
2736
3403
  files: [tsconfigPath],
2737
- error: ts.flattenDiagnosticMessageText(read.error.messageText, " ")
3404
+ error: ts3.flattenDiagnosticMessageText(read.error.messageText, " ")
2738
3405
  };
2739
3406
  }
2740
3407
  const host = {
2741
- useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
3408
+ useCaseSensitiveFileNames: ts3.sys.useCaseSensitiveFileNames,
2742
3409
  // The one line that keeps a tsconfig from being a filesystem walk.
2743
3410
  readDirectory: () => [],
2744
- fileExists: (path) => ts.sys.fileExists(path),
3411
+ fileExists: (path) => ts3.sys.fileExists(path),
2745
3412
  // `extends` is resolved through the host, so a consumer whose `paths` live
2746
3413
  // in a base tsconfig keeps working.
2747
- readFile: (path) => ts.sys.readFile(path)
3414
+ readFile: (path) => ts3.sys.readFile(path)
2748
3415
  };
2749
- const sourceFile = ts.readJsonConfigFile(tsconfigPath, (path) => ts.sys.readFile(path));
2750
- const parsed = ts.parseJsonSourceFileConfigFileContent(
3416
+ const sourceFile = ts3.readJsonConfigFile(tsconfigPath, (path) => ts3.sys.readFile(path));
3417
+ const parsed = ts3.parseJsonSourceFileConfigFileContent(
2751
3418
  sourceFile,
2752
3419
  host,
2753
3420
  dirname2(tsconfigPath),
@@ -2770,7 +3437,7 @@ function createDiscoveryProject(tsconfigPath, tsconfig = loadDiscoveryTsconfig(t
2770
3437
  `[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.`
2771
3438
  );
2772
3439
  }
2773
- return new Project3({
3440
+ return new Project4({
2774
3441
  compilerOptions: tsconfig.options ?? { ...NO_TSCONFIG_OPTIONS },
2775
3442
  skipAddingFilesFromTsConfig: true,
2776
3443
  skipLoadingLibFiles: true,
@@ -2882,15 +3549,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
2882
3549
  clearTypeResolutionCaches(this.project);
2883
3550
  clearEnumCache(this.project);
2884
3551
  clearMixinTypeProject();
3552
+ clearSchemaTypeProject(this.project);
2885
3553
  return extractRoutesFrom(this.project, this.controllerPaths);
2886
3554
  }
2887
3555
  };
2888
3556
  function decoratorStringArg(decoratorExpr) {
2889
3557
  if (!decoratorExpr) return void 0;
2890
- if (Node9.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
2891
- if (Node9.isArrayLiteralExpression(decoratorExpr)) {
3558
+ if (Node11.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
3559
+ if (Node11.isArrayLiteralExpression(decoratorExpr)) {
2892
3560
  const first = decoratorExpr.getElements()[0];
2893
- if (first && Node9.isStringLiteral(first)) return first.getLiteralValue();
3561
+ if (first && Node11.isStringLiteral(first)) return first.getLiteralValue();
2894
3562
  }
2895
3563
  return void 0;
2896
3564
  }
@@ -3011,9 +3679,9 @@ function extractContractRoute(args) {
3011
3679
  let contractDef = null;
3012
3680
  let bodyZodRef = null;
3013
3681
  let queryZodRef = null;
3014
- if (Node9.isCallExpression(firstDecoratorArg)) {
3015
- contractDef = parseDefineContractCall(firstDecoratorArg);
3016
- } else if (Node9.isIdentifier(firstDecoratorArg)) {
3682
+ if (Node11.isCallExpression(firstDecoratorArg)) {
3683
+ contractDef = parseDefineContractCall(firstDecoratorArg, project);
3684
+ } else if (Node11.isIdentifier(firstDecoratorArg)) {
3017
3685
  const identName = firstDecoratorArg.getText();
3018
3686
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
3019
3687
  if (!resolvedVar) {
@@ -3025,7 +3693,7 @@ function extractContractRoute(args) {
3025
3693
  const { decl: varDecl, file: declFile } = resolvedVar;
3026
3694
  const initializer = varDecl.getInitializer();
3027
3695
  if (!initializer) return null;
3028
- contractDef = parseDefineContractCall(initializer);
3696
+ contractDef = parseDefineContractCall(initializer, project);
3029
3697
  if (contractDef && varDecl.isExported()) {
3030
3698
  const filePath = declFile.getFilePath();
3031
3699
  if (contractDef.body !== null) {
@@ -3096,6 +3764,7 @@ function extractDtoRoute(args) {
3096
3764
  body: dtoContract?.body ?? null,
3097
3765
  response: mixinResponse ?? dtoContract?.response ?? "unknown",
3098
3766
  error: dtoContract?.error ?? null,
3767
+ paramTypes: dtoContract?.paramTypes ?? null,
3099
3768
  queryRef: dtoContract?.queryRef ?? null,
3100
3769
  bodyRef: dtoContract?.bodyRef ?? null,
3101
3770
  responseRef: dtoContract?.responseRef ?? null,
@@ -3218,7 +3887,7 @@ function createExtensionContext(config, getRoutes, trackedInputs) {
3218
3887
  },
3219
3888
  project() {
3220
3889
  if (!project) {
3221
- project = new Project4({
3890
+ project = new Project5({
3222
3891
  skipAddingFilesFromTsConfig: true,
3223
3892
  skipLoadingLibFiles: true,
3224
3893
  skipFileDependencyResolution: true,
@@ -3336,10 +4005,11 @@ function insertIntoTree(tree, segments, leaf, fullName) {
3336
4005
  insertIntoTree(branch.children, rest, leaf, fullName);
3337
4006
  }
3338
4007
  }
3339
- function buildParamsType(params) {
4008
+ function buildParamsType(params, paramTypes) {
3340
4009
  const pathParams = params.filter((p) => p.source === "path");
3341
4010
  if (pathParams.length === 0) return "never";
3342
- return `{ ${pathParams.map((p) => `${p.name}: string`).join("; ")} }`;
4011
+ const typeOf = (name) => paramTypes && Object.hasOwn(paramTypes, name) && paramTypes[name] || "string";
4012
+ return `{ ${pathParams.map((p) => `${p.name}: ${typeOf(p.name)}`).join("; ")} }`;
3343
4013
  }
3344
4014
  function hasPathParams(params) {
3345
4015
  return params.some((p) => p.source === "path");
@@ -3433,7 +4103,7 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
3433
4103
  }
3434
4104
  const response = buildResponseType(c, outDir, serialization);
3435
4105
  const error = buildErrorType(c);
3436
- const params = buildParamsType(c.params);
4106
+ const params = buildParamsType(c.params, c.contractSource.paramTypes);
3437
4107
  const safeMethod = JSON.stringify(method);
3438
4108
  const safeUrl = JSON.stringify(c.path);
3439
4109
  const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
@@ -3521,28 +4191,28 @@ function renderFetcherRequest(req, binaryResponse) {
3521
4191
  }
3522
4192
  function emitReqHelper() {
3523
4193
  return [
3524
- "/** Awaitable request handle. `await api.x.y({...})` runs the fetch; extensions add query/mutation helpers. */",
3525
- "type __Req<R> = {",
3526
- " then<T1 = R, T2 = never>(",
4194
+ "/** An awaitable request handle: a lazy, memoized `Promise` that also carries `fetch()` and the members extensions add. */",
4195
+ "export type RequestHandle<R, M = {}> = Promise<R> & { fetch(): Promise<R> } & M;",
4196
+ "class __Request<R> extends Promise<R> {",
4197
+ " static override get [Symbol.species](): PromiseConstructor {",
4198
+ " return Promise;",
4199
+ " }",
4200
+ " private __run: () => Promise<R>;",
4201
+ " private __p: Promise<R> | undefined;",
4202
+ " constructor(run: () => Promise<R>) {",
4203
+ " super(() => {});",
4204
+ " this.__run = run;",
4205
+ " }",
4206
+ " override then<T1 = R, T2 = never>(",
3527
4207
  " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
3528
4208
  " onrejected?: ((reason: unknown) => T2 | PromiseLike<T2>) | null,",
3529
- " ): Promise<T1 | T2>;",
3530
- " catch<T = never>(onrejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<R | T>;",
3531
- " finally(onfinally?: (() => void) | null): Promise<R>;",
3532
- " fetch(): Promise<R>;",
3533
- "};",
3534
- "function __req<R>(run: () => Promise<R>): __Req<R> {",
3535
- " let __p: Promise<R> | undefined;",
3536
- " const __promise = () => {",
3537
- " __p ??= run();",
3538
- " return __p;",
3539
- " };",
3540
- " return {",
3541
- " then: (onfulfilled, onrejected) => __promise().then(onfulfilled, onrejected),",
3542
- " catch: (onrejected) => __promise().catch(onrejected),",
3543
- " finally: (onfinally) => __promise().finally(onfinally),",
3544
- " fetch: run,",
3545
- " };",
4209
+ " ): Promise<T1 | T2> {",
4210
+ " this.__p ??= this.__run();",
4211
+ " return this.__p.then(onfulfilled, onrejected);",
4212
+ " }",
4213
+ "}",
4214
+ "function __req<R, M extends object = {}>(run: () => Promise<R>, members?: M): RequestHandle<R, M> {",
4215
+ " return Object.assign(new __Request<R>(run), { fetch: run }, members) as RequestHandle<R, M>;",
3546
4216
  "}",
3547
4217
  ""
3548
4218
  ];
@@ -3560,18 +4230,26 @@ function emitFilterFieldGuard() {
3560
4230
  ];
3561
4231
  }
3562
4232
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
3563
- const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
3564
- lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
4233
+ const memberLines = [];
3565
4234
  if (req.filterFieldsExpr) {
3566
- lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4235
+ memberLines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
3567
4236
  }
3568
4237
  if (streamExpr) {
3569
- lines.push(`${pad} stream: () => ${streamExpr},`);
4238
+ memberLines.push(`${pad} stream: () => ${streamExpr},`);
3570
4239
  }
3571
4240
  for (const [name, value] of Object.entries(members)) {
3572
- lines.push(`${pad} ${name}: ${value},`);
3573
- }
3574
- lines.push(`${pad}}),`);
4241
+ memberLines.push(`${pad} ${name}: ${value},`);
4242
+ }
4243
+ const body = [requestExpr, ...memberLines].join("\n");
4244
+ const param = /\binput\b/.test(body) ? `input?: ${req.inputType}` : "";
4245
+ const lines = [`${pad}${objKey}: (${param}) =>`];
4246
+ if (memberLines.length === 0) {
4247
+ lines.push(`${pad} __req<${req.responseType}>(() => ${requestExpr}),`);
4248
+ return lines;
4249
+ }
4250
+ lines.push(`${pad} __req(() => ${requestExpr}, {`);
4251
+ lines.push(...memberLines);
4252
+ lines.push(`${pad} }),`);
3575
4253
  return lines;
3576
4254
  }
3577
4255
  function renderStreamExpr(req) {
@@ -3702,6 +4380,26 @@ var EMPTY_PATH_NAMESPACE = [
3702
4380
  "}",
3703
4381
  ""
3704
4382
  ];
4383
+ function isClientCallable(route) {
4384
+ return route.method.toUpperCase() !== "ALL";
4385
+ }
4386
+ var ROUTES_IMPORT_PLACEHOLDER = "/* @@routes-import@@ */";
4387
+ var ROUTES_EXPORTS = [
4388
+ // Called, never mentioned in prose: match the call so a comment saying "route" doesn't count.
4389
+ { name: "route", typeOnly: false },
4390
+ { name: "ROUTES", typeOnly: false },
4391
+ { name: "RouteName", typeOnly: true },
4392
+ { name: "ExtractParams", typeOnly: true },
4393
+ { name: "RouteParams", typeOnly: true }
4394
+ ];
4395
+ function finishRoutesImport(lines) {
4396
+ const body = lines.filter((l) => l !== ROUTES_IMPORT_PLACEHOLDER).join("\n");
4397
+ const used = ROUTES_EXPORTS.filter(
4398
+ ({ name }) => new RegExp(name === "route" ? "\\broute\\(" : `\\b${name}\\b`).test(body)
4399
+ ).map(({ name, typeOnly }) => typeOnly ? `type ${name}` : name);
4400
+ const importLine = used.length ? `import { ${used.join(", ")} } from './routes.js';` : null;
4401
+ return lines.flatMap((l) => l === ROUTES_IMPORT_PLACEHOLDER ? importLine ? [importLine] : [] : [l]).join("\n");
4402
+ }
3705
4403
  function buildApiFile(routes, outDir, opts = {}) {
3706
4404
  const fetcherImportPath = opts.fetcherImportPath;
3707
4405
  const serialization = opts.serialization ?? "json";
@@ -3709,7 +4407,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3709
4407
  const { layer } = resolveApiSlots(extensions);
3710
4408
  const memberExts = extensions.filter((e) => e.apiMembers);
3711
4409
  const headerExts = extensions.filter((e) => e.apiHeader);
3712
- const contracted = routes.filter((r) => r.contract);
4410
+ const contracted = routes.filter((r) => r.contract && isClientCallable(r));
3713
4411
  const ctx = opts.ctx ?? {
3714
4412
  cwd: outDir ?? "",
3715
4413
  outDir: outDir ?? "",
@@ -3761,9 +4459,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3761
4459
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
3762
4460
  }
3763
4461
  lines.push(...extImports);
3764
- lines.push(
3765
- "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
3766
- );
4462
+ lines.push(ROUTES_IMPORT_PLACEHOLDER);
3767
4463
  const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
3768
4464
  lines.push(`import type { Fetcher } from '${runtimeImport}';`);
3769
4465
  if (serialization === "json" && contracted.length > 0) {
@@ -3814,7 +4510,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3814
4510
  lines.push(...statements, "");
3815
4511
  }
3816
4512
  }
3817
- return lines.join("\n");
4513
+ return finishRoutesImport(lines);
3818
4514
  }
3819
4515
  const tree = /* @__PURE__ */ new Map();
3820
4516
  for (const r of contracted) {
@@ -3867,7 +4563,7 @@ function buildApiFile(routes, outDir, opts = {}) {
3867
4563
  lines.push(...statements, "");
3868
4564
  }
3869
4565
  }
3870
- return lines.join("\n");
4566
+ return finishRoutesImport(lines);
3871
4567
  }
3872
4568
 
3873
4569
  // src/emit/emit-cache.ts
@@ -3880,6 +4576,7 @@ async function emitCache(pages, outDir) {
3880
4576
  const s = await stat(p.absolutePath);
3881
4577
  return {
3882
4578
  name: p.name,
4579
+ scope: p.scope ?? "default",
3883
4580
  relativePath: p.relativePath,
3884
4581
  mtime: s.mtime.toISOString()
3885
4582
  };
@@ -4630,6 +5327,7 @@ function buildOpenApiSpec(routes, opts = {}) {
4630
5327
  const paths = {};
4631
5328
  for (const route of routes) {
4632
5329
  if (!route.contract) continue;
5330
+ if (route.method.toUpperCase() === "ALL") continue;
4633
5331
  const oaPath = toOpenApiPath(route.path);
4634
5332
  const method = route.method.toLowerCase();
4635
5333
  let pathItem = paths[oaPath];
@@ -4673,10 +5371,11 @@ async function emitPages(pages, outDir, _options = {}) {
4673
5371
  }).join("\n");
4674
5372
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
4675
5373
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
5374
+ const scopesBlock = buildScopesBlock(pages);
4676
5375
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
4677
5376
 
4678
5377
  export type InertiaPageName = ${pageNameUnion};
4679
- ${propsHelper}
5378
+ ${propsHelper}${scopesBlock}
4680
5379
  declare module '@dudousxd/nestjs-inertia' {
4681
5380
  interface InertiaPages {
4682
5381
  ${augBody}
@@ -4685,6 +5384,31 @@ ${sharedPropsBlock}}
4685
5384
  `;
4686
5385
  await writeFile7(join11(outDir, "pages.d.ts"), content, "utf8");
4687
5386
  }
5387
+ function buildScopesBlock(pages) {
5388
+ const byScope = /* @__PURE__ */ new Map();
5389
+ for (const page of pages) {
5390
+ const scope = page.scope ?? "default";
5391
+ const names = byScope.get(scope) ?? [];
5392
+ names.push(JSON.stringify(page.name));
5393
+ byScope.set(scope, names);
5394
+ }
5395
+ if (![...byScope.keys()].some((scope) => scope !== "default")) return "";
5396
+ if (!byScope.has("default")) byScope.set("default", []);
5397
+ const scopes = ["default", ...[...byScope.keys()].filter((s) => s !== "default")];
5398
+ const body = scopes.map((scope) => {
5399
+ const key = needsQuotes(scope) ? JSON.stringify(scope) : scope;
5400
+ const names = byScope.get(scope) ?? [];
5401
+ return ` ${key}: ${names.length ? names.join(" | ") : "never"};`;
5402
+ }).join("\n");
5403
+ return `
5404
+ /** The page names of each Inertia app: \`default\` (forRoot) and every \`pages.scopes\` entry. */
5405
+ export interface InertiaScopePages {
5406
+ ${body}
5407
+ }
5408
+
5409
+ export type InertiaScopePageName<S extends keyof InertiaScopePages> = InertiaScopePages[S];
5410
+ `;
5411
+ }
4688
5412
  function buildSharedPropsBlock(sharedProps) {
4689
5413
  if (!sharedProps) return "";
4690
5414
  if (sharedProps.isImportRef) {
@@ -4892,8 +5616,7 @@ function diffConfigKeyHashes(previous, current) {
4892
5616
  return [...keys].filter((key) => previous[key] !== current[key]).sort();
4893
5617
  }
4894
5618
  async function discoverInputFiles(config) {
4895
- const globs = [config.contracts.glob, config.forms.watch];
4896
- if (config.pages) globs.push(config.pages.glob);
5619
+ const globs = [config.contracts.glob, config.forms.watch, ...allPageGlobs(config)];
4897
5620
  const cwd = config.codegen.cwd;
4898
5621
  const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
4899
5622
  return [...new Set(matched)].sort();
@@ -5024,12 +5747,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5024
5747
  }
5025
5748
  if (config.pages) {
5026
5749
  const pagesConfig = config.pages;
5027
- const pages = await discoverPages({
5028
- glob: pagesConfig.glob,
5029
- cwd: config.codegen.cwd,
5030
- propsExport: pagesConfig.propsExport,
5031
- componentNameStrategy: pagesConfig.componentNameStrategy
5032
- });
5750
+ const pages = await discoverPageScopes(config);
5033
5751
  const sharedProps = discoverSharedPropsFromConfig(config);
5034
5752
  await emitPages(pages, config.codegen.outDir, {
5035
5753
  propsExport: pagesConfig.propsExport,
@@ -5204,12 +5922,17 @@ async function watch(config, onChange, options = {}) {
5204
5922
  await runInitialPass();
5205
5923
  }
5206
5924
  let pagesDebounceTimer;
5207
- const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
5208
- const pagesWatcher = chokidar.watch(join16(config.codegen.cwd, pagesGlob), {
5209
- ignoreInitial: true,
5210
- persistent: true,
5211
- awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5212
- });
5925
+ const pageGlobs = allPageGlobs(config);
5926
+ const pagesWatcher = chokidar.watch(
5927
+ (pageGlobs.length ? pageGlobs : [".nestjs-codegen-no-pages"]).map(
5928
+ (glob) => join16(config.codegen.cwd, glob)
5929
+ ),
5930
+ {
5931
+ ignoreInitial: true,
5932
+ persistent: true,
5933
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5934
+ }
5935
+ );
5213
5936
  function schedulePagesRegenerate() {
5214
5937
  if (pagesDebounceTimer !== void 0) {
5215
5938
  clearTimeout(pagesDebounceTimer);
@@ -5345,7 +6068,7 @@ function toObjectKey2(name) {
5345
6068
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
5346
6069
  }
5347
6070
  function createChainModuleRenderer(opts) {
5348
- const { render, recursiveAnnotation } = opts;
6071
+ const { render: render2, recursiveAnnotation } = opts;
5349
6072
  return (mod) => {
5350
6073
  const ctx = { named: mod.named };
5351
6074
  const recursive = mod.recursive ?? /* @__PURE__ */ new Set();
@@ -5354,7 +6077,7 @@ function createChainModuleRenderer(opts) {
5354
6077
  const namedTypeAliases = /* @__PURE__ */ new Map();
5355
6078
  const namedAnnotations = /* @__PURE__ */ new Map();
5356
6079
  for (const [name, node] of mod.named) {
5357
- namedNestedSchemas.set(name, render(node, ctx));
6080
+ namedNestedSchemas.set(name, render2(node, ctx));
5358
6081
  if (recursive.has(name)) {
5359
6082
  const typeName = typeNameFor(name);
5360
6083
  namedTypeAliases.set(name, `type ${typeName} = ${renderTsType(node, tctx)}`);
@@ -5362,7 +6085,7 @@ function createChainModuleRenderer(opts) {
5362
6085
  }
5363
6086
  }
5364
6087
  return {
5365
- schemaText: render(mod.root, ctx),
6088
+ schemaText: render2(mod.root, ctx),
5366
6089
  namedNestedSchemas,
5367
6090
  namedTypeAliases,
5368
6091
  namedAnnotations,
@@ -5372,7 +6095,7 @@ function createChainModuleRenderer(opts) {
5372
6095
  }
5373
6096
 
5374
6097
  // src/index.ts
5375
- var VERSION = "0.25.2";
6098
+ var VERSION = "0.27.0";
5376
6099
  export {
5377
6100
  CodegenError,
5378
6101
  ConfigError,