@dudousxd/nestjs-codegen 0.13.2 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -146,7 +146,8 @@ function applyDefaults(userConfig, cwd) {
146
146
  fileName: userConfig.mocks?.fileName ?? "mocks.ts",
147
147
  seed: userConfig.mocks?.seed ?? 1,
148
148
  baseUrl: userConfig.mocks?.baseUrl ?? ""
149
- }
149
+ },
150
+ driftGuard: userConfig.driftGuard ?? true
150
151
  };
151
152
  }
152
153
  async function loadConfig(cwd) {
@@ -606,8 +607,8 @@ async function collectEmittedFiles(extensions, ctx) {
606
607
  function requestShape(route) {
607
608
  const cs = route.contract?.contractSource;
608
609
  const isGet = route.method.toUpperCase() === "GET";
609
- const isQuery = isGet || !!cs?.filterFields?.length;
610
- const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
610
+ const isQuery = isGet || !!cs?.filterFields?.length || !!cs?.asQuery;
611
+ const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never" || !!cs?.multipart;
611
612
  const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
612
613
  return { isGet, isQuery, hasBody, hasQuery };
613
614
  }
@@ -713,6 +714,7 @@ function emitFilterQueryType(c) {
713
714
  return `import('@dudousxd/nestjs-filter-client').TypedFilterQuery<${emitFilterQueryTypeArgs(c)}>`;
714
715
  }
715
716
  function buildResponseType(c, outDir, serialization) {
717
+ if (c.contractSource.binaryResponse) return "RawResponse<Blob>";
716
718
  const raw = rawResponseType(c, outDir);
717
719
  return serialization === "json" ? `Jsonify<${raw}>` : raw;
718
720
  }
@@ -767,8 +769,9 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
767
769
  const safeUrl = JSON.stringify(c.path);
768
770
  const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
769
771
  const stream = c.contractSource.stream ? "true" : "false";
772
+ const binary = c.contractSource.binaryResponse ? "true" : "false";
770
773
  lines.push(
771
- `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream} };`
774
+ `${pad}${objKey}: { method: ${safeMethod}; url: ${safeUrl}; params: ${params}; query: ${query}; body: ${body}; response: ${response}; error: ${error}; filterFields: ${filterFields}; stream: ${stream}; binary: ${binary} };`
772
775
  );
773
776
  } else {
774
777
  lines.push(`${pad}${objKey}: {`);
@@ -814,6 +817,9 @@ function buildRequestModel(c) {
814
817
  if (hasQuery) optsParts.push("query: input?.query as Record<string, unknown> | undefined");
815
818
  if (hasBody) optsParts.push("body: input?.body");
816
819
  if (hasBody && c.contractSource.multipart) optsParts.push("multipart: true");
820
+ if (c.contractSource.binaryResponse && m !== "get") {
821
+ optsParts.unshift(`method: ${JSON.stringify(m.toUpperCase())}`);
822
+ }
817
823
  const optsExpr = optsParts.length ? `{ ${optsParts.join(", ")} }` : "{}";
818
824
  return {
819
825
  routeName: c.name,
@@ -833,7 +839,8 @@ function buildRequestModel(c) {
833
839
  queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`
834
840
  };
835
841
  }
836
- function renderFetcherRequest(req) {
842
+ function renderFetcherRequest(req, binaryResponse) {
843
+ if (binaryResponse) return `fetcher.fetchBlob(${req.urlExpr}, ${req.optsExpr})`;
837
844
  return `fetcher.${req.method}<${req.responseType}>(${req.urlExpr}, ${req.optsExpr})`;
838
845
  }
839
846
  function emitReqHelper() {
@@ -894,7 +901,7 @@ function emitApiObjectBlock(tree, indent, p) {
894
901
  const leaf = {
895
902
  route: node.route,
896
903
  request: req,
897
- requestExpr: renderFetcherRequest(req)
904
+ requestExpr: renderFetcherRequest(req, node.contractSource.binaryResponse === true)
898
905
  };
899
906
  const owned = /* @__PURE__ */ new Map();
900
907
  if (p.layer) {
@@ -954,6 +961,8 @@ var ROUTE_NAMESPACE = [
954
961
  ' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
955
962
  " /** The streamed element type of an `@Sse()`/streaming route \u2014 the type yielded by its `stream()` AsyncIterable. */",
956
963
  ' export type Stream<K extends string> = ResolveByName<K, "response">;',
964
+ " /** True for a binary/blob route (`StreamableFile`/`Buffer` handler return type). */",
965
+ ' export type Binary<K extends string> = ResolveByName<K, "binary">;',
957
966
  " export type Request<K extends string> = {",
958
967
  " body: Body<K>;",
959
968
  " query: Query<K>;",
@@ -971,6 +980,7 @@ var PATH_NAMESPACE = [
971
980
  ' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
972
981
  ' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
973
982
  ' export type Stream<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
983
+ ' export type Binary<M extends string, U extends string> = ResolveByPath<M, U, "binary">;',
974
984
  "}",
975
985
  ""
976
986
  ];
@@ -983,6 +993,7 @@ var EMPTY_ROUTE_NAMESPACE = [
983
993
  " export type Error<K extends string> = never;",
984
994
  " export type FilterFields<K extends string> = never;",
985
995
  " export type Stream<K extends string> = never;",
996
+ " export type Binary<K extends string> = never;",
986
997
  " export type Request<K extends string> = { body: never; query: never; params: never };",
987
998
  "}",
988
999
  ""
@@ -996,6 +1007,7 @@ var EMPTY_PATH_NAMESPACE = [
996
1007
  " export type Error<M extends string, U extends string> = never;",
997
1008
  " export type FilterFields<M extends string, U extends string> = never;",
998
1009
  " export type Stream<M extends string, U extends string> = never;",
1010
+ " export type Binary<M extends string, U extends string> = never;",
999
1011
  "}",
1000
1012
  ""
1001
1013
  ];
@@ -1061,6 +1073,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1061
1073
  if (serialization === "json" && contracted.length > 0) {
1062
1074
  lines.push(`import type { Jsonify } from '${runtimeImport}';`);
1063
1075
  }
1076
+ if (contracted.some((r) => r.contract?.contractSource.binaryResponse)) {
1077
+ lines.push(`import type { RawResponse } from '${runtimeImport}';`);
1078
+ }
1064
1079
  if (importsByFile.size > 0 && outDir) {
1065
1080
  lines.push("");
1066
1081
  const emittedNames = /* @__PURE__ */ new Set();
@@ -1097,6 +1112,12 @@ function buildApiFile(routes, outDir, opts = {}) {
1097
1112
  lines.push("");
1098
1113
  lines.push(...EMPTY_ROUTE_NAMESPACE);
1099
1114
  lines.push(...EMPTY_PATH_NAMESPACE);
1115
+ for (const ext of headerExts) {
1116
+ const statements = ext.apiHeader?.(ctx)?.statements;
1117
+ if (statements?.length) {
1118
+ lines.push(...statements, "");
1119
+ }
1120
+ }
1100
1121
  return lines.join("\n");
1101
1122
  }
1102
1123
  const tree = /* @__PURE__ */ new Map();
@@ -2114,11 +2135,22 @@ import { join as join12, relative as relative6 } from "path";
2114
2135
  import fg2 from "fast-glob";
2115
2136
  var MANIFEST_FILE = ".codegen-manifest.json";
2116
2137
  var LOCK_FILE = ".watcher.lock";
2138
+ var DriftGuardError = class extends Error {
2139
+ constructor(message) {
2140
+ super(message);
2141
+ this.name = "DriftGuardError";
2142
+ }
2143
+ };
2144
+ function isEntryPoint(value) {
2145
+ return value === "cli" || value === "module";
2146
+ }
2117
2147
  function isManifestShape(value) {
2118
2148
  if (typeof value !== "object" || value === null) return false;
2119
2149
  const candidate = value;
2120
2150
  if (typeof candidate.version !== "string") return false;
2121
2151
  if (typeof candidate.hash !== "string") return false;
2152
+ if (candidate.entryPoint !== void 0 && !isEntryPoint(candidate.entryPoint)) return false;
2153
+ if (candidate.configHash !== void 0 && typeof candidate.configHash !== "string") return false;
2122
2154
  if (!Array.isArray(candidate.files)) return false;
2123
2155
  return candidate.files.every((entry) => typeof entry === "string");
2124
2156
  }
@@ -2161,11 +2193,20 @@ async function readManifest(outDir) {
2161
2193
  const raw = await readFile2(join12(outDir, MANIFEST_FILE), "utf8");
2162
2194
  const parsed = JSON.parse(raw);
2163
2195
  if (!isManifestShape(parsed)) return null;
2164
- return { version: parsed.version, hash: parsed.hash, files: parsed.files };
2196
+ return {
2197
+ version: parsed.version,
2198
+ hash: parsed.hash,
2199
+ ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
2200
+ ...parsed.configHash ? { configHash: parsed.configHash } : {},
2201
+ files: parsed.files
2202
+ };
2165
2203
  } catch {
2166
2204
  return null;
2167
2205
  }
2168
2206
  }
2207
+ function computeConfigHash(config) {
2208
+ return createHash("sha256").update(serializeConfig(config)).digest("hex");
2209
+ }
2169
2210
  async function writeManifest(outDir, manifest) {
2170
2211
  await writeFile9(join12(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
2171
2212
  `, "utf8");
@@ -2210,7 +2251,10 @@ function debugWarn(message) {
2210
2251
  }
2211
2252
 
2212
2253
  // src/generate.ts
2213
- async function generate(config, inputRoutes = []) {
2254
+ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint) {
2255
+ 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.`;
2256
+ }
2257
+ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2214
2258
  setCodegenDebug(config.debug);
2215
2259
  const inputsHash = await computeInputsHash(config);
2216
2260
  const manifest = await readManifest(config.codegen.outDir);
@@ -2218,6 +2262,12 @@ async function generate(config, inputRoutes = []) {
2218
2262
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
2219
2263
  return;
2220
2264
  }
2265
+ const configHash = computeConfigHash(config);
2266
+ if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
2267
+ throw new DriftGuardError(
2268
+ driftGuardMessage(config.codegen.outDir, manifest.entryPoint, entryPoint)
2269
+ );
2270
+ }
2221
2271
  const extensions = config.extensions ?? [];
2222
2272
  let routes = inputRoutes;
2223
2273
  const ctx = createExtensionContext(config, () => routes);
@@ -2283,6 +2333,8 @@ async function generate(config, inputRoutes = []) {
2283
2333
  await writeManifest(config.codegen.outDir, {
2284
2334
  version: VERSION,
2285
2335
  hash: inputsHash,
2336
+ entryPoint,
2337
+ configHash,
2286
2338
  files: outputFiles
2287
2339
  });
2288
2340
  }
@@ -3872,6 +3924,14 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
3872
3924
  var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
3873
3925
  var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
3874
3926
  var STREAM_ENVELOPES = /* @__PURE__ */ new Set(["MessageEvent", "MessageEventLike"]);
3927
+ var BINARY_RESPONSE_TYPES = /* @__PURE__ */ new Set(["StreamableFile", "Buffer"]);
3928
+ function detectBinaryResponse(method) {
3929
+ const node = unwrapNamedContainer(method.getReturnTypeNode(), /* @__PURE__ */ new Set(["Promise"]));
3930
+ if (!node || !Node6.isTypeReference(node)) return false;
3931
+ const typeName = node.getTypeName();
3932
+ const name = Node6.isIdentifier(typeName) ? typeName.getText() : "";
3933
+ return BINARY_RESPONSE_TYPES.has(name);
3934
+ }
3875
3935
  function detectStreamElement(method) {
3876
3936
  const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
3877
3937
  let node = method.getReturnTypeNode();
@@ -3892,6 +3952,9 @@ function streamContainerElement(node) {
3892
3952
  }
3893
3953
  return null;
3894
3954
  }
3955
+ function hasAsQueryDecorator(method) {
3956
+ return method.getDecorators().some((d) => d.getName() === "AsQuery");
3957
+ }
3895
3958
  function unwrapNamedContainer(node, names) {
3896
3959
  if (!node || !Node6.isTypeReference(node)) return node;
3897
3960
  const typeName = node.getTypeName();
@@ -3909,6 +3972,8 @@ function extractDtoContract(method, sourceFile, project) {
3909
3972
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
3910
3973
  const streamElement = detectStreamElement(method);
3911
3974
  const isStream = streamElement !== null;
3975
+ const binaryResponse = detectBinaryResponse(method);
3976
+ const asQuery = hasAsQueryDecorator(method);
3912
3977
  if (filterInfo && filterInfo.source === "body") {
3913
3978
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
3914
3979
  body = body ?? bodyType;
@@ -3916,7 +3981,7 @@ function extractDtoContract(method, sourceFile, project) {
3916
3981
  const paramsType = extractParamsType(method, sourceFile, project);
3917
3982
  const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3918
3983
  const errorInfo = extractErrorType(method, sourceFile, project);
3919
- if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart) {
3984
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart && !binaryResponse && !asQuery) {
3920
3985
  return null;
3921
3986
  }
3922
3987
  let bodyRef = null;
@@ -3991,7 +4056,9 @@ function extractDtoContract(method, sourceFile, project) {
3991
4056
  querySchema,
3992
4057
  stream: isStream,
3993
4058
  multipart: uploads.multipart,
3994
- multipartBody
4059
+ multipartBody,
4060
+ binaryResponse,
4061
+ asQuery
3995
4062
  };
3996
4063
  }
3997
4064
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -4478,7 +4545,9 @@ function extractDtoRoute(args) {
4478
4545
  querySchema: dtoContract?.querySchema ?? null,
4479
4546
  stream: dtoContract?.stream ?? false,
4480
4547
  multipart: dtoContract?.multipart ?? false,
4481
- multipartBody: dtoContract?.multipartBody ?? null
4548
+ multipartBody: dtoContract?.multipartBody ?? null,
4549
+ binaryResponse: dtoContract?.binaryResponse ?? false,
4550
+ asQuery: dtoContract?.asQuery ?? false
4482
4551
  }
4483
4552
  });
4484
4553
  }
@@ -4571,6 +4640,7 @@ var PAGES_DEBOUNCE_MS = 150;
4571
4640
  var NO_OP_WATCHER = { close: async () => {
4572
4641
  } };
4573
4642
  async function watch(config, onChange, options = {}) {
4643
+ const entryPoint = options.entryPoint ?? "cli";
4574
4644
  const lock = await acquireLock(config.codegen.outDir);
4575
4645
  if (lock === null) {
4576
4646
  let holderPid = "unknown";
@@ -4602,13 +4672,17 @@ async function watch(config, onChange, options = {}) {
4602
4672
  try {
4603
4673
  const initialRoutes = (await getDiscovery()).discover();
4604
4674
  lastRoutes = initialRoutes;
4605
- await generate(config, initialRoutes);
4675
+ await generate(config, initialRoutes, entryPoint);
4606
4676
  } catch (err) {
4677
+ if (err instanceof DriftGuardError) {
4678
+ console.error(err.message);
4679
+ return;
4680
+ }
4607
4681
  console.warn(
4608
4682
  `[nestjs-codegen] Initial route discovery failed, falling back to pages-only: ${err instanceof Error ? err.message : String(err)}`
4609
4683
  );
4610
4684
  try {
4611
- await generate(config, lastRoutes);
4685
+ await generate(config, lastRoutes, entryPoint);
4612
4686
  } catch {
4613
4687
  }
4614
4688
  }
@@ -4636,7 +4710,7 @@ async function watch(config, onChange, options = {}) {
4636
4710
  pagesDebounceTimer = setTimeout(async () => {
4637
4711
  pagesDebounceTimer = void 0;
4638
4712
  try {
4639
- await generate(config, lastRoutes);
4713
+ await generate(config, lastRoutes, entryPoint);
4640
4714
  } catch (err) {
4641
4715
  console.error(
4642
4716
  "[nestjs-codegen] Pages generation failed:",
@@ -4668,7 +4742,7 @@ async function watch(config, onChange, options = {}) {
4668
4742
  try {
4669
4743
  const routes = await (await getDiscovery()).rediscover(changed);
4670
4744
  lastRoutes = routes;
4671
- await generate(config, routes);
4745
+ await generate(config, routes, entryPoint);
4672
4746
  } catch (err) {
4673
4747
  console.error(
4674
4748
  "[nestjs-codegen] Contracts generation failed:",
@@ -4708,14 +4782,14 @@ async function watch(config, onChange, options = {}) {
4708
4782
  }
4709
4783
 
4710
4784
  // src/index.ts
4711
- var VERSION = "0.13.2";
4785
+ var VERSION = "0.14.1";
4712
4786
 
4713
4787
  // src/cli/codegen.ts
4714
4788
  async function runCodegen(opts = {}) {
4715
4789
  const cwd = opts.cwd ?? process.cwd();
4716
4790
  const config = await loadConfig(cwd);
4717
4791
  if (opts.watch) {
4718
- const watcher = await watch(config);
4792
+ const watcher = await watch(config, void 0, { entryPoint: "cli" });
4719
4793
  await new Promise((resolve4) => {
4720
4794
  function onSignal() {
4721
4795
  watcher.close().then(resolve4).catch(resolve4);
@@ -4730,7 +4804,7 @@ async function runCodegen(opts = {}) {
4730
4804
  glob: config.contracts.glob,
4731
4805
  ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4732
4806
  });
4733
- await generate(config, routes);
4807
+ await generate(config, routes, "cli");
4734
4808
  console.log("\u2713 Codegen generated artifacts in", config.codegen.outDir);
4735
4809
  }
4736
4810