@dudousxd/nestjs-codegen 0.13.1 → 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/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,7 +607,7 @@ 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 isQuery = isGet || !!cs?.filterFields?.length || !!cs?.asQuery;
610
611
  const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
611
612
  const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
612
613
  return { isGet, isQuery, hasBody, hasQuery };
@@ -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
  }
@@ -3587,6 +3639,19 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth, subst = /
3587
3639
  dbg("unresolvable type:", name, "in", sourceFile.getFilePath());
3588
3640
  return "unknown";
3589
3641
  }
3642
+ if (Node6.isTypeLiteral(typeNode)) {
3643
+ const members = [];
3644
+ for (const member of typeNode.getMembers()) {
3645
+ if (Node6.isPropertySignature(member)) {
3646
+ const memberTypeNode = member.getTypeNode();
3647
+ const memberType = memberTypeNode ? resolveTypeNodeToString(memberTypeNode, sourceFile, project, depth, subst) : "unknown";
3648
+ members.push(`${member.getName()}${member.hasQuestionToken() ? "?" : ""}: ${memberType}`);
3649
+ } else {
3650
+ members.push(member.getText());
3651
+ }
3652
+ }
3653
+ return members.length > 0 ? `{ ${members.join("; ")} }` : "{}";
3654
+ }
3590
3655
  const kind = typeNode.getKind();
3591
3656
  if (kind === SyntaxKind3.StringKeyword) return "string";
3592
3657
  if (kind === SyntaxKind3.NumberKeyword) return "number";
@@ -3859,6 +3924,14 @@ function resolveBodyQueryResponseRef(typeNode, sourceFile, project) {
3859
3924
  var STREAM_CONTAINERS = /* @__PURE__ */ new Set(["Observable", "AsyncIterable", "AsyncIterableIterator"]);
3860
3925
  var STREAM_CONTAINERS_GENERATOR = /* @__PURE__ */ new Set(["AsyncGenerator"]);
3861
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
+ }
3862
3935
  function detectStreamElement(method) {
3863
3936
  const hasSse = method.getDecorators().some((d) => d.getName() === "Sse");
3864
3937
  let node = method.getReturnTypeNode();
@@ -3879,6 +3952,9 @@ function streamContainerElement(node) {
3879
3952
  }
3880
3953
  return null;
3881
3954
  }
3955
+ function hasAsQueryDecorator(method) {
3956
+ return method.getDecorators().some((d) => d.getName() === "AsQuery");
3957
+ }
3882
3958
  function unwrapNamedContainer(node, names) {
3883
3959
  if (!node || !Node6.isTypeReference(node)) return node;
3884
3960
  const typeName = node.getTypeName();
@@ -3896,6 +3972,8 @@ function extractDtoContract(method, sourceFile, project) {
3896
3972
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
3897
3973
  const streamElement = detectStreamElement(method);
3898
3974
  const isStream = streamElement !== null;
3975
+ const binaryResponse = detectBinaryResponse(method);
3976
+ const asQuery = hasAsQueryDecorator(method);
3899
3977
  if (filterInfo && filterInfo.source === "body") {
3900
3978
  const bodyType = "import('@dudousxd/nestjs-filter-client').FilterQueryResult";
3901
3979
  body = body ?? bodyType;
@@ -3903,7 +3981,7 @@ function extractDtoContract(method, sourceFile, project) {
3903
3981
  const paramsType = extractParamsType(method, sourceFile, project);
3904
3982
  const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3905
3983
  const errorInfo = extractErrorType(method, sourceFile, project);
3906
- 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) {
3907
3985
  return null;
3908
3986
  }
3909
3987
  let bodyRef = null;
@@ -3978,7 +4056,9 @@ function extractDtoContract(method, sourceFile, project) {
3978
4056
  querySchema,
3979
4057
  stream: isStream,
3980
4058
  multipart: uploads.multipart,
3981
- multipartBody
4059
+ multipartBody,
4060
+ binaryResponse,
4061
+ asQuery
3982
4062
  };
3983
4063
  }
3984
4064
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -4465,7 +4545,9 @@ function extractDtoRoute(args) {
4465
4545
  querySchema: dtoContract?.querySchema ?? null,
4466
4546
  stream: dtoContract?.stream ?? false,
4467
4547
  multipart: dtoContract?.multipart ?? false,
4468
- multipartBody: dtoContract?.multipartBody ?? null
4548
+ multipartBody: dtoContract?.multipartBody ?? null,
4549
+ binaryResponse: dtoContract?.binaryResponse ?? false,
4550
+ asQuery: dtoContract?.asQuery ?? false
4469
4551
  }
4470
4552
  });
