@dudousxd/nestjs-codegen 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { U as UserConfig, R as ResolvedConfig, a as RouteDescriptor, S as SchemaNode, b as RenderContext, c as SchemaModule, d as RenderedModule, e as ResolvedFormsConfig, V as ValidationAdapter, C as CodegenExtension, E as ExtensionContext, f as SerializationMode } from './index-D8RIMVpU.cjs';
2
- export { A as AdapterUsage, g as ContractDescriptor, h as ContractSource, i as ControllerRef, N as NumberCheck, j as ScopeConfig, k as StringCheck, T as TypeRef, l as ValidationOption, r as resolveAdapter } from './index-D8RIMVpU.cjs';
1
+ import { U as UserConfig, R as ResolvedConfig, a as RouteDescriptor, S as SchemaNode, b as RenderContext, c as SchemaModule, d as RenderedModule, e as ResolvedFormsConfig, V as ValidationAdapter, C as CodegenExtension, E as ExtensionContext, f as SerializationMode } from './index-DT8SgPxp.cjs';
2
+ export { A as AdapterUsage, g as ContractDescriptor, h as ContractSource, i as ControllerRef, N as NumberCheck, j as ScopeConfig, k as StringCheck, T as TypeRef, l as ValidationOption, r as resolveAdapter } from './index-DT8SgPxp.cjs';
3
3
  import { ClassDeclaration, SourceFile, Project } from 'ts-morph';
4
4
 
5
5
  declare function defineConfig(c: UserConfig): UserConfig;
@@ -31,6 +31,52 @@ declare class CodegenError extends Error {
31
31
  constructor(message: string, options?: ErrorOptions);
32
32
  }
33
33
 
34
+ /**
35
+ * Which entry point produced a generate pass: the one-shot/`--watch` CLI
36
+ * (reads `nestjs-codegen.config.ts` from disk), or the Nest module
37
+ * (`NestjsCodegenModule.forRoot()`, options passed in-process). Recorded in
38
+ * the manifest so {@link CodegenManifest.configHash} drift between the two can
39
+ * be detected — see {@link DriftGuardError}.
40
+ */
41
+ type EntryPoint = 'cli' | 'module';
42
+ /**
43
+ * Thrown by `generate()` when the drift guard (see `driftGuard` in
44
+ * {@link import('./config/types.js').UserConfig}) detects that the CLI and the
45
+ * Nest module are writing the same `outDir` from two different resolved
46
+ * configs. Distinguished from a generic `Error` so callers (the watcher's
47
+ * initial-pass fallback) can single it out instead of treating it as a
48
+ * transient discovery failure to retry.
49
+ */
50
+ declare class DriftGuardError extends Error {
51
+ constructor(message: string);
52
+ }
53
+ /**
54
+ * Persisted record of the last successful generate, written to
55
+ * `<outDir>/.codegen-manifest.json`. Used to skip regeneration when nothing
56
+ * relevant changed (see {@link isManifestFresh}), and to detect CLI↔module
57
+ * config drift (see {@link DriftGuardError}).
58
+ */
59
+ interface CodegenManifest {
60
+ /** Lib version that produced the output. A lib upgrade invalidates the manifest. */
61
+ version: string;
62
+ /** Content hash over all generate inputs (source files + resolved config + version). */
63
+ hash: string;
64
+ /**
65
+ * Which entry point produced this manifest. Absent on manifests written before
66
+ * this field existed — treated as "unknown", which never trips the drift guard.
67
+ */
68
+ entryPoint?: EntryPoint;
69
+ /**
70
+ * Hash of ONLY the serialized resolved config (not source files / version) —
71
+ * a narrower signal than {@link hash}, used to tell "same config, different
72
+ * entry point" (fine) apart from "different config" (drift) regardless of
73
+ * unrelated source-file changes. Absent on pre-drift-guard manifests.
74
+ */
75
+ configHash?: string;
76
+ /** Generated output files, relative to `outDir`, recorded after the last run. */
77
+ files: string[];
78
+ }
79
+
34
80
  /**
35
81
  * Run one full codegen pass: discover pages, emit pages.d.ts, components.json, index.d.ts.
36
82
  * Route discovery is deliberately skipped — it requires spawning a Nest app and is
@@ -39,8 +85,11 @@ declare class CodegenError extends Error {
39
85
  * Optionally accepts pre-discovered routes (e.g. from a full generate + route-discovery pass).
40
86
  * When routes are present, emits routes.ts.
41
87
  * When routes with contracts are present, also emits api.ts.
88
+ *
89
+ * `entryPoint` identifies which caller is running — the CLI or the Nest module — and is
90
+ * recorded in the manifest for the {@link DriftGuardError} check below.
42
91
  */
