@dudousxd/nestjs-codegen 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,16 +20,26 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/extension/index.ts
21
21
  var extension_exports = {};
22
22
  __export(extension_exports, {
23
- defineExtension: () => defineExtension
23
+ defineExtension: () => defineExtension,
24
+ requestShape: () => requestShape
24
25
  });
25
26
  module.exports = __toCommonJS(extension_exports);
26
27
 
27
28
  // src/extension/types.ts
29
+ function requestShape(route) {
30
+ const cs = route.contract?.contractSource;
31
+ const isGet = route.method.toUpperCase() === "GET";
32
+ const isQuery = isGet || !!cs?.filterFields?.length;
33
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
34
+ const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
35
+ return { isGet, isQuery, hasBody, hasQuery };
36
+ }
28
37
  function defineExtension(ext) {
29
38
  return ext;
30
39
  }
31
40
  // Annotate the CommonJS export names for ESM import in node:
32
41
  0 && (module.exports = {
33
- defineExtension
42
+ defineExtension,
43
+ requestShape
34
44
  });
35
45
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/index.ts","../../src/extension/types.ts"],"sourcesContent":["export { defineExtension } from './types.js';\nexport type {\n ApiClientLayer,\n ApiHeaderContribution,\n ApiModuleDeps,\n ApiTransport,\n CodegenExtension,\n EmittedFile,\n ExtensionContext,\n LeafModel,\n RequestModel,\n} from './types.js';\n","import type { Project } from 'ts-morph';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { RouteDescriptor } from '../discovery/types.js';\n\n/**\n * The published, versioned extension contract for `@dudousxd/nestjs-codegen`.\n *\n * Extensions are **build-time** objects (usually returned by a factory so they can take\n * options) registered explicitly via `forRoot({ extensions: [...] })`. The host runs them\n * around the core discovery → IR → emit pipeline.\n *\n * Hooks split into **multi** (every extension runs; results accumulate or chain) and\n * **single-slot** (at most one extension may claim it — two claimers is a hard error).\n *\n * @remarks Semver 0.x — the shape may change until 1.0. Out-of-repo extensions should pin\n * a compatible `@dudousxd/nestjs-codegen` peer range.\n */\nexport interface CodegenExtension {\n /** Unique id. Used in conflict/collision errors and for deterministic ordering. */\n name: string;\n\n // ── multi hooks (every extension runs) ────────────────────────────────────\n\n /**\n * Mutate/augment the route IR before emit. Runs in registration order, chained\n * (each extension sees the previous one's output). Return the new array, or mutate\n * in place and return void. Example: the filter extension attaches `filterFields` to\n * matching routes here.\n */\n transformRoutes?(\n routes: RouteDescriptor[],\n ctx: ExtensionContext,\n ): RouteDescriptor[] | undefined | Promise<RouteDescriptor[] | undefined>;\n\n /**\n * Contribute extra output files (additive). Paths are relative to `outDir`; a path\n * claimed by two extensions is a hard error. Example: the Inertia extension does its\n * own page discovery via `ctx.project()` and emits `pages.d.ts` + `components.json`.\n */\n emitFiles?(ctx: ExtensionContext): EmittedFile[] | Promise<EmittedFile[]>;\n\n /**\n * Contribute top-level code to `api.ts` (imports + statements). Runs in registration\n * order; imports are deduped by the host. Example: the Inertia extension adds\n * `import { router } from '@inertiajs/react'` and the `navigate()` helper.\n */\n apiHeader?(ctx: ExtensionContext): ApiHeaderContribution | undefined;\n\n /**\n * Add named members to a **handle** leaf. Only runs when a client layer is active\n * (i.e. the leaf is a handle, not a bare callable). Member-name collisions across\n * extensions are a hard error. Example: the filter extension adds `filterQuery` to\n * leaves whose route carries `filterFields`.\n */\n apiMembers?(leaf: LeafModel, ctx: ExtensionContext): Record<string, string> | undefined;\n\n // ── single-slot hooks (at most one extension) ─────────────────────────────\n\n /**\n * Claims **how** a single endpoint issues its request. When unset by every extension,\n * the host falls back to the neutral fetcher transport. Example: the Inertia extension\n * routes mutations through the Inertia router while GETs stay fetcher-typed.\n */\n apiTransport?: ApiTransport;\n\n /**\n * Claims **what** a leaf returns. When unset, a leaf is a bare callable returning a\n * `Promise`. Example: the TanStack extension wraps each leaf into a handle exposing\n * `{ fetch, queryKey, queryOptions | mutationOptions }`.\n */\n apiClientLayer?: ApiClientLayer;\n}\n\n/** Shared, read-only context handed to every extension hook. */\nexport interface ExtensionContext {\n cwd: string;\n outDir: string;\n routes: readonly RouteDescriptor[];\n config: ResolvedConfig;\n /** Lazily-created shared ts-morph Project for AST work (pages, custom decorators). */\n project(): Project;\n}\n\n/** A file contributed by an extension's `emitFiles` hook. */\nexport interface EmittedFile {\n /** Path relative to `outDir`. A collision across extensions throws. */\n path: string;\n contents: string;\n}\n\n/** Top-level `api.ts` contributions from an extension's `apiHeader` hook. */\nexport interface ApiHeaderContribution {\n /** Raw import lines (e.g. `import { router } from '@inertiajs/react';`), deduped by the host. */\n imports?: string[];\n /** Top-level statements appended after the api factory (e.g. the `navigate()` helper). */\n statements?: string[];\n}\n\n/**\n * The neutral, per-endpoint request model the host builds for each leaf before any\n * transport/layer runs. Extensions read this to render their output.\n */\nexport interface RequestModel {\n /** Dot-path route name, e.g. `users.show`. */\n routeName: string;\n method: 'get' | 'post' | 'put' | 'patch' | 'delete';\n isGet: boolean;\n /** True for reads: a GET, or a filter-search route (has `filterFields`) even when POST.\n * Client layers use this (not `isGet`) to decide query vs mutation helpers. */\n isQuery: boolean;\n hasParams: boolean;\n hasBody: boolean;\n /** Type of the leaf's `input` arg, e.g. `{ params: ...; query?: ... }` or `Record<string, never>`. */\n inputType: string;\n /** URL expression, e.g. `route('users.show', input?.params) || '/api/users/:id'`. */\n urlExpr: string;\n /** Request-options expression, e.g. `{ query: ... }` or `{ body: input?.body }`. */\n optsExpr: string;\n /** Response type access, e.g. `ApiRouter['users']['show']['response']`. */\n responseType: string;\n /** Body type access, e.g. `ApiRouter['users']['create']['body']` (for mutation layers). */\n bodyType: string;\n /** Stable query-key expression, e.g. `[\"users.show\", input] as const`. */\n queryKeyExpr: string;\n}\n\n/**\n * Per-leaf model passed through the api.ts pipeline: transport → layer → member\n * contributors → render. `requestExpr` is set by the transport; `members`, when present,\n * flips the leaf from a bare callable to a handle.\n */\nexport interface LeafModel {\n route: RouteDescriptor;\n request: RequestModel;\n /** The expression that issues the request (set by the transport, default = fetcher). */\n requestExpr: string;\n /** When present, the leaf renders as a handle exposing these members (ordered). */\n members?: Record<string, string>;\n}\n\n/**\n * Top-level `api.ts` imports + helpers a transport or layer depends on. Functions of the\n * context so they can be route-aware (e.g. only import `mutationOptions` when a mutation\n * exists). Imports are deduped by the host across all extensions.\n */\nexport interface ApiModuleDeps {\n /** Raw import lines (e.g. `import { queryOptions as _q } from '@tanstack/react-query';`). */\n imports?(ctx: ExtensionContext): string[];\n /** Module-level helper declarations the rendered expressions depend on. */\n helpers?(ctx: ExtensionContext): string[];\n}\n\n/** Single-slot: decides how an endpoint issues its request. */\nexport interface ApiTransport extends ApiModuleDeps {\n name: string;\n /** Render the expression that issues this endpoint's request (e.g. `fetcher.get<Res>(url, opts)`). */\n renderRequest(leaf: LeafModel, ctx: ExtensionContext): string;\n}\n\n/** Single-slot: decides what a leaf returns (the handle members). */\nexport interface ApiClientLayer extends ApiModuleDeps {\n name: string;\n /**\n * Given the request expression (from the transport) and the leaf, return the handle's\n * members as an ordered `name → value` map (value is the expression after `name: `).\n * Returning members flips the leaf from a bare callable to a handle.\n */\n buildMembers(requestExpr: string, leaf: LeafModel, ctx: ExtensionContext): Record<string, string>;\n}\n\n/** Identity helper for authoring extensions with full type inference. */\nexport function defineExtension(ext: CodegenExtension): CodegenExtension {\n return ext;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC2KO,SAAS,gBAAgB,KAAyC;AACvE,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/extension/index.ts","../../src/extension/types.ts"],"sourcesContent":["export { defineExtension, requestShape } from './types.js';\nexport type {\n ApiClientLayer,\n ApiHeaderContribution,\n ApiModuleDeps,\n CodegenExtension,\n EmittedFile,\n ExtensionContext,\n LeafModel,\n RequestModel,\n RequestShape,\n} from './types.js';\n","import type { Project } from 'ts-morph';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { RouteDescriptor } from '../discovery/types.js';\n\n/**\n * The published, versioned extension contract for `@dudousxd/nestjs-codegen`.\n *\n * Extensions are **build-time** objects (usually returned by a factory so they can take\n * options) registered explicitly via `forRoot({ extensions: [...] })`. The host runs them\n * around the core discovery → IR → emit pipeline.\n *\n * Hooks split into **multi** (every extension runs; results accumulate or chain) and\n * **single-slot** (at most one extension may claim it — two claimers is a hard error).\n *\n * @remarks Semver 0.x — the shape may change until 1.0. Out-of-repo extensions should pin\n * a compatible `@dudousxd/nestjs-codegen` peer range.\n */\nexport interface CodegenExtension {\n /** Unique id. Used in conflict/collision errors and for deterministic ordering. */\n name: string;\n\n // ── multi hooks (every extension runs) ────────────────────────────────────\n\n /**\n * Mutate/augment the route IR before emit. Runs in registration order, chained\n * (each extension sees the previous one's output). Return the new array, or mutate\n * in place and return void. Example: the filter extension attaches `filterFields` to\n * matching routes here.\n */\n transformRoutes?(\n routes: RouteDescriptor[],\n ctx: ExtensionContext,\n ): RouteDescriptor[] | undefined | Promise<RouteDescriptor[] | undefined>;\n\n /**\n * Contribute extra output files (additive). Paths are relative to `outDir`; a path\n * claimed by two extensions is a hard error. Example: the Inertia extension does its\n * own page discovery via `ctx.project()` and emits `pages.d.ts` + `components.json`.\n */\n emitFiles?(ctx: ExtensionContext): EmittedFile[] | Promise<EmittedFile[]>;\n\n /**\n * Contribute top-level code to `api.ts` (imports + statements). Runs in registration\n * order; imports are deduped by the host. Example: the Inertia extension adds\n * `import { router } from '@inertiajs/react'` and the `navigate()` helper.\n */\n apiHeader?(ctx: ExtensionContext): ApiHeaderContribution | undefined;\n\n /**\n * Add named members to a **handle** leaf. Only runs when a client layer is active\n * (i.e. the leaf is a handle, not a bare callable). Member-name collisions across\n * extensions are a hard error. Example: the filter extension adds `filterQuery` to\n * leaves whose route carries `filterFields`.\n */\n apiMembers?(leaf: LeafModel, ctx: ExtensionContext): Record<string, string> | undefined;\n\n // ── single-slot hooks (at most one extension) ─────────────────────────────\n\n /**\n * Claims **what** a leaf returns and **how** it issues its request. At most one extension\n * may claim it. When unset, a leaf is a bare awaitable callable backed by the neutral\n * fetcher. Example: the TanStack extension wraps each leaf into a handle exposing\n * `{ fetch, queryKey, queryOptions | mutationOptions }`, composing with the fetcher\n * request the host passes in.\n */\n apiClientLayer?: ApiClientLayer;\n}\n\n/** Shared, read-only context handed to every extension hook. */\nexport interface ExtensionContext {\n cwd: string;\n outDir: string;\n routes: readonly RouteDescriptor[];\n config: ResolvedConfig;\n /** Lazily-created shared ts-morph Project for AST work (pages, custom decorators). */\n project(): Project;\n}\n\n/** A file contributed by an extension's `emitFiles` hook. */\nexport interface EmittedFile {\n /** Path relative to `outDir`. A collision across extensions throws. */\n path: string;\n contents: string;\n}\n\n/** Top-level `api.ts` contributions from an extension's `apiHeader` hook. */\nexport interface ApiHeaderContribution {\n /** Raw import lines (e.g. `import { router } from '@inertiajs/react';`), deduped by the host. */\n imports?: string[];\n /** Top-level statements appended after the api factory (e.g. the `navigate()` helper). */\n statements?: string[];\n}\n\n/**\n * The neutral, per-endpoint request model the host builds for each leaf before any\n * transport/layer runs. Extensions read this to render their output.\n */\nexport interface RequestModel {\n /** Dot-path route name, e.g. `users.show`. */\n routeName: string;\n method: 'get' | 'post' | 'put' | 'patch' | 'delete';\n isGet: boolean;\n /** True for reads: a GET, or a filter-search route (has `filterFields`) even when POST.\n * Client layers use this (not `isGet`) to decide query vs mutation helpers. */\n isQuery: boolean;\n hasParams: boolean;\n hasBody: boolean;\n /** Type of the leaf's `input` arg, e.g. `{ params: ...; query?: ... }` or `Record<string, never>`. */\n inputType: string;\n /** URL expression, e.g. `route('users.show', input?.params) || '/api/users/:id'`. */\n urlExpr: string;\n /** Request-options expression, e.g. `{ query: ... }` or `{ body: input?.body }`. */\n optsExpr: string;\n /** Response type access, e.g. `ApiRouter['users']['show']['response']`. */\n responseType: string;\n /** Stable query-key expression, e.g. `[\"users.show\", input] as const`. */\n queryKeyExpr: string;\n}\n\n/**\n * Per-leaf model passed through the api.ts pipeline: layer → member contributors → render.\n * `requestExpr` is the host's neutral fetcher request; `members`, when present, flips the\n * leaf from a bare callable to a handle.\n */\nexport interface LeafModel {\n route: RouteDescriptor;\n request: RequestModel;\n /** The expression that issues the request (the host's neutral fetcher call). */\n requestExpr: string;\n /** When present, the leaf renders as a handle exposing these members (ordered). */\n members?: Record<string, string>;\n}\n\n/**\n * Top-level `api.ts` imports a client layer depends on. A function of the context so it can\n * be route-aware (e.g. only import `mutationOptions` when a mutation exists). Imports are\n * deduped by the host across all extensions.\n */\nexport interface ApiModuleDeps {\n /** Raw import lines (e.g. `import { queryOptions as _q } from '@tanstack/react-query';`). */\n imports?(ctx: ExtensionContext): string[];\n}\n\n/** Single-slot: decides what a leaf returns (the handle members). */\nexport interface ApiClientLayer extends ApiModuleDeps {\n name: string;\n /**\n * Given the request expression (from the transport) and the leaf, return the handle's\n * members as an ordered `name → value` map (value is the expression after `name: `).\n * Returning members flips the leaf from a bare callable to a handle.\n */\n buildMembers(requestExpr: string, leaf: LeafModel, ctx: ExtensionContext): Record<string, string>;\n}\n\n/**\n * The four request-shape flags derived from a route's method + contract. Computed in ONE\n * place ({@link requestShape}) and read by both the host emitter and client-layer\n * extensions, so the \"filter-search POST counts as a read\" rule is encoded exactly once.\n */\nexport interface RequestShape {\n /** The route is a `GET`. */\n isGet: boolean;\n /** True for reads: a GET, or a filter-search route (carries `filterFields`) even when POST.\n * Client layers use this (not `isGet`) to decide query vs mutation helpers. */\n isQuery: boolean;\n /** The route carries a body contract (a mutation payload). */\n hasBody: boolean;\n /** The route can take a query string — always for GET; a mutation may too (query + body). */\n hasQuery: boolean;\n}\n\n/**\n * Compute the {@link RequestShape} flags for a route from its method and contract. This is\n * the SINGLE source of truth for these flags — `buildRequestModel`, the TanStack layer's\n * `imports()`, and any other reader must call this rather than re-deriving. The\n * \"filter-search POST counts as a read\" rule lives here and nowhere else.\n */\nexport function requestShape(route: RouteDescriptor): RequestShape {\n const cs = route.contract?.contractSource;\n const isGet = route.method.toUpperCase() === 'GET';\n const isQuery = isGet || !!cs?.filterFields?.length;\n const hasBody = !!cs?.bodyRef || (cs?.body != null && cs.body !== 'never');\n const hasQuery = isGet || !!cs?.queryRef || (cs?.query != null && cs.query !== 'never');\n return { isGet, isQuery, hasBody, hasQuery };\n}\n\n/** Identity helper for authoring extensions with full type inference. */\nexport function defineExtension(ext: CodegenExtension): CodegenExtension {\n return ext;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiLO,SAAS,aAAa,OAAsC;AACjE,QAAM,KAAK,MAAM,UAAU;AAC3B,QAAM,QAAQ,MAAM,OAAO,YAAY,MAAM;AAC7C,QAAM,UAAU,SAAS,CAAC,CAAC,IAAI,cAAc;AAC7C,QAAM,UAAU,CAAC,CAAC,IAAI,WAAY,IAAI,QAAQ,QAAQ,GAAG,SAAS;AAClE,QAAM,WAAW,SAAS,CAAC,CAAC,IAAI,YAAa,IAAI,SAAS,QAAQ,GAAG,UAAU;AAC/E,SAAO,EAAE,OAAO,SAAS,SAAS,SAAS;AAC7C;AAGO,SAAS,gBAAgB,KAAyC;AACvE,SAAO;AACT;","names":[]}
@@ -1,2 +1,2 @@
1
- export { l as ApiClientLayer, m as ApiHeaderContribution, n as ApiModuleDeps, o as ApiTransport, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as defineExtension } from '../index-BwIRjOQA.cjs';
1
+ export { l as ApiClientLayer, m as ApiHeaderContribution, n as ApiModuleDeps, C as CodegenExtension, o as EmittedFile, E as ExtensionContext, L as LeafModel, p as RequestModel, q as RequestShape, s as defineExtension, t as requestShape } from '../index-DA4uySjo.cjs';
2
2
  import 'ts-morph';
@@ -1,2 +1,2 @@
1
- export { l as ApiClientLayer, m as ApiHeaderContribution, n as ApiModuleDeps, o as ApiTransport, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as defineExtension } from '../index-BwIRjOQA.js';
1
+ export { l as ApiClientLayer, m as ApiHeaderContribution, n as ApiModuleDeps, C as CodegenExtension, o as EmittedFile, E as ExtensionContext, L as LeafModel, p as RequestModel, q as RequestShape, s as defineExtension, t as requestShape } from '../index-DA4uySjo.js';
2
2
  import 'ts-morph';
@@ -1,8 +1,17 @@
1
1
  // src/extension/types.ts
2
+ function requestShape(route) {
3
+ const cs = route.contract?.contractSource;
4
+ const isGet = route.method.toUpperCase() === "GET";
5
+ const isQuery = isGet || !!cs?.filterFields?.length;
6
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
7
+ const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
8
+ return { isGet, isQuery, hasBody, hasQuery };
9
+ }
2
10
  function defineExtension(ext) {
3
11
  return ext;
4
12
  }
5
13
  export {
6
- defineExtension
14
+ defineExtension,
15
+ requestShape
7
16
  };
8
17
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/types.ts"],"sourcesContent":["import type { Project } from 'ts-morph';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { RouteDescriptor } from '../discovery/types.js';\n\n/**\n * The published, versioned extension contract for `@dudousxd/nestjs-codegen`.\n *\n * Extensions are **build-time** objects (usually returned by a factory so they can take\n * options) registered explicitly via `forRoot({ extensions: [...] })`. The host runs them\n * around the core discovery → IR → emit pipeline.\n *\n * Hooks split into **multi** (every extension runs; results accumulate or chain) and\n * **single-slot** (at most one extension may claim it — two claimers is a hard error).\n *\n * @remarks Semver 0.x — the shape may change until 1.0. Out-of-repo extensions should pin\n * a compatible `@dudousxd/nestjs-codegen` peer range.\n */\nexport interface CodegenExtension {\n /** Unique id. Used in conflict/collision errors and for deterministic ordering. */\n name: string;\n\n // ── multi hooks (every extension runs) ────────────────────────────────────\n\n /**\n * Mutate/augment the route IR before emit. Runs in registration order, chained\n * (each extension sees the previous one's output). Return the new array, or mutate\n * in place and return void. Example: the filter extension attaches `filterFields` to\n * matching routes here.\n */\n transformRoutes?(\n routes: RouteDescriptor[],\n ctx: ExtensionContext,\n ): RouteDescriptor[] | undefined | Promise<RouteDescriptor[] | undefined>;\n\n /**\n * Contribute extra output files (additive). Paths are relative to `outDir`; a path\n * claimed by two extensions is a hard error. Example: the Inertia extension does its\n * own page discovery via `ctx.project()` and emits `pages.d.ts` + `components.json`.\n */\n emitFiles?(ctx: ExtensionContext): EmittedFile[] | Promise<EmittedFile[]>;\n\n /**\n * Contribute top-level code to `api.ts` (imports + statements). Runs in registration\n * order; imports are deduped by the host. Example: the Inertia extension adds\n * `import { router } from '@inertiajs/react'` and the `navigate()` helper.\n */\n apiHeader?(ctx: ExtensionContext): ApiHeaderContribution | undefined;\n\n /**\n * Add named members to a **handle** leaf. Only runs when a client layer is active\n * (i.e. the leaf is a handle, not a bare callable). Member-name collisions across\n * extensions are a hard error. Example: the filter extension adds `filterQuery` to\n * leaves whose route carries `filterFields`.\n */\n apiMembers?(leaf: LeafModel, ctx: ExtensionContext): Record<string, string> | undefined;\n\n // ── single-slot hooks (at most one extension) ─────────────────────────────\n\n /**\n * Claims **how** a single endpoint issues its request. When unset by every extension,\n * the host falls back to the neutral fetcher transport. Example: the Inertia extension\n * routes mutations through the Inertia router while GETs stay fetcher-typed.\n */\n apiTransport?: ApiTransport;\n\n /**\n * Claims **what** a leaf returns. When unset, a leaf is a bare callable returning a\n * `Promise`. Example: the TanStack extension wraps each leaf into a handle exposing\n * `{ fetch, queryKey, queryOptions | mutationOptions }`.\n */\n apiClientLayer?: ApiClientLayer;\n}\n\n/** Shared, read-only context handed to every extension hook. */\nexport interface ExtensionContext {\n cwd: string;\n outDir: string;\n routes: readonly RouteDescriptor[];\n config: ResolvedConfig;\n /** Lazily-created shared ts-morph Project for AST work (pages, custom decorators). */\n project(): Project;\n}\n\n/** A file contributed by an extension's `emitFiles` hook. */\nexport interface EmittedFile {\n /** Path relative to `outDir`. A collision across extensions throws. */\n path: string;\n contents: string;\n}\n\n/** Top-level `api.ts` contributions from an extension's `apiHeader` hook. */\nexport interface ApiHeaderContribution {\n /** Raw import lines (e.g. `import { router } from '@inertiajs/react';`), deduped by the host. */\n imports?: string[];\n /** Top-level statements appended after the api factory (e.g. the `navigate()` helper). */\n statements?: string[];\n}\n\n/**\n * The neutral, per-endpoint request model the host builds for each leaf before any\n * transport/layer runs. Extensions read this to render their output.\n */\nexport interface RequestModel {\n /** Dot-path route name, e.g. `users.show`. */\n routeName: string;\n method: 'get' | 'post' | 'put' | 'patch' | 'delete';\n isGet: boolean;\n /** True for reads: a GET, or a filter-search route (has `filterFields`) even when POST.\n * Client layers use this (not `isGet`) to decide query vs mutation helpers. */\n isQuery: boolean;\n hasParams: boolean;\n hasBody: boolean;\n /** Type of the leaf's `input` arg, e.g. `{ params: ...; query?: ... }` or `Record<string, never>`. */\n inputType: string;\n /** URL expression, e.g. `route('users.show', input?.params) || '/api/users/:id'`. */\n urlExpr: string;\n /** Request-options expression, e.g. `{ query: ... }` or `{ body: input?.body }`. */\n optsExpr: string;\n /** Response type access, e.g. `ApiRouter['users']['show']['response']`. */\n responseType: string;\n /** Body type access, e.g. `ApiRouter['users']['create']['body']` (for mutation layers). */\n bodyType: string;\n /** Stable query-key expression, e.g. `[\"users.show\", input] as const`. */\n queryKeyExpr: string;\n}\n\n/**\n * Per-leaf model passed through the api.ts pipeline: transport → layer → member\n * contributors → render. `requestExpr` is set by the transport; `members`, when present,\n * flips the leaf from a bare callable to a handle.\n */\nexport interface LeafModel {\n route: RouteDescriptor;\n request: RequestModel;\n /** The expression that issues the request (set by the transport, default = fetcher). */\n requestExpr: string;\n /** When present, the leaf renders as a handle exposing these members (ordered). */\n members?: Record<string, string>;\n}\n\n/**\n * Top-level `api.ts` imports + helpers a transport or layer depends on. Functions of the\n * context so they can be route-aware (e.g. only import `mutationOptions` when a mutation\n * exists). Imports are deduped by the host across all extensions.\n */\nexport interface ApiModuleDeps {\n /** Raw import lines (e.g. `import { queryOptions as _q } from '@tanstack/react-query';`). */\n imports?(ctx: ExtensionContext): string[];\n /** Module-level helper declarations the rendered expressions depend on. */\n helpers?(ctx: ExtensionContext): string[];\n}\n\n/** Single-slot: decides how an endpoint issues its request. */\nexport interface ApiTransport extends ApiModuleDeps {\n name: string;\n /** Render the expression that issues this endpoint's request (e.g. `fetcher.get<Res>(url, opts)`). */\n renderRequest(leaf: LeafModel, ctx: ExtensionContext): string;\n}\n\n/** Single-slot: decides what a leaf returns (the handle members). */\nexport interface ApiClientLayer extends ApiModuleDeps {\n name: string;\n /**\n * Given the request expression (from the transport) and the leaf, return the handle's\n * members as an ordered `name → value` map (value is the expression after `name: `).\n * Returning members flips the leaf from a bare callable to a handle.\n */\n buildMembers(requestExpr: string, leaf: LeafModel, ctx: ExtensionContext): Record<string, string>;\n}\n\n/** Identity helper for authoring extensions with full type inference. */\nexport function defineExtension(ext: CodegenExtension): CodegenExtension {\n return ext;\n}\n"],"mappings":";AA2KO,SAAS,gBAAgB,KAAyC;AACvE,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/extension/types.ts"],"sourcesContent":["import type { Project } from 'ts-morph';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { RouteDescriptor } from '../discovery/types.js';\n\n/**\n * The published, versioned extension contract for `@dudousxd/nestjs-codegen`.\n *\n * Extensions are **build-time** objects (usually returned by a factory so they can take\n * options) registered explicitly via `forRoot({ extensions: [...] })`. The host runs them\n * around the core discovery → IR → emit pipeline.\n *\n * Hooks split into **multi** (every extension runs; results accumulate or chain) and\n * **single-slot** (at most one extension may claim it — two claimers is a hard error).\n *\n * @remarks Semver 0.x — the shape may change until 1.0. Out-of-repo extensions should pin\n * a compatible `@dudousxd/nestjs-codegen` peer range.\n */\nexport interface CodegenExtension {\n /** Unique id. Used in conflict/collision errors and for deterministic ordering. */\n name: string;\n\n // ── multi hooks (every extension runs) ────────────────────────────────────\n\n /**\n * Mutate/augment the route IR before emit. Runs in registration order, chained\n * (each extension sees the previous one's output). Return the new array, or mutate\n * in place and return void. Example: the filter extension attaches `filterFields` to\n * matching routes here.\n */\n transformRoutes?(\n routes: RouteDescriptor[],\n ctx: ExtensionContext,\n ): RouteDescriptor[] | undefined | Promise<RouteDescriptor[] | undefined>;\n\n /**\n * Contribute extra output files (additive). Paths are relative to `outDir`; a path\n * claimed by two extensions is a hard error. Example: the Inertia extension does its\n * own page discovery via `ctx.project()` and emits `pages.d.ts` + `components.json`.\n */\n emitFiles?(ctx: ExtensionContext): EmittedFile[] | Promise<EmittedFile[]>;\n\n /**\n * Contribute top-level code to `api.ts` (imports + statements). Runs in registration\n * order; imports are deduped by the host. Example: the Inertia extension adds\n * `import { router } from '@inertiajs/react'` and the `navigate()` helper.\n */\n apiHeader?(ctx: ExtensionContext): ApiHeaderContribution | undefined;\n\n /**\n * Add named members to a **handle** leaf. Only runs when a client layer is active\n * (i.e. the leaf is a handle, not a bare callable). Member-name collisions across\n * extensions are a hard error. Example: the filter extension adds `filterQuery` to\n * leaves whose route carries `filterFields`.\n */\n apiMembers?(leaf: LeafModel, ctx: ExtensionContext): Record<string, string> | undefined;\n\n // ── single-slot hooks (at most one extension) ─────────────────────────────\n\n /**\n * Claims **what** a leaf returns and **how** it issues its request. At most one extension\n * may claim it. When unset, a leaf is a bare awaitable callable backed by the neutral\n * fetcher. Example: the TanStack extension wraps each leaf into a handle exposing\n * `{ fetch, queryKey, queryOptions | mutationOptions }`, composing with the fetcher\n * request the host passes in.\n */\n apiClientLayer?: ApiClientLayer;\n}\n\n/** Shared, read-only context handed to every extension hook. */\nexport interface ExtensionContext {\n cwd: string;\n outDir: string;\n routes: readonly RouteDescriptor[];\n config: ResolvedConfig;\n /** Lazily-created shared ts-morph Project for AST work (pages, custom decorators). */\n project(): Project;\n}\n\n/** A file contributed by an extension's `emitFiles` hook. */\nexport interface EmittedFile {\n /** Path relative to `outDir`. A collision across extensions throws. */\n path: string;\n contents: string;\n}\n\n/** Top-level `api.ts` contributions from an extension's `apiHeader` hook. */\nexport interface ApiHeaderContribution {\n /** Raw import lines (e.g. `import { router } from '@inertiajs/react';`), deduped by the host. */\n imports?: string[];\n /** Top-level statements appended after the api factory (e.g. the `navigate()` helper). */\n statements?: string[];\n}\n\n/**\n * The neutral, per-endpoint request model the host builds for each leaf before any\n * transport/layer runs. Extensions read this to render their output.\n */\nexport interface RequestModel {\n /** Dot-path route name, e.g. `users.show`. */\n routeName: string;\n method: 'get' | 'post' | 'put' | 'patch' | 'delete';\n isGet: boolean;\n /** True for reads: a GET, or a filter-search route (has `filterFields`) even when POST.\n * Client layers use this (not `isGet`) to decide query vs mutation helpers. */\n isQuery: boolean;\n hasParams: boolean;\n hasBody: boolean;\n /** Type of the leaf's `input` arg, e.g. `{ params: ...; query?: ... }` or `Record<string, never>`. */\n inputType: string;\n /** URL expression, e.g. `route('users.show', input?.params) || '/api/users/:id'`. */\n urlExpr: string;\n /** Request-options expression, e.g. `{ query: ... }` or `{ body: input?.body }`. */\n optsExpr: string;\n /** Response type access, e.g. `ApiRouter['users']['show']['response']`. */\n responseType: string;\n /** Stable query-key expression, e.g. `[\"users.show\", input] as const`. */\n queryKeyExpr: string;\n}\n\n/**\n * Per-leaf model passed through the api.ts pipeline: layer → member contributors → render.\n * `requestExpr` is the host's neutral fetcher request; `members`, when present, flips the\n * leaf from a bare callable to a handle.\n */\nexport interface LeafModel {\n route: RouteDescriptor;\n request: RequestModel;\n /** The expression that issues the request (the host's neutral fetcher call). */\n requestExpr: string;\n /** When present, the leaf renders as a handle exposing these members (ordered). */\n members?: Record<string, string>;\n}\n\n/**\n * Top-level `api.ts` imports a client layer depends on. A function of the context so it can\n * be route-aware (e.g. only import `mutationOptions` when a mutation exists). Imports are\n * deduped by the host across all extensions.\n */\nexport interface ApiModuleDeps {\n /** Raw import lines (e.g. `import { queryOptions as _q } from '@tanstack/react-query';`). */\n imports?(ctx: ExtensionContext): string[];\n}\n\n/** Single-slot: decides what a leaf returns (the handle members). */\nexport interface ApiClientLayer extends ApiModuleDeps {\n name: string;\n /**\n * Given the request expression (from the transport) and the leaf, return the handle's\n * members as an ordered `name → value` map (value is the expression after `name: `).\n * Returning members flips the leaf from a bare callable to a handle.\n */\n buildMembers(requestExpr: string, leaf: LeafModel, ctx: ExtensionContext): Record<string, string>;\n}\n\n/**\n * The four request-shape flags derived from a route's method + contract. Computed in ONE\n * place ({@link requestShape}) and read by both the host emitter and client-layer\n * extensions, so the \"filter-search POST counts as a read\" rule is encoded exactly once.\n */\nexport interface RequestShape {\n /** The route is a `GET`. */\n isGet: boolean;\n /** True for reads: a GET, or a filter-search route (carries `filterFields`) even when POST.\n * Client layers use this (not `isGet`) to decide query vs mutation helpers. */\n isQuery: boolean;\n /** The route carries a body contract (a mutation payload). */\n hasBody: boolean;\n /** The route can take a query string — always for GET; a mutation may too (query + body). */\n hasQuery: boolean;\n}\n\n/**\n * Compute the {@link RequestShape} flags for a route from its method and contract. This is\n * the SINGLE source of truth for these flags — `buildRequestModel`, the TanStack layer's\n * `imports()`, and any other reader must call this rather than re-deriving. The\n * \"filter-search POST counts as a read\" rule lives here and nowhere else.\n */\nexport function requestShape(route: RouteDescriptor): RequestShape {\n const cs = route.contract?.contractSource;\n const isGet = route.method.toUpperCase() === 'GET';\n const isQuery = isGet || !!cs?.filterFields?.length;\n const hasBody = !!cs?.bodyRef || (cs?.body != null && cs.body !== 'never');\n const hasQuery = isGet || !!cs?.queryRef || (cs?.query != null && cs.query !== 'never');\n return { isGet, isQuery, hasBody, hasQuery };\n}\n\n/** Identity helper for authoring extensions with full type inference. */\nexport function defineExtension(ext: CodegenExtension): CodegenExtension {\n return ext;\n}\n"],"mappings":";AAiLO,SAAS,aAAa,OAAsC;AACjE,QAAM,KAAK,MAAM,UAAU;AAC3B,QAAM,QAAQ,MAAM,OAAO,YAAY,MAAM;AAC7C,QAAM,UAAU,SAAS,CAAC,CAAC,IAAI,cAAc;AAC7C,QAAM,UAAU,CAAC,CAAC,IAAI,WAAY,IAAI,QAAQ,QAAQ,GAAG,SAAS;AAClE,QAAM,WAAW,SAAS,CAAC,CAAC,IAAI,YAAa,IAAI,SAAS,QAAQ,GAAG,UAAU;AAC/E,SAAO,EAAE,OAAO,SAAS,SAAS,SAAS;AAC7C;AAGO,SAAS,gBAAgB,KAAyC;AACvE,SAAO;AACT;","names":[]}
@@ -111,6 +111,14 @@ interface SchemaModule {
111
111
  root: SchemaNode;
112
112
  named: Map<string, SchemaNode>;
113
113
  warnings: string[];
114
+ /**
115
+ * Names (keys of {@link named}) that are genuinely self/mutually recursive,
116
+ * i.e. reachable from themselves through a `lazyRef` back-edge. Adapters use
117
+ * this to break the TypeScript inference cycle (annotated const + hoisted
118
+ * structural type for zod/valibot; `this`/degrade for arktype). Absent or
119
+ * empty means no recursion.
120
+ */
121
+ recursive?: Set<string>;
114
122
  }
115
123
 
116
124
  /** Signals to an adapter what it must import to render the current output. */
@@ -121,12 +129,32 @@ interface AdapterUsage {
121
129
  interface RenderContext {
122
130
  /** Hoisted named schemas being emitted alongside the root. */
123
131
  named: Map<string, SchemaNode>;
132
+ /**
133
+ * Name of the hoisted schema currently being rendered, when rendering a named
134
+ * schema's own body. Lets an adapter express a self-reference specially (e.g.
135
+ * arktype's `this` keyword). Absent when rendering the root.
136
+ */
137
+ selfName?: string;
124
138
  }
125
139
  interface RenderedModule {
126
140
  /** Root schema source text, e.g. `"z.object({ email: z.string().email() })"`. */
127
141
  schemaText: string;
128
142
  /** name → schema source text, hoisted above the parent. */
129
143
  namedNestedSchemas: Map<string, string>;
144
+ /**
145
+ * name → hoisted TS `type` alias *body* for a recursive schema (e.g.
146
+ * `"{ field?: string; and?: Array<ColumnFilter> }"`), emitted as
147
+ * `type <TypeName> = <body>;` above the const. The alias name is derived from
148
+ * the schema name. Absent for adapters/schemas that don't need it (arktype,
149
+ * non-recursive schemas).
150
+ */
151
+ namedTypeAliases?: Map<string, string>;
152
+ /**
153
+ * name → const type annotation for a recursive schema (e.g.
154
+ * `"z.ZodType<ColumnFilter>"`), emitted as `const <name>: <annotation> = ...`
155
+ * to break the implicit-any self-reference cycle.
156
+ */
157
+ namedAnnotations?: Map<string, string>;
130
158
  warnings: string[];
131
159
  }
132
160
  /**
@@ -137,6 +165,12 @@ interface RenderedModule {
137
165
  interface ValidationAdapter {
138
166
  /** 'zod' | 'valibot' | 'arktype'. */
139
167
  name: string;
168
+ /**
169
+ * When `true`, the emitter may pass raw zod source from `defineContract`
170
+ * (`bodyZodText`/`bodyZodRef`) through verbatim and emit a `z` import. Only the
171
+ * zod adapter sets this; any other adapter skips raw-zod sources with a warning.
172
+ */
173
+ acceptsRawZodSource?: boolean;
140
174
  /** Import lines required for any rendered text (e.g. `import { z } from 'zod'`). */
141
175
  importStatements(usage: AdapterUsage): string[];
142
176
  /** Render a single node to this lib's source text. */
@@ -150,14 +184,14 @@ interface ValidationAdapter {
150
184
  /** A built-in adapter name or a custom adapter instance. */
151
185
  type ValidationOption = 'zod' | 'valibot' | 'arktype' | ValidationAdapter;
152
186
  /**
153
- * Resolve a `validation` config value to a {@link ValidationAdapter}. `'zod'` is
154
- * bundled in core; the valibot/arktype adapters ship as their own packages — import
155
- * the adapter instance and pass it directly (it passes through here). A custom
156
- * adapter object also passes through.
187
+ * Resolve a `validation` config value to a {@link ValidationAdapter}. No adapter is
188
+ * bundled in core the zod/valibot/arktype adapters ship as their own packages.
189
+ * Import the adapter instance and pass it directly (it passes through here). A
190
+ * custom adapter object also passes through.
157
191
  *
158
192
  * @example
159
- * import { valibotAdapter } from '@dudousxd/nestjs-codegen-valibot';
160
- * defineConfig({ validation: valibotAdapter });
193
+ * import { zodAdapter } from '@dudousxd/nestjs-codegen-zod';
194
+ * defineConfig({ validation: zodAdapter });
161
195
  */
162
196
  declare function resolveAdapter(option: ValidationOption): ValidationAdapter;
163
197
 
@@ -166,15 +200,15 @@ interface UserConfig {
166
200
  * Codegen extensions, applied in order. Each may augment the route IR
167
201
  * (`transformRoutes`), contribute extra output files (`emitFiles`), and — once a
168
202
  * client layer is active — shape `api.ts`. Registered explicitly, e.g.
169
- * `extensions: [nestjsInertiaCodegen(), tanstackQuery()]`.
203
+ * `extensions: [clientCodegen(), tanstackQuery()]`.
170
204
  */
171
205
  extensions?: CodegenExtension[];
172
206
  /**
173
- * Validation library for emitted `forms.ts` schemas. `'zod'` (bundled, default)
174
- * or an imported adapter instance (`valibotAdapter`/`arktypeAdapter`).
175
- * @default 'zod'
207
+ * Validation library for emitted `forms.ts` schemas. Required pass an imported
208
+ * adapter instance, e.g. `zodAdapter` from `@dudousxd/nestjs-codegen-zod`, or
209
+ * `valibotAdapter`/`arktypeAdapter` from their packages.
176
210
  */
177
- validation?: ValidationOption;
211
+ validation: ValidationOption;
178
212
  /** Inertia page discovery. Omit when you don't use Inertia. */
179
213
  pages?: {
180
214
  glob: string;
@@ -202,11 +236,11 @@ interface UserConfig {
202
236
  * This lets users configure baseUrl, headers, plugins (e.g. superjson).
203
237
  *
204
238
  * @example
205
- * // nestjs-inertia.config.ts
239
+ * // nestjs-codegen.config.ts
206
240
  * fetcher: { importPath: '~/lib/api' }
207
241
  *
208
- * // inertia/lib/api.ts
209
- * import { createFetcher } from '@dudousxd/nestjs-inertia-client';
242
+ * // src/lib/api.ts
243
+ * import { createFetcher } from '@dudousxd/nestjs-client';
210
244
  * export const fetcher = createFetcher({ baseUrl: '/api' });
211
245
  */
212
246
  fetcher?: {
@@ -402,15 +436,11 @@ interface CodegenExtension {
402
436
  */
403
437
  apiMembers?(leaf: LeafModel, ctx: ExtensionContext): Record<string, string> | undefined;
404
438
  /**
405
- * Claims **how** a single endpoint issues its request. When unset by every extension,
406
- * the host falls back to the neutral fetcher transport. Example: the Inertia extension
407
- * routes mutations through the Inertia router while GETs stay fetcher-typed.
408
- */
409
- apiTransport?: ApiTransport;
410
- /**
411
- * Claims **what** a leaf returns. When unset, a leaf is a bare callable returning a
412
- * `Promise`. Example: the TanStack extension wraps each leaf into a handle exposing
413
- * `{ fetch, queryKey, queryOptions | mutationOptions }`.
439
+ * Claims **what** a leaf returns and **how** it issues its request. At most one extension
440
+ * may claim it. When unset, a leaf is a bare awaitable callable backed by the neutral
441
+ * fetcher. Example: the TanStack extension wraps each leaf into a handle exposing
442
+ * `{ fetch, queryKey, queryOptions | mutationOptions }`, composing with the fetcher
443
+ * request the host passes in.
414
444
  */
415
445
  apiClientLayer?: ApiClientLayer;
416
446
  }
@@ -458,40 +488,30 @@ interface RequestModel {
458
488
  optsExpr: string;
459
489
  /** Response type access, e.g. `ApiRouter['users']['show']['response']`. */
460
490
  responseType: string;
461
- /** Body type access, e.g. `ApiRouter['users']['create']['body']` (for mutation layers). */
462
- bodyType: string;
463
491
  /** Stable query-key expression, e.g. `["users.show", input] as const`. */
464
492
  queryKeyExpr: string;
465
493
  }
466
494
  /**
467
- * Per-leaf model passed through the api.ts pipeline: transportlayermember
468
- * contributors → render. `requestExpr` is set by the transport; `members`, when present,
469
- * flips the leaf from a bare callable to a handle.
495
+ * Per-leaf model passed through the api.ts pipeline: layermember contributors render.
496
+ * `requestExpr` is the host's neutral fetcher request; `members`, when present, flips the
497
+ * leaf from a bare callable to a handle.
470
498
  */
471
499
  interface LeafModel {
472
500
  route: RouteDescriptor;
473
501
  request: RequestModel;
474
- /** The expression that issues the request (set by the transport, default = fetcher). */
502
+ /** The expression that issues the request (the host's neutral fetcher call). */
475
503
  requestExpr: string;
476
504
  /** When present, the leaf renders as a handle exposing these members (ordered). */
477
505
  members?: Record<string, string>;
478
506
  }
479
507
  /**
480
- * Top-level `api.ts` imports + helpers a transport or layer depends on. Functions of the
481
- * context so they can be route-aware (e.g. only import `mutationOptions` when a mutation
482
- * exists). Imports are deduped by the host across all extensions.
508
+ * Top-level `api.ts` imports a client layer depends on. A function of the context so it can
509
+ * be route-aware (e.g. only import `mutationOptions` when a mutation exists). Imports are
510
+ * deduped by the host across all extensions.
483
511
  */
484
512
  interface ApiModuleDeps {
485
513
  /** Raw import lines (e.g. `import { queryOptions as _q } from '@tanstack/react-query';`). */
486
514
  imports?(ctx: ExtensionContext): string[];
487
- /** Module-level helper declarations the rendered expressions depend on. */
488
- helpers?(ctx: ExtensionContext): string[];
489
- }
490
- /** Single-slot: decides how an endpoint issues its request. */
491
- interface ApiTransport extends ApiModuleDeps {
492
- name: string;
493
- /** Render the expression that issues this endpoint's request (e.g. `fetcher.get<Res>(url, opts)`). */
494
- renderRequest(leaf: LeafModel, ctx: ExtensionContext): string;
495
515
  }
496
516
  /** Single-slot: decides what a leaf returns (the handle members). */
497
517
  interface ApiClientLayer extends ApiModuleDeps {
@@ -503,7 +523,30 @@ interface ApiClientLayer extends ApiModuleDeps {
503
523
  */
504
524
  buildMembers(requestExpr: string, leaf: LeafModel, ctx: ExtensionContext): Record<string, string>;
505
525
  }
526
+ /**
527
+ * The four request-shape flags derived from a route's method + contract. Computed in ONE
528
+ * place ({@link requestShape}) and read by both the host emitter and client-layer
529
+ * extensions, so the "filter-search POST counts as a read" rule is encoded exactly once.
530
+ */
531
+ interface RequestShape {
532
+ /** The route is a `GET`. */
533
+ isGet: boolean;
534
+ /** True for reads: a GET, or a filter-search route (carries `filterFields`) even when POST.
535
+ * Client layers use this (not `isGet`) to decide query vs mutation helpers. */
536
+ isQuery: boolean;
537
+ /** The route carries a body contract (a mutation payload). */
538
+ hasBody: boolean;
539
+ /** The route can take a query string — always for GET; a mutation may too (query + body). */
540
+ hasQuery: boolean;
541
+ }
542
+ /**
543
+ * Compute the {@link RequestShape} flags for a route from its method and contract. This is
544
+ * the SINGLE source of truth for these flags — `buildRequestModel`, the TanStack layer's
545
+ * `imports()`, and any other reader must call this rather than re-deriving. The
546
+ * "filter-search POST counts as a read" rule lives here and nowhere else.
547
+ */
548
+ declare function requestShape(route: RouteDescriptor): RequestShape;
506
549
  /** Identity helper for authoring extensions with full type inference. */
507
550
  declare function defineExtension(ext: CodegenExtension): CodegenExtension;
508
551
 
509
- export { type AdapterUsage as A, type CodegenExtension as C, type ExtensionContext as E, type LeafModel as L, type NumberCheck as N, type ResolvedConfig as R, type SchemaModule as S, type TypeRef as T, type UserConfig as U, type ValidationAdapter as V, type RouteDescriptor as a, type ResolvedFormsConfig as b, type ContractDescriptor as c, type ContractSource as d, type ControllerRef as e, type RenderContext as f, type RenderedModule as g, type SchemaNode as h, type ScopeConfig as i, type StringCheck as j, type ValidationOption as k, type ApiClientLayer as l, type ApiHeaderContribution as m, type ApiModuleDeps as n, type ApiTransport as o, type EmittedFile as p, type RequestModel as q, resolveAdapter as r, defineExtension as s };
552
+ export { type AdapterUsage as A, type CodegenExtension as C, type ExtensionContext as E, type LeafModel as L, type NumberCheck as N, type ResolvedConfig as R, type SchemaNode as S, type TypeRef as T, type UserConfig as U, type ValidationAdapter as V, type RouteDescriptor as a, type SchemaModule as b, type ResolvedFormsConfig as c, type ContractDescriptor as d, type ContractSource as e, type ControllerRef as f, type RenderContext as g, type RenderedModule as h, type ScopeConfig as i, type StringCheck as j, type ValidationOption as k, type ApiClientLayer as l, type ApiHeaderContribution as m, type ApiModuleDeps as n, type EmittedFile as o, type RequestModel as p, type RequestShape as q, resolveAdapter as r, defineExtension as s, requestShape as t };
@@ -111,6 +111,14 @@ interface SchemaModule {
111
111
  root: SchemaNode;
112
112
  named: Map<string, SchemaNode>;
113
113
  warnings: string[];
114
+ /**
115
+ * Names (keys of {@link named}) that are genuinely self/mutually recursive,
116
+ * i.e. reachable from themselves through a `lazyRef` back-edge. Adapters use
117
+ * this to break the TypeScript inference cycle (annotated const + hoisted
118
+ * structural type for zod/valibot; `this`/degrade for arktype). Absent or
119
+ * empty means no recursion.
120
+ */
121
+ recursive?: Set<string>;
114
122
  }
115
123
 
116
124
  /** Signals to an adapter what it must import to render the current output. */
@@ -121,12 +129,32 @@ interface AdapterUsage {
121
129
  interface RenderContext {
122
130
  /** Hoisted named schemas being emitted alongside the root. */
123
131
  named: Map<string, SchemaNode>;
132
+ /**
133
+ * Name of the hoisted schema currently being rendered, when rendering a named
134
+ * schema's own body. Lets an adapter express a self-reference specially (e.g.
135
+ * arktype's `this` keyword). Absent when rendering the root.
136
+ */
137
+ selfName?: string;
124
138
  }
125
139
  interface RenderedModule {
126
140
  /** Root schema source text, e.g. `"z.object({ email: z.string().email() })"`. */
127
141
  schemaText: string;
128
142
  /** name → schema source text, hoisted above the parent. */
129
143
  namedNestedSchemas: Map<string, string>;
144
+ /**
145
+ * name → hoisted TS `type` alias *body* for a recursive schema (e.g.
146
+ * `"{ field?: string; and?: Array<ColumnFilter> }"`), emitted as
147
+ * `type <TypeName> = <body>;` above the const. The alias name is derived from
148
+ * the schema name. Absent for adapters/schemas that don't need it (arktype,
149
+ * non-recursive schemas).
150
+ */
151
+ namedTypeAliases?: Map<string, string>;
152
+ /**
153
+ * name → const type annotation for a recursive schema (e.g.
154
+ * `"z.ZodType<ColumnFilter>"`), emitted as `const <name>: <annotation> = ...`
155
+ * to break the implicit-any self-reference cycle.
156
+ */
157
+ namedAnnotations?: Map<string, string>;
130
158
  warnings: string[];
131
159
  }
132
160
  /**
@@ -137,6 +165,12 @@ interface RenderedModule {
137
165
  interface ValidationAdapter {
138
166
  /** 'zod' | 'valibot' | 'arktype'. */
139
167
  name: string;
168
+ /**
169
+ * When `true`, the emitter may pass raw zod source from `defineContract`
170
+ * (`bodyZodText`/`bodyZodRef`) through verbatim and emit a `z` import. Only the
171
+ * zod adapter sets this; any other adapter skips raw-zod sources with a warning.
172
+ */
173
+ acceptsRawZodSource?: boolean;
140
174
  /** Import lines required for any rendered text (e.g. `import { z } from 'zod'`). */
141
175
  importStatements(usage: AdapterUsage): string[];
142
176
  /** Render a single node to this lib's source text. */
@@ -150,14 +184,14 @@ interface ValidationAdapter {
150
184
  /** A built-in adapter name or a custom adapter instance. */
151
185
  type ValidationOption = 'zod' | 'valibot' | 'arktype' | ValidationAdapter;
152
186
  /**
153
- * Resolve a `validation` config value to a {@link ValidationAdapter}. `'zod'` is
154
- * bundled in core; the valibot/arktype adapters ship as their own packages — import
155
- * the adapter instance and pass it directly (it passes through here). A custom
156
- * adapter object also passes through.
187
+ * Resolve a `validation` config value to a {@link ValidationAdapter}. No adapter is
188
+ * bundled in core the zod/valibot/arktype adapters ship as their own packages.
189
+ * Import the adapter instance and pass it directly (it passes through here). A
190
+ * custom adapter object also passes through.
157
191
  *
158
192
  * @example
159
- * import { valibotAdapter } from '@dudousxd/nestjs-codegen-valibot';
160
- * defineConfig({ validation: valibotAdapter });
193
+ * import { zodAdapter } from '@dudousxd/nestjs-codegen-zod';
194
+ * defineConfig({ validation: zodAdapter });
161
195
  */
162
196
  declare function resolveAdapter(option: ValidationOption): ValidationAdapter;
163
197
 
@@ -166,15 +200,15 @@ interface UserConfig {
166
200
  * Codegen extensions, applied in order. Each may augment the route IR
167
201
  * (`transformRoutes`), contribute extra output files (`emitFiles`), and — once a
168
202
  * client layer is active — shape `api.ts`. Registered explicitly, e.g.
169
- * `extensions: [nestjsInertiaCodegen(), tanstackQuery()]`.
203
+ * `extensions: [clientCodegen(), tanstackQuery()]`.
170
204
  */
171
205
  extensions?: CodegenExtension[];
172
206
  /**
173
- * Validation library for emitted `forms.ts` schemas. `'zod'` (bundled, default)
174
- * or an imported adapter instance (`valibotAdapter`/`arktypeAdapter`).
175
- * @default 'zod'
207
+ * Validation library for emitted `forms.ts` schemas. Required pass an imported
208
+ * adapter instance, e.g. `zodAdapter` from `@dudousxd/nestjs-codegen-zod`, or
209
+ * `valibotAdapter`/`arktypeAdapter` from their packages.
176
210
  */
177
- validation?: ValidationOption;
211
+ validation: ValidationOption;
178
212
  /** Inertia page discovery. Omit when you don't use Inertia. */
179
213
  pages?: {
180
214
  glob: string;
@@ -202,11 +236,11 @@ interface UserConfig {
202
236
  * This lets users configure baseUrl, headers, plugins (e.g. superjson).
203
237
  *
204
238
  * @example
205
- * // nestjs-inertia.config.ts
239
+ * // nestjs-codegen.config.ts
206
240
  * fetcher: { importPath: '~/lib/api' }
207
241
  *
208
- * // inertia/lib/api.ts
209
- * import { createFetcher } from '@dudousxd/nestjs-inertia-client';
242
+ * // src/lib/api.ts
243
+ * import { createFetcher } from '@dudousxd/nestjs-client';
210
244
  * export const fetcher = createFetcher({ baseUrl: '/api' });
211
245
  */
212
246
  fetcher?: {
@@ -402,15 +436,11 @@ interface CodegenExtension {
402
436
  */
403
437
  apiMembers?(leaf: LeafModel, ctx: ExtensionContext): Record<string, string> | undefined;
404
438
  /**
405
- * Claims **how** a single endpoint issues its request. When unset by every extension,
406
- * the host falls back to the neutral fetcher transport. Example: the Inertia extension
407
- * routes mutations through the Inertia router while GETs stay fetcher-typed.
408
- */
409
- apiTransport?: ApiTransport;
410
- /**
411
- * Claims **what** a leaf returns. When unset, a leaf is a bare callable returning a
412
- * `Promise`. Example: the TanStack extension wraps each leaf into a handle exposing
413
- * `{ fetch, queryKey, queryOptions | mutationOptions }`.
439
+ * Claims **what** a leaf returns and **how** it issues its request. At most one extension
440
+ * may claim it. When unset, a leaf is a bare awaitable callable backed by the neutral
441
+ * fetcher. Example: the TanStack extension wraps each leaf into a handle exposing
442
+ * `{ fetch, queryKey, queryOptions | mutationOptions }`, composing with the fetcher
443
+ * request the host passes in.
414
444
  */
415
445
  apiClientLayer?: ApiClientLayer;
416
446
  }
@@ -458,40 +488,30 @@ interface RequestModel {
458
488
  optsExpr: string;
459
489
  /** Response type access, e.g. `ApiRouter['users']['show']['response']`. */
460
490
  responseType: string;
461
- /** Body type access, e.g. `ApiRouter['users']['create']['body']` (for mutation layers). */
462
- bodyType: string;
463
491
  /** Stable query-key expression, e.g. `["users.show", input] as const`. */
464
492
  queryKeyExpr: string;
465
493
  }
466
494
  /**
467
- * Per-leaf model passed through the api.ts pipeline: transportlayermember
468
- * contributors → render. `requestExpr` is set by the transport; `members`, when present,
469
- * flips the leaf from a bare callable to a handle.
495
+ * Per-leaf model passed through the api.ts pipeline: layermember contributors render.
496
+ * `requestExpr` is the host's neutral fetcher request; `members`, when present, flips the
497
+ * leaf from a bare callable to a handle.
470
498
  */
471
499
  interface LeafModel {
472
500
  route: RouteDescriptor;
473
501
  request: RequestModel;
474
- /** The expression that issues the request (set by the transport, default = fetcher). */
502
+ /** The expression that issues the request (the host's neutral fetcher call). */
475
503
  requestExpr: string;
476
504
  /** When present, the leaf renders as a handle exposing these members (ordered). */
477
505
  members?: Record<string, string>;
478
506
  }
479
507
  /**
480
- * Top-level `api.ts` imports + helpers a transport or layer depends on. Functions of the
481
- * context so they can be route-aware (e.g. only import `mutationOptions` when a mutation
482
- * exists). Imports are deduped by the host across all extensions.
508
+ * Top-level `api.ts` imports a client layer depends on. A function of the context so it can
509
+ * be route-aware (e.g. only import `mutationOptions` when a mutation exists). Imports are
510
+ * deduped by the host across all extensions.
483
511
  */
484
512
  interface ApiModuleDeps {
485
513
  /** Raw import lines (e.g. `import { queryOptions as _q } from '@tanstack/react-query';`). */
486
514
  imports?(ctx: ExtensionContext): string[];
487
- /** Module-level helper declarations the rendered expressions depend on. */
488
- helpers?(ctx: ExtensionContext): string[];
489
- }
490
- /** Single-slot: decides how an endpoint issues its request. */
491
- interface ApiTransport extends ApiModuleDeps {
492
- name: string;
493
- /** Render the expression that issues this endpoint's request (e.g. `fetcher.get<Res>(url, opts)`). */
494
- renderRequest(leaf: LeafModel, ctx: ExtensionContext): string;
495
515
  }
496
516
  /** Single-slot: decides what a leaf returns (the handle members). */
497
517
  interface ApiClientLayer extends ApiModuleDeps {
@@ -503,7 +523,30 @@ interface ApiClientLayer extends ApiModuleDeps {
503
523
  */
504
524
  buildMembers(requestExpr: string, leaf: LeafModel, ctx: ExtensionContext): Record<string, string>;
505
525
  }
526
+ /**
527
+ * The four request-shape flags derived from a route's method + contract. Computed in ONE
528
+ * place ({@link requestShape}) and read by both the host emitter and client-layer
529
+ * extensions, so the "filter-search POST counts as a read" rule is encoded exactly once.
530
+ */
531
+ interface RequestShape {
532
+ /** The route is a `GET`. */
533
+ isGet: boolean;
534
+ /** True for reads: a GET, or a filter-search route (carries `filterFields`) even when POST.
535
+ * Client layers use this (not `isGet`) to decide query vs mutation helpers. */
536
+ isQuery: boolean;
537
+ /** The route carries a body contract (a mutation payload). */
538
+ hasBody: boolean;
539
+ /** The route can take a query string — always for GET; a mutation may too (query + body). */
540
+ hasQuery: boolean;
541
+ }
542
+ /**
543
+ * Compute the {@link RequestShape} flags for a route from its method and contract. This is
544
+ * the SINGLE source of truth for these flags — `buildRequestModel`, the TanStack layer's
545
+ * `imports()`, and any other reader must call this rather than re-deriving. The
546
+ * "filter-search POST counts as a read" rule lives here and nowhere else.
547
+ */
548
+ declare function requestShape(route: RouteDescriptor): RequestShape;
506
549
  /** Identity helper for authoring extensions with full type inference. */
507
550
  declare function defineExtension(ext: CodegenExtension): CodegenExtension;
508
551
 
509
- export { type AdapterUsage as A, type CodegenExtension as C, type ExtensionContext as E, type LeafModel as L, type NumberCheck as N, type ResolvedConfig as R, type SchemaModule as S, type TypeRef as T, type UserConfig as U, type ValidationAdapter as V, type RouteDescriptor as a, type ResolvedFormsConfig as b, type ContractDescriptor as c, type ContractSource as d, type ControllerRef as e, type RenderContext as f, type RenderedModule as g, type SchemaNode as h, type ScopeConfig as i, type StringCheck as j, type ValidationOption as k, type ApiClientLayer as l, type ApiHeaderContribution as m, type ApiModuleDeps as n, type ApiTransport as o, type EmittedFile as p, type RequestModel as q, resolveAdapter as r, defineExtension as s };
552
+ export { type AdapterUsage as A, type CodegenExtension as C, type ExtensionContext as E, type LeafModel as L, type NumberCheck as N, type ResolvedConfig as R, type SchemaNode as S, type TypeRef as T, type UserConfig as U, type ValidationAdapter as V, type RouteDescriptor as a, type SchemaModule as b, type ResolvedFormsConfig as c, type ContractDescriptor as d, type ContractSource as e, type ControllerRef as f, type RenderContext as g, type RenderedModule as h, type ScopeConfig as i, type StringCheck as j, type ValidationOption as k, type ApiClientLayer as l, type ApiHeaderContribution as m, type ApiModuleDeps as n, type EmittedFile as o, type RequestModel as p, type RequestShape as q, resolveAdapter as r, defineExtension as s, requestShape as t };