4471
4553
  }
@@ -4558,6 +4640,7 @@ var PAGES_DEBOUNCE_MS = 150;
4558
4640
  var NO_OP_WATCHER = { close: async () => {
4559
4641
  } };
4560
4642
  async function watch(config, onChange, options = {}) {
4643
+ const entryPoint = options.entryPoint ?? "cli";
4561
4644
  const lock = await acquireLock(config.codegen.outDir);
4562
4645
  if (lock === null) {
4563
4646
  let holderPid = "unknown";
@@ -4589,13 +4672,17 @@ async function watch(config, onChange, options = {}) {
4589
4672
  try {
4590
4673
  const initialRoutes = (await getDiscovery()).discover();
4591
4674
  lastRoutes = initialRoutes;
4592
- await generate(config, initialRoutes);
4675
+ await generate(config, initialRoutes, entryPoint);
4593
4676
  } catch (err) {
4677
+ if (err instanceof DriftGuardError) {
4678
+ console.error(err.message);
4679
+ return;
4680
+ }
4594
4681
  console.warn(
4595
4682
  `[nestjs-codegen] Initial route discovery failed, falling back to pages-only: ${err instanceof Error ? err.message : String(err)}`
4596
4683
  );
4597
4684
  try {
4598
- await generate(config, lastRoutes);
4685
+ await generate(config, lastRoutes, entryPoint);
4599
4686
  } catch {
4600
4687
  }
4601
4688
  }
@@ -4623,7 +4710,7 @@ async function watch(config, onChange, options = {}) {
4623
4710
  pagesDebounceTimer = setTimeout(async () => {
4624
4711
  pagesDebounceTimer = void 0;
4625
4712
  try {
4626
- await generate(config, lastRoutes);
4713
+ await generate(config, lastRoutes, entryPoint);
4627
4714
  } catch (err) {
4628
4715
  console.error(
4629
4716
  "[nestjs-codegen] Pages generation failed:",
@@ -4655,7 +4742,7 @@ async function watch(config, onChange, options = {}) {
4655
4742
  try {
4656
4743
  const routes = await (await getDiscovery()).rediscover(changed);
4657
4744
  lastRoutes = routes;
4658
- await generate(config, routes);
4745
+ await generate(config, routes, entryPoint);
4659
4746
  } catch (err) {
4660
4747
  console.error(
4661
4748
  "[nestjs-codegen] Contracts generation failed:",
@@ -4695,14 +4782,14 @@ async function watch(config, onChange, options = {}) {
4695
4782
  }
4696
4783
 
4697
4784
  // src/index.ts
4698
- var VERSION = "0.13.1";
4785
+ var VERSION = "0.14.0";
4699
4786
 
4700
4787
  // src/cli/codegen.ts
4701
4788
  async function runCodegen(opts = {}) {
4702
4789
  const cwd = opts.cwd ?? process.cwd();
4703
4790
  const config = await loadConfig(cwd);
4704
4791
  if (opts.watch) {
4705
- const watcher = await watch(config);
4792
+ const watcher = await watch(config, void 0, { entryPoint: "cli" });
4706
4793
  await new Promise((resolve4) => {
4707
4794
  function onSignal() {
4708
4795
  watcher.close().then(resolve4).catch(resolve4);
@@ -4717,7 +4804,7 @@ async function runCodegen(opts = {}) {
4717
4804
  glob: config.contracts.glob,
4718
4805
  ...config.app?.tsconfig ? { tsconfig: config.app.tsconfig } : {}
4719
4806
  });
4720
- await generate(config, routes);
4807
+ await generate(config, routes, "cli");
4721
4808
  console.log("\u2713 Codegen generated artifacts in", config.codegen.outDir);
4722
4809
  }
4723
4810