@dudousxd/nestjs-codegen 0.16.0 → 0.17.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.
@@ -1 +1 @@
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, a filter-search route (has `filterFields`), or an\n * `@AsQuery()`-marked route — even when the method is POST. Client layers\n * 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 * Runtime filter-fields literal-array expression, e.g. `[\"id\", \"name\"] as const`,\n * or `undefined` for a route with no filter. Derived from the same discovered\n * field list as the type-level `filterFields` union, so the emitted value stays\n * in lockstep with the type.\n */\n filterFieldsExpr?: string | undefined;\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 // A route counts as a read when it's a GET, a filter-search POST (carries\n // `filterFields`), or explicitly opted in via `@AsQuery()` (`cs.asQuery`) —\n // e.g. a POST whose semantics are a read (a query-shaped payload).\n const isQuery = isGet || !!cs?.filterFields?.length || !!cs?.asQuery;\n // `multipart` implies a body even when the route has no `@Body()` at all (a bare\n // `@UploadedFile()` route): the type block intersects `multipartBody` into the body\n // type, so the runtime leaf must accept and forward it too — otherwise the ApiRouter\n // type promises `body: { file: File | Blob }` while the generated call silently\n // drops the file.\n const hasBody = !!cs?.bodyRef || (cs?.body != null && cs.body !== 'never') || !!cs?.multipart;\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;;;ACyLO,SAAS,aAAa,OAAsC;AACjE,QAAM,KAAK,MAAM,UAAU;AAC3B,QAAM,QAAQ,MAAM,OAAO,YAAY,MAAM;AAI7C,QAAM,UAAU,SAAS,CAAC,CAAC,IAAI,cAAc,UAAU,CAAC,CAAC,IAAI;AAM7D,QAAM,UAAU,CAAC,CAAC,IAAI,WAAY,IAAI,QAAQ,QAAQ,GAAG,SAAS,WAAY,CAAC,CAAC,IAAI;AACpF,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
+ {"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 * Declare source files this extension read that the host's own input globs\n * (`contracts.glob`, `forms.watch`, `pages.glob`) do NOT cover, so they take\n * part in the skip-when-unchanged check.\n *\n * The freshness hash is computed from the globbed inputs alone. An extension\n * that reaches outside them — the filter extension resolves each route's\n * `@ApplyFilter(FilterClass)` target and reads its `@Filterable`/`@Computed`\n * declarations — otherwise produces output that nothing invalidates: editing\n * that filter class leaves the hash untouched and the next run reports \"up\n * to date, skipped\" while serving stale types.\n *\n * Declaring them up front is not possible for such an extension (it needs\n * the discovered routes to know which files to read), so this works like a\n * compiler depfile instead: paths tracked during a run are recorded in the\n * manifest, and the NEXT run folds their contents into the hash. A file that\n * becomes a dependency for the first time is therefore picked up on the run\n * after it is first read — in practice not a gap, since wiring a new filter\n * class also edits a controller, which the globs already cover.\n *\n * Absolute or cwd-relative paths; both are normalized. Safe to call\n * repeatedly with the same path.\n */\n trackInput(...paths: string[]): void;\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, a filter-search route (has `filterFields`), or an\n * `@AsQuery()`-marked route — even when the method is POST. Client layers\n * 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 * Runtime filter-fields literal-array expression, e.g. `[\"id\", \"name\"] as const`,\n * or `undefined` for a route with no filter. Derived from the same discovered\n * field list as the type-level `filterFields` union, so the emitted value stays\n * in lockstep with the type.\n */\n filterFieldsExpr?: string | undefined;\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 // A route counts as a read when it's a GET, a filter-search POST (carries\n // `filterFields`), or explicitly opted in via `@AsQuery()` (`cs.asQuery`) —\n // e.g. a POST whose semantics are a read (a query-shaped payload).\n const isQuery = isGet || !!cs?.filterFields?.length || !!cs?.asQuery;\n // `multipart` implies a body even when the route has no `@Body()` at all (a bare\n // `@UploadedFile()` route): the type block intersects `multipartBody` into the body\n // type, so the runtime leaf must accept and forward it too — otherwise the ApiRouter\n // type promises `body: { file: File | Blob }` while the generated call silently\n // drops the file.\n const hasBody = !!cs?.bodyRef || (cs?.body != null && cs.body !== 'never') || !!cs?.multipart;\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;;;ACiNO,SAAS,aAAa,OAAsC;AACjE,QAAM,KAAK,MAAM,UAAU;AAC3B,QAAM,QAAQ,MAAM,OAAO,YAAY,MAAM;AAI7C,QAAM,UAAU,SAAS,CAAC,CAAC,IAAI,cAAc,UAAU,CAAC,CAAC,IAAI;AAM7D,QAAM,UAAU,CAAC,CAAC,IAAI,WAAY,IAAI,QAAQ,QAAQ,GAAG,SAAS,WAAY,CAAC,CAAC,IAAI;AACpF,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 { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-LQNP7Ms3.cjs';
1
+ export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-DCF9QSPY.cjs';
2
2
  import 'ts-morph';
@@ -1,2 +1,2 @@
1
- export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-LQNP7Ms3.js';
1
+ export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-DCF9QSPY.js';
2
2
  import 'ts-morph';
@@ -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 **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, a filter-search route (has `filterFields`), or an\n * `@AsQuery()`-marked route — even when the method is POST. Client layers\n * 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 * Runtime filter-fields literal-array expression, e.g. `[\"id\", \"name\"] as const`,\n * or `undefined` for a route with no filter. Derived from the same discovered\n * field list as the type-level `filterFields` union, so the emitted value stays\n * in lockstep with the type.\n */\n filterFieldsExpr?: string | undefined;\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 // A route counts as a read when it's a GET, a filter-search POST (carries\n // `filterFields`), or explicitly opted in via `@AsQuery()` (`cs.asQuery`) —\n // e.g. a POST whose semantics are a read (a query-shaped payload).\n const isQuery = isGet || !!cs?.filterFields?.length || !!cs?.asQuery;\n // `multipart` implies a body even when the route has no `@Body()` at all (a bare\n // `@UploadedFile()` route): the type block intersects `multipartBody` into the body\n // type, so the runtime leaf must accept and forward it too — otherwise the ApiRouter\n // type promises `body: { file: File | Blob }` while the generated call silently\n // drops the file.\n const hasBody = !!cs?.bodyRef || (cs?.body != null && cs.body !== 'never') || !!cs?.multipart;\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":";AAyLO,SAAS,aAAa,OAAsC;AACjE,QAAM,KAAK,MAAM,UAAU;AAC3B,QAAM,QAAQ,MAAM,OAAO,YAAY,MAAM;AAI7C,QAAM,UAAU,SAAS,CAAC,CAAC,IAAI,cAAc,UAAU,CAAC,CAAC,IAAI;AAM7D,QAAM,UAAU,CAAC,CAAC,IAAI,WAAY,IAAI,QAAQ,QAAQ,GAAG,SAAS,WAAY,CAAC,CAAC,IAAI;AACpF,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
+ {"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 * Declare source files this extension read that the host's own input globs\n * (`contracts.glob`, `forms.watch`, `pages.glob`) do NOT cover, so they take\n * part in the skip-when-unchanged check.\n *\n * The freshness hash is computed from the globbed inputs alone. An extension\n * that reaches outside them — the filter extension resolves each route's\n * `@ApplyFilter(FilterClass)` target and reads its `@Filterable`/`@Computed`\n * declarations — otherwise produces output that nothing invalidates: editing\n * that filter class leaves the hash untouched and the next run reports \"up\n * to date, skipped\" while serving stale types.\n *\n * Declaring them up front is not possible for such an extension (it needs\n * the discovered routes to know which files to read), so this works like a\n * compiler depfile instead: paths tracked during a run are recorded in the\n * manifest, and the NEXT run folds their contents into the hash. A file that\n * becomes a dependency for the first time is therefore picked up on the run\n * after it is first read — in practice not a gap, since wiring a new filter\n * class also edits a controller, which the globs already cover.\n *\n * Absolute or cwd-relative paths; both are normalized. Safe to call\n * repeatedly with the same path.\n */\n trackInput(...paths: string[]): void;\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, a filter-search route (has `filterFields`), or an\n * `@AsQuery()`-marked route — even when the method is POST. Client layers\n * 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 * Runtime filter-fields literal-array expression, e.g. `[\"id\", \"name\"] as const`,\n * or `undefined` for a route with no filter. Derived from the same discovered\n * field list as the type-level `filterFields` union, so the emitted value stays\n * in lockstep with the type.\n */\n filterFieldsExpr?: string | undefined;\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 // A route counts as a read when it's a GET, a filter-search POST (carries\n // `filterFields`), or explicitly opted in via `@AsQuery()` (`cs.asQuery`) —\n // e.g. a POST whose semantics are a read (a query-shaped payload).\n const isQuery = isGet || !!cs?.filterFields?.length || !!cs?.asQuery;\n // `multipart` implies a body even when the route has no `@Body()` at all (a bare\n // `@UploadedFile()` route): the type block intersects `multipartBody` into the body\n // type, so the runtime leaf must accept and forward it too — otherwise the ApiRouter\n // type promises `body: { file: File | Blob }` while the generated call silently\n // drops the file.\n const hasBody = !!cs?.bodyRef || (cs?.body != null && cs.body !== 'never') || !!cs?.multipart;\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":";AAiNO,SAAS,aAAa,OAAsC;AACjE,QAAM,KAAK,MAAM,UAAU;AAC3B,QAAM,QAAQ,MAAM,OAAO,YAAY,MAAM;AAI7C,QAAM,UAAU,SAAS,CAAC,CAAC,IAAI,cAAc,UAAU,CAAC,CAAC,IAAI;AAM7D,QAAM,UAAU,CAAC,CAAC,IAAI,WAAY,IAAI,QAAQ,QAAQ,GAAG,SAAS,WAAY,CAAC,CAAC,IAAI;AACpF,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":[]}
@@ -639,6 +639,30 @@ interface ExtensionContext {
639
639
  config: ResolvedConfig;
640
640
  /** Lazily-created shared ts-morph Project for AST work (pages, custom decorators). */
641
641
  project(): Project;
642
+ /**
643
+ * Declare source files this extension read that the host's own input globs
644
+ * (`contracts.glob`, `forms.watch`, `pages.glob`) do NOT cover, so they take
645
+ * part in the skip-when-unchanged check.
646
+ *
647
+ * The freshness hash is computed from the globbed inputs alone. An extension
648
+ * that reaches outside them — the filter extension resolves each route's
649
+ * `@ApplyFilter(FilterClass)` target and reads its `@Filterable`/`@Computed`
650
+ * declarations — otherwise produces output that nothing invalidates: editing
651
+ * that filter class leaves the hash untouched and the next run reports "up
652
+ * to date, skipped" while serving stale types.
653
+ *
654
+ * Declaring them up front is not possible for such an extension (it needs
655
+ * the discovered routes to know which files to read), so this works like a
656
+ * compiler depfile instead: paths tracked during a run are recorded in the
657
+ * manifest, and the NEXT run folds their contents into the hash. A file that
658
+ * becomes a dependency for the first time is therefore picked up on the run
659
+ * after it is first read — in practice not a gap, since wiring a new filter
660
+ * class also edits a controller, which the globs already cover.
661
+ *
662
+ * Absolute or cwd-relative paths; both are normalized. Safe to call
663
+ * repeatedly with the same path.
664
+ */
665
+ trackInput(...paths: string[]): void;
642
666
  }
643
667
  /** A file contributed by an extension's `emitFiles` hook. */
644
668
  interface EmittedFile {
@@ -639,6 +639,30 @@ interface ExtensionContext {
639
639
  config: ResolvedConfig;
640
640
  /** Lazily-created shared ts-morph Project for AST work (pages, custom decorators). */
641
641
  project(): Project;
642
+ /**
643
+ * Declare source files this extension read that the host's own input globs
644
+ * (`contracts.glob`, `forms.watch`, `pages.glob`) do NOT cover, so they take
645
+ * part in the skip-when-unchanged check.
646
+ *
647
+ * The freshness hash is computed from the globbed inputs alone. An extension
648
+ * that reaches outside them — the filter extension resolves each route's
649
+ * `@ApplyFilter(FilterClass)` target and reads its `@Filterable`/`@Computed`
650
+ * declarations — otherwise produces output that nothing invalidates: editing
651
+ * that filter class leaves the hash untouched and the next run reports "up
652
+ * to date, skipped" while serving stale types.
653
+ *
654
+ * Declaring them up front is not possible for such an extension (it needs
655
+ * the discovered routes to know which files to read), so this works like a
656
+ * compiler depfile instead: paths tracked during a run are recorded in the
657
+ * manifest, and the NEXT run folds their contents into the hash. A file that
658
+ * becomes a dependency for the first time is therefore picked up on the run
659
+ * after it is first read — in practice not a gap, since wiring a new filter
660
+ * class also edits a controller, which the globs already cover.
661
+ *
662
+ * Absolute or cwd-relative paths; both are normalized. Safe to call
663
+ * repeatedly with the same path.
664
+ */
665
+ trackInput(...paths: string[]): void;
642
666
  }
643
667
  /** A file contributed by an extension's `emitFiles` hook. */
644
668
  interface EmittedFile {