43
- declare function generate(config: ResolvedConfig, inputRoutes?: RouteDescriptor[]): Promise<void>;
92
+ declare function generate(config: ResolvedConfig, inputRoutes?: RouteDescriptor[], entryPoint?: EntryPoint): Promise<void>;
44
93
 
45
94
  interface Watcher {
46
95
  close(): Promise<void>;
@@ -56,6 +105,16 @@ interface WatchOptions {
56
105
  * @default false
57
106
  */
58
107
  deferInitialGenerate?: boolean;
108
+ /**
109
+ * Which entry point is running the watcher — threaded through to every
110
+ * `generate()` call so the drift guard (see `driftGuard` in
111
+ * {@link import('../config/types.js').UserConfig}) can tell the CLI and the
112
+ * Nest module apart. The one-shot/`--watch` CLI passes `'cli'` (the
113
+ * default); `NestjsCodegenModule` passes `'module'`.
114
+ *
115
+ * @default 'cli'
116
+ */
117
+ entryPoint?: EntryPoint;
59
118
  }
60
119
  /**
61
120
  * Start two chokidar watchers:
@@ -85,20 +144,6 @@ declare function acquireLock(outDir: string): Promise<{
85
144
  release: () => Promise<void>;
86
145
  } | null>;
87
146
 
88
- /**
89
- * Persisted record of the last successful generate, written to
90
- * `<outDir>/.codegen-manifest.json`. Used to skip regeneration when nothing
91
- * relevant changed (see {@link isManifestFresh}).
92
- */
93
- interface CodegenManifest {
94
- /** Lib version that produced the output. A lib upgrade invalidates the manifest. */
95
- version: string;
96
- /** Content hash over all generate inputs (source files + resolved config + version). */
97
- hash: string;
98
- /** Generated output files, relative to `outDir`, recorded after the last run. */
99
- files: string[];
100
- }
101
-
102
147
  /**
103
148
  * Renders the neutral {@link SchemaNode} IR to a TypeScript *type* expression
104
149
  * (not a validation-lib schema). Used to synthesize the hoisted structural type
@@ -322,6 +367,6 @@ interface FastDiscoveryOptions {
322
367
  }
323
368
  declare function discoverContractsFast(opts: FastDiscoveryOptions): Promise<RouteDescriptor[]>;
324
369
 
325
- declare const VERSION = "0.13.2";
370
+ declare const VERSION = "0.14.0";
326
371
 
327
- export { type ChainModuleRendererOptions, CodegenError, type CodegenManifest, ConfigError, type FastDiscoveryOptions, type JsonSchema, type MocksEmitOptions, type OpenApiDocument, type OpenApiEmitOptions, type OpenApiInfo, RenderContext, RenderedModule, ResolvedConfig, RouteDescriptor, SchemaModule, SchemaNode, type TsTypeContext, UserConfig, VERSION, ValidationAdapter, type WatchOptions, type Watcher, acquireLock, buildMocksFile, buildOpenApiSpec, createChainModuleRenderer, defineConfig, discoverContractsFast, emitApi, emitForms, emitMocks, emitOpenApi, emitRoutes, extractSchemaFromDto, generate, loadConfig, renderTsType, resolveConfig, schemaModuleToJsonSchema, schemaNodeToJsonSchema, toObjectKey, typeNameFor, watch };
372
+ export { type ChainModuleRendererOptions, CodegenError, type CodegenManifest, ConfigError, DriftGuardError, type EntryPoint, type FastDiscoveryOptions, type JsonSchema, type MocksEmitOptions, type OpenApiDocument, type OpenApiEmitOptions, type OpenApiInfo, RenderContext, RenderedModule, ResolvedConfig, RouteDescriptor, SchemaModule, SchemaNode, type TsTypeContext, UserConfig, VERSION, ValidationAdapter, type WatchOptions, type Watcher, acquireLock, buildMocksFile, buildOpenApiSpec, createChainModuleRenderer, defineConfig, discoverContractsFast, emitApi, emitForms, emitMocks, emitOpenApi, emitRoutes, extractSchemaFromDto, generate, loadConfig, renderTsType, resolveConfig, schemaModuleToJsonSchema, schemaNodeToJsonSchema, toObjectKey, typeNameFor, watch };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { U as UserConfig, R as ResolvedConfig, a as RouteDescriptor, S as SchemaNode, b as RenderContext, c as SchemaModule, d as RenderedModule, e as ResolvedFormsConfig, V as ValidationAdapter, C as CodegenExtension, E as ExtensionContext, f as SerializationMode } from './index-D8RIMVpU.js';
2
- export { A as AdapterUsage, g as ContractDescriptor, h as ContractSource, i as ControllerRef, N as NumberCheck, j as ScopeConfig, k as StringCheck, T as TypeRef, l as ValidationOption, r as resolveAdapter } from './index-D8RIMVpU.js';
1
+ import { U as UserConfig, R as ResolvedConfig, a as RouteDescriptor, S as SchemaNode, b as RenderContext, c as SchemaModule, d as RenderedModule, e as ResolvedFormsConfig, V as ValidationAdapter, C as CodegenExtension, E as ExtensionContext, f as SerializationMode } from './index-DT8SgPxp.js';
2
+ export { A as AdapterUsage, g as ContractDescriptor, h as ContractSource, i as ControllerRef, N as NumberCheck, j as ScopeConfig, k as StringCheck, T as TypeRef, l as ValidationOption, r as resolveAdapter } from './index-DT8SgPxp.js';
3
3
  import { ClassDeclaration, SourceFile, Project } from 'ts-morph';
4
4
 
5
5
  declare function defineConfig(c: UserConfig): UserConfig;
@@ -31,6 +31,52 @@ declare class CodegenError extends Error {
31
31
  constructor(message: string, options?: ErrorOptions);
32
32
  }
33
33
 
34
+ /**
35
+ * Which entry point produced a generate pass: the one-shot/`--watch` CLI
36
+ * (reads `nestjs-codegen.config.ts` from disk), or the Nest module
37
+ * (`NestjsCodegenModule.forRoot()`, options passed in-process). Recorded in
38
+ * the manifest so {@link CodegenManifest.configHash} drift between the two can
39
+ * be detected — see {@link DriftGuardError}.
40
+ */
41
+ type EntryPoint = 'cli' | 'module';
42
+ /**
43
+ * Thrown by `generate()` when the drift guard (see `driftGuard` in
44
+ * {@link import('./config/types.js').UserConfig}) detects that the CLI and the
45
+ * Nest module are writing the same `outDir` from two different resolved
46
+ * configs. Distinguished from a generic `Error` so callers (the watcher's
47
+ * initial-pass fallback) can single it out instead of treating it as a
48
+ * transient discovery failure to retry.
49
+ */
50
+ declare class DriftGuardError extends Error {
51
+ constructor(message: string);
52
+ }
53
+ /**
54
+ * Persisted record of the last successful generate, written to
55
+ * `<outDir>/.codegen-manifest.json`. Used to skip regeneration when nothing
56
+ * relevant changed (see {@link isManifestFresh}), and to detect CLI↔module
57
+ * config drift (see {@link DriftGuardError}).
58
+ */
59
+ interface CodegenManifest {
60
+ /** Lib version that produced the output. A lib upgrade invalidates the manifest. */
61
+ version: string;
62
+ /** Content hash over all generate inputs (source files + resolved config + version). */
63
+ hash: string;
64
+ /**
65
+ * Which entry point produced this manifest. Absent on manifests written before
66
+ * this field existed — treated as "unknown", which never trips the drift guard.
67
+ */
68
+ entryPoint?: EntryPoint;
69
+ /**
70
+ * Hash of ONLY the serialized resolved config (not source files / version) —
71
+ * a narrower signal than {@link hash}, used to tell "same config, different
72
+ * entry point" (fine) apart from "different config" (drift) regardless of
73
+ * unrelated source-file changes. Absent on pre-drift-guard manifests.
74
+ */
75
+ configHash?: string;
76
+ /** Generated output files, relative to `outDir`, recorded after the last run. */
77
+ files: string[];
78
+ }
79
+
34
80
  /**
35
81
  * Run one full codegen pass: discover pages, emit pages.d.ts, components.json, index.d.ts.
36
82
  * Route discovery is deliberately skipped — it requires spawning a Nest app and is
@@ -39,8 +85,11 @@ declare class CodegenError extends Error {
39
85
  * Optionally accepts pre-discovered routes (e.g. from a full generate + route-discovery pass).
40
86
  * When routes are present, emits routes.ts.
41
87
  * When routes with contracts are present, also emits api.ts.
88
+ *
89
+ * `entryPoint` identifies which caller is running — the CLI or the Nest module — and is
90
+ * recorded in the manifest for the {@link DriftGuardError} check below.
42
91
  */
43
- declare function generate(config: ResolvedConfig, inputRoutes?: RouteDescriptor[]): Promise<void>;
92
+ declare function generate(config: ResolvedConfig, inputRoutes?: RouteDescriptor[], entryPoint?: EntryPoint): Promise<void>;
44
93
 
45
94
  interface Watcher {
46
95
  close(): Promise<void>;
@@ -56,6 +105,16 @@ interface WatchOptions {
56
105
  * @default false
57
106
  */
58
107
  deferInitialGenerate?: boolean;
108
+ /**
109
+ * Which entry point is running the watcher — threaded through to every
110
+ * `generate()` call so the drift guard (see `driftGuard` in
111
+ * {@link import('../config/types.js').UserConfig}) can tell the CLI and the
112
+ * Nest module apart. The one-shot/`--watch` CLI passes `'cli'` (the
113
+ * default); `NestjsCodegenModule` passes `'module'`.
114
+ *
115
+ * @default 'cli'
116
+ */
117
+ entryPoint?: EntryPoint;
59
118
  }
60
119
  /**
61
120
  * Start two chokidar watchers:
@@ -85,20 +144,6 @@ declare function acquireLock(outDir: string): Promise<{
85
144
  release: () => Promise<void>;
86
145
  } | null>;
87
146
 
88
- /**
89
- * Persisted record of the last successful generate, written to
90
- * `<outDir>/.codegen-manifest.json`. Used to skip regeneration when nothing
91
- * relevant changed (see {@link isManifestFresh}).
92
- */
93
- interface CodegenManifest {
94
- /** Lib version that produced the output. A lib upgrade invalidates the manifest. */
95
- version: string;
96
- /** Content hash over all generate inputs (source files + resolved config + version). */
97
- hash: string;
98
- /** Generated output files, relative to `outDir`, recorded after the last run. */
99
- files: string[];
100
- }
101
-
102
147
  /**
103
148
  * Renders the neutral {@link SchemaNode} IR to a TypeScript *type* expression
104
149
  * (not a validation-lib schema). Used to synthesize the hoisted structural type
@@ -322,6 +367,6 @@ interface FastDiscoveryOptions {
322
367
  }
323
368
  declare function discoverContractsFast(opts: FastDiscoveryOptions): Promise<RouteDescriptor[]>;
324
369
 
325
- declare const VERSION = "0.13.2";
370
+ declare const VERSION = "0.14.0";
326
371
 
327
- export { type ChainModuleRendererOptions, CodegenError, type CodegenManifest, ConfigError, type FastDiscoveryOptions, type JsonSchema, type MocksEmitOptions, type OpenApiDocument, type OpenApiEmitOptions, type OpenApiInfo, RenderContext, RenderedModule, ResolvedConfig, RouteDescriptor, SchemaModule, SchemaNode, type TsTypeContext, UserConfig, VERSION, ValidationAdapter, type WatchOptions, type Watcher, acquireLock, buildMocksFile, buildOpenApiSpec, createChainModuleRenderer, defineConfig, discoverContractsFast, emitApi, emitForms, emitMocks, emitOpenApi, emitRoutes, extractSchemaFromDto, generate, loadConfig, renderTsType, resolveConfig, schemaModuleToJsonSchema, schemaNodeToJsonSchema, toObjectKey, typeNameFor, watch };
372
+ export { type ChainModuleRendererOptions, CodegenError, type CodegenManifest, ConfigError, DriftGuardError, type EntryPoint, type FastDiscoveryOptions, type JsonSchema, type MocksEmitOptions, type OpenApiDocument, type OpenApiEmitOptions, type OpenApiInfo, RenderContext, RenderedModule, ResolvedConfig, RouteDescriptor, SchemaModule, SchemaNode, type TsTypeContext, UserConfig, VERSION, ValidationAdapter, type WatchOptions, type Watcher, acquireLock, buildMocksFile, buildOpenApiSpec, createChainModuleRenderer, defineConfig, discoverContractsFast, emitApi, emitForms, emitMocks, emitOpenApi, emitRoutes, extractSchemaFromDto, generate, loadConfig, renderTsType, resolveConfig, schemaModuleToJsonSchema, schemaNodeToJsonSchema, toObjectKey, typeNameFor, watch };
package/dist/index.js CHANGED
@@ -151,7 +151,8 @@ function applyDefaults(userConfig, cwd) {
151
151
  fileName: userConfig.mocks?.fileName ?? "mocks.ts",
152
152
  seed: userConfig.mocks?.seed ?? 1,
153
153
  baseUrl: userConfig.mocks?.baseUrl ?? ""
154
- }
154
+ },
155
+ driftGuard: userConfig.driftGuard ?? true
155
156
  };
156
157
  }
157
158
  async function loadConfig(cwd) {
@@ -611,7 +612,7 @@ async function collectEmittedFiles(extensions, ctx) {
611
612
  function requestShape(route) {
612
613
  const cs = route.contract?.contractSource;
613
614
  const isGet = route.method.toUpperCase() === "GET";
614
- const isQuery = isGet || !!cs?.filterFields?.length;
615
+ const isQuery = isGet || !!cs?.filterFields?.length || !!cs?.asQuery;
615
616
  const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
616
617
  const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
617
618
  return { isGet, isQuery, hasBody, hasQuery };
@@ -718,6 +719,7 @@ function emitFilterQueryType(c) {
718
719
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
719
720
  }
720
721
  function buildResponseType(c, outDir, serialization) {
722
+ if (c.contractSource.binaryResponse) return "RawResponse<Blob>";
721
723
  const raw = rawResponseType(c, outDir);
722
724
  return serialization === "json" ? `Jsonify<${raw}>` : raw;
723
725
  }
@@ -772,8 +774,9 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
772
774
  const safeUrl = JSON.stringify(c.path);
773
775
  const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
774
776
  const stream = c.contractSource.stream ? "true" : "false";
777
+ const binary = c.contractSource.binaryResponse ? "true" : "false";
775
778
  lines.push(
776
- `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
779
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream}; binary: ${binary} };`
777
780
  );
778
781
  } else {
779
782
  lines.push(`${pad}${objKey}: {`);
@@ -819,6 +822,9 @@ function buildRequestModel(c) {
819
822
  if (hasQuery) optsParts.push("query: input?.query as Record<string, unknown> | undefined");
820
823
  if (hasBody) optsParts.push("body: input?.body");
821
824
  if (hasBody && c.contractSource.multipart) optsParts.push("multipart: true");
825
+ if (c.contractSource.binaryResponse && m !== "get") {
826
+ optsParts.unshift(`method: ${JSON.stringify(m.toUpperCase())}`);
827
+ }
822
828
  const optsExpr = optsParts.length ? `{ ${optsParts.join(", ")} }` : "{}";
823
829
  return {
824
830
  routeName: c.name,
@@ -838,7 +844,8 @@ function buildRequestModel(c) {
838
844
  queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`
839
845
  };
840
846
  }
841
- function renderFetcherRequest(req) {
847
+ function renderFetcherRequest(req, binaryResponse) {
848
+ if (binaryResponse) return `fetcher.fetchBlob(${req.urlExpr}, ${req.optsExpr})`;
842
849
  return `fetcher.${req.method}<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
843
850
  }
844
851
  function emitReqHelper() {
@@ -899,7 +906,7 @@ function emitApiObjectBlock(tree, indent, p) {
899
906
  const leaf = {
900
907
  route: node.route,
901
908
  request: req,
902
- requestExpr: renderFetcherRequest(req)
909
+ requestExpr: renderFetcherRequest(req, node.contractSource.binaryResponse === true)
903
910
  };
904
911
  const owned = /* @__PURE__ */ new Map();
905
912
  if (p.layer) {
@@ -959,6 +966,8 @@ var ROUTE_NAMESPACE = [
959
966
  ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
960
967
  " /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
961
968
  ' export type Stream<K extends string> = ResolveByName<K, "response">;',
969
+ " /** True for a binary/blob route (`StreamableFile`/`Buffer` handler return type). */",
970
+ ' export type Binary<K extends string> = ResolveByName<K, "binary">;',
962
971
  " export type Request<K extends string> = {",
963
972
  " body: Body<K>;",
964
973
  " query: Query<K>;",
@@ -976,6 +985,7 @@ var PATH_NAMESPACE = [
976
985
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
977
986
  ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
978
987
  ' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
988
+ ' export type Binary<M extends string, U extends string> = ResolveByPath<M, U, "binary">;',
979
989
  "}",
980
990
  ""
981
991
  ];
@@ -988,6 +998,7 @@ var EMPTY_ROUTE_NAMESPACE = [
988
998
  " export type Error<K extends string> = never;",
989
999
  " export type FilterFields<K extends string> = never;",
990
1000
  " export type Stream<K extends string> = never;",
1001
+ " export type Binary<K extends string> = never;",
991
1002
  " export type Request<K extends string> = { body: never; query: never; params: never };",
992
1003
  "}",
993
1004
  ""
@@ -1001,6 +1012,7 @@ var EMPTY_PATH_NAMESPACE = [
1001
1012
  " export type Error<M extends string, U extends string> = never;",
1002
1013
  " export type FilterFields<M extends string, U extends string> = never;",
1003
1014
  " export type Stream<M extends string, U extends string> = never;",
1015
+ " export type Binary<M extends string, U extends string> = never;",
1004
1016
  "}",
1005
1017
  ""
1006
1018
  ];
@@ -1066,6 +1078,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1066
1078
  if (serialization === "json" && contracted.length > 0) {
1067
1079
  lines.push(`import type { Jsonify } from '${runtimeImport}';`);
1068
1080
  }
1081
+ if (contracted.some((r) => r.contract?.contractSource.binaryResponse)) {
1082
+ lines.push(`import type { RawResponse } from '${runtimeImport}';`);
1083
+ }
1069
1084
  if (importsByFile.size > 0 && outDir) {
1070
1085
  lines.push("");
1071
1086
  const emittedNames = /* @__PURE__ */ new Set();
@@ -1102,6 +1117,12 @@ function buildApiFile(routes, outDir, opts = {}) {
1102
1117
  lines.push("");
1103
1118
  lines.push(...EMPTY_ROUTE_NAMESPACE);
1104
1119
  lines.push(...EMPTY_PATH_NAMESPACE);
1120
+ for (const ext of headerExts) {
1121
+ const statements = ext.apiHeader?.(ctx)?.statements;
1122
+ if (statements?.length) {
1123
+ lines.push(...statements, "");
1124
+ }
1125
+ }
1105
1126
  return lines.join("\n");
1106
1127
  }
1107
1128
  const tree = /* @__PURE__ */ new Map();
@@ -2122,11 +2143,22 @@ import { join as join12, relative as relative6 } from "path";
2122
2143
  import fg2 from "fast-glob";
2123
2144
  var MANIFEST_FILE = ".codegen-manifest.json";
2124
2145
  var LOCK_FILE = ".watcher.lock";
2146
+ var DriftGuardError = class extends Error {
2147
+ constructor(message) {
2148
+ super(message);
2149
+ this.name = "DriftGuardError";
2150
+ }
2151
+ };
2152
+ function isEntryPoint(value) {
2153
+ return value === "cli" || value === "module";
2154
+ }
2125
2155
  function isManifestShape(value) {
2126
2156
  if (typeof value !== "object" || value === null) return false;
2127
2157
  const candidate = value;
2128
2158
  if (typeof candidate.version !== "string") return false;
2129
2159
  if (typeof candidate.hash !== "string") return false;
2160
+ if (candidate.entryPoint !== void 0 && !isEntryPoint(candidate.entryPoint)) return false;
2161
+ if (candidate.configHash !== void 0 && typeof candidate.configHash !== "string") return false;
2130
2162
  if (!Array.isArray(candidate.files)) return false;
2131
2163
  return candidate.files.every((entry) => typeof entry === "string");
2132
2164
  }
@@ -2169,11 +2201,20 @@ async function readManifest(outDir) {
2169
2201
  const raw = await readFile2(join12(outDir, MANIFEST_FILE), "utf8");
2170
2202
  const parsed = JSON.parse(raw);
2171
2203
  if (!isManifestShape(parsed)) return null;
2172
- return { version: parsed.version, hash: parsed.hash, files: parsed.files };
2204
+ return {
2205
+ version: parsed.version,
2206
+ hash: parsed.hash,
2207
+ ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
2208
+ ...parsed.configHash ? { configHash: parsed.configHash } : {},
2209
+ files: parsed.files
2210
+ };
2173
2211
  } catch {
2174
2212
  return null;
2175
2213
  }
2176
2214
  }
2215
+ function computeConfigHash(config) {
2216
+ return createHash("sha256").update(serializeConfig(config)).digest("hex");
2217
+ }
2177
2218
  async function writeManifest(outDir, manifest) {
2178
2219
  await writeFile9(join12(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
2179
2220
  `, "utf8");
@@ -2218,7 +2259,10 @@ function debugWarn(message) {
2218
2259
  }
2219
2260
 
2220
2261
  // src/generate.ts
2221
- async function generate(config, inputRoutes = []) {
2262
+ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint) {
2263
+ return `[nestjs-codegen] Config drift detected in "${outDir}": the last generate ran from the "${previousEntryPoint}" entry point, this run is from the "${currentEntryPoint}" entry point, and their resolved configs differ (e.g. \`serialization: "json"\` vs \`"superjson"\`). Both entry points must read the SAME config \u2014 export a shared config object (e.g. codegen.config.ts) and import it from BOTH nestjs-codegen.config.ts (CLI) and NestjsCodegenModule.forRoot() (Nest module), or set \`driftGuard: false\` on either config to opt out of this check.`;
2264
+ }
2265
+ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2222
2266
  setCodegenDebug(config.debug);
2223
2267
  const inputsHash = await computeInputsHash(config);
2224
2268
  const manifest = await readManifest(config.codegen.outDir);
@@ -2226,6 +2270,12 @@ async function generate(config, inputRoutes = []) {
2226
2270
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
2227
2271
  return;
2228
2272
  }
2273
+ const configHash = computeConfigHash(config);
2274
+ if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
2275
+ throw new DriftGuardError(
2276
+ driftGuardMessage(config.codegen.outDir, manifest.entryPoint, entryPoint)
2277
+ );
2278
+ }
2229
2279
  const extensions = config.extensions ?? [];
2230
2280
  let routes = inputRoutes;
2231
2281
  const ctx = createExtensionContext(config, () => routes);
@@ -2291,6 +2341,8 @@ async function generate(config, inputRoutes = []) {
2291
2341
  await writeManifest(config.codegen.outDir, {
2292
2342
  version: VERSION,
2293
2343
  hash: inputsHash,
2344
+ entryPoint,
2345
+ configHash,
2294
2346
  files: outputFiles
2295
2347
  });
2296
2348
  }
@@ -3880,6 +3932,14 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
3880
3932
  var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
3881
3933
  var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
3882
3934
  var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
3935
+ var BINARY_RESPONSE_TYPES = /* @__PURE__ */ new Set(["StreamableFile", "Buffer"]);
3936
+ function detectBinaryResponse(method) {
3937
+ const node = unwrapNamedContainer(method.getReturnTypeNode(), /* @__PURE__ */ new Set(["Promise"]));
3938
+ if (!node || !Node6.isTypeReference(node)) return false;
3939
+ const typeName = node.getTypeName();
3940
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
3941
+ return BINARY_RESPONSE_TYPES.has(name);
3942
+ }
3883
3943
  function detectStreamElement(method) {
3884
3944
  const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
3885
3945
  let node = method.getReturnTypeNode();
@@ -3900,6 +3960,9 @@ function streamContainerElement(node) {
3900
3960
  }
3901
3961
  return null;
3902
3962
  }
3963
+ function hasAsQueryDecorator(method) {
3964
+ return method.getDecorators().some((d) => d.getName() === "AsQuery");
3965
+ }
3903
3966
  function unwrapNamedContainer(node, names) {
3904
3967
  if (!node || !Node6.isTypeReference(node)) return node;
3905
3968
  const typeName = node.getTypeName();
@@ -3917,6 +3980,8 @@ function extractDtoContract(method, sourceFile, project) {
3917
3980
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
3918
3981
  const streamElement = detectStreamElement(method);
3919
3982
  const isStream = streamElement !== null;
3983
+ const binaryResponse = detectBinaryResponse(method);
3984
+ const asQuery = hasAsQueryDecorator(method);
3920
3985
  if (filterInfo && filterInfo.source === "body") {
3921
3986
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
3922
3987
  body = body ?? bodyType;
@@ -3924,7 +3989,7 @@ function extractDtoContract(method, sourceFile, project) {
3924
3989
  const paramsType = extractParamsType(method, sourceFile, project);
3925
3990
  const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3926
3991
  const errorInfo = extractErrorType(method, sourceFile, project);
3927
- if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart) {
3992
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart && !binaryResponse && !asQuery) {
3928
3993
  return null;
3929
3994
  }
3930
3995
  let bodyRef = null;
@@ -3999,7 +4064,9 @@ function extractDtoContract(method, sourceFile, project) {
3999
4064
  querySchema,
4000
4065
  stream: isStream,
4001
4066
  multipart: uploads.multipart,
4002
- multipartBody
4067
+ multipartBody,
4068
+ binaryResponse,
4069
+ asQuery
4003
4070
  };
4004
4071
  }
4005
4072
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -4486,7 +4553,9 @@ function extractDtoRoute(args) {
4486
4553
  querySchema: dtoContract?.querySchema ?? null,
4487
4554
  stream: dtoContract?.stream ?? false,
4488
4555
  multipart: dtoContract?.multipart ?? false,
4489
- multipartBody: dtoContract?.multipartBody ?? null
4556
+ multipartBody: dtoContract?.multipartBody ?? null,
4557
+ binaryResponse: dtoContract?.binaryResponse ?? false,
4558
+ asQuery: dtoContract?.asQuery ?? false
4490
4559
  }
4491
4560
  });
4492
4561
  }
@@ -4579,6 +4648,7 @@ var PAGES_DEBOUNCE_MS = 150;
4579
4648
  var NO_OP_WATCHER = { close: async () => {
4580
4649
  } };
4581
4650
  async function watch(config, onChange, options = {}) {
4651
+ const entryPoint = options.entryPoint ?? "cli";
4582
4652
  const lock = await acquireLock(config.codegen.outDir);
4583
4653
  if (lock === null) {
4584
4654
  let holderPid = "unknown";
@@ -4610,13 +4680,17 @@ async function watch(config, onChange, options = {}) {
4610
4680
  try {
4611
4681
  const initialRoutes = (await getDiscovery()).discover();
4612
4682
  lastRoutes = initialRoutes;
4613
- await generate(config, initialRoutes);
4683
+ await generate(config, initialRoutes, entryPoint);
4614
4684
  } catch (err) {
4685
+ if (err instanceof DriftGuardError) {
4686
+ console.error(err.message);
4687
+ return;
4688
+ }
4615
4689
  console.warn(
4616
4690
  `[nestjs-codegen] Initial route discovery failed, falling back to pages-only: ${err instanceof Error ? err.message : String(err)}`
4617
4691
  );
4618
4692
  try {
4619
- await generate(config, lastRoutes);
4693
+ await generate(config, lastRoutes, entryPoint);
4620
4694
  } catch {
4621
4695
  }
4622
4696
  }
@@ -4644,7 +4718,7 @@ async function watch(config, onChange, options = {}) {
4644
4718
  pagesDebounceTimer = setTimeout(async () => {
4645
4719
  pagesDebounceTimer = void 0;
4646
4720
  try {
4647
- await generate(config, lastRoutes);
4721
+ await generate(config, lastRoutes, entryPoint);
4648
4722
  } catch (err) {
4649
4723
  console.error(
4650
4724
  "[nestjs-codegen] Pages generation failed:",
@@ -4676,7 +4750,7 @@ async function watch(config, onChange, options = {}) {
4676
4750
  try {
4677
4751
  const routes = await (await getDiscovery()).rediscover(changed);
4678
4752
  lastRoutes = routes;
4679
- await generate(config, routes);
4753
+ await generate(config, routes, entryPoint);
4680
4754
  } catch (err) {
4681
4755
  console.error(
4682
4756
  "[nestjs-codegen] Contracts generation failed:",
@@ -4799,10 +4873,11 @@ function createChainModuleRenderer(opts) {
4799
4873
  }
4800
4874
 
4801
4875
  // src/index.ts
4802
- var VERSION = "0.13.2";
4876
+ var VERSION = "0.14.0";
4803
4877
  export {
4804
4878
  CodegenError,
4805
4879
  ConfigError,
4880
+ DriftGuardError,
4806
4881
  VERSION,
4807
4882
  acquireLock,
4808
4883
  buildMocksFile,