@dudousxd/nestjs-codegen 0.15.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.
@@ -180,12 +180,12 @@ function applyDefaults(userConfig, cwd) {
180
180
  // src/generate-manifest.ts
181
181
  var import_node_crypto = require("crypto");
182
182
  var import_promises15 = require("fs/promises");
183
- var import_node_path17 = require("path");
183
+ var import_node_path18 = require("path");
184
184
  var import_fast_glob3 = __toESM(require("fast-glob"), 1);
185
185
 
186
186
  // src/generate.ts
187
187
  var import_promises11 = require("fs/promises");
188
- var import_node_path12 = require("path");
188
+ var import_node_path13 = require("path");
189
189
 
190
190
  // src/discovery/pages.ts
191
191
  var import_promises2 = require("fs/promises");
@@ -514,9 +514,10 @@ function inferExpressionType(node) {
514
514
 
515
515
  // src/emit/emit-api.ts
516
516
  var import_promises3 = require("fs/promises");
517
- var import_node_path4 = require("path");
517
+ var import_node_path5 = require("path");
518
518
 
519
519
  // src/extension/registry.ts
520
+ var import_node_path4 = require("path");
520
521
  var import_ts_morph2 = require("ts-morph");
521
522
  function resolveApiSlots(extensions) {
522
523
  let layer;
@@ -549,7 +550,7 @@ function mergeExclusive(target, incoming, {
549
550
  target.set(key, { value, owner });
550
551
  }
551
552
  }
552
- function createExtensionContext(config, getRoutes) {
553
+ function createExtensionContext(config, getRoutes, trackedInputs) {
553
554
  let project;
554
555
  return {
555
556
  cwd: config.codegen.cwd,
@@ -558,6 +559,13 @@ function createExtensionContext(config, getRoutes) {
558
559
  get routes() {
559
560
  return getRoutes();
560
561
  },
562
+ trackInput(...paths) {
563
+ if (!trackedInputs) return;
564
+ for (const path of paths) {
565
+ if (!path) continue;
566
+ trackedInputs.add((0, import_node_path4.relative)(config.codegen.cwd, (0, import_node_path4.resolve)(config.codegen.cwd, path)));
567
+ }
568
+ },
561
569
  project() {
562
570
  if (!project) {
563
571
  project = new import_ts_morph2.Project({
@@ -617,7 +625,7 @@ function requestShape(route) {
617
625
  async function emitApi(routes, outDir, opts = {}) {
618
626
  await (0, import_promises3.mkdir)(outDir, { recursive: true });
619
627
  const content = buildApiFile(routes, outDir, opts);
620
- await (0, import_promises3.writeFile)((0, import_node_path4.join)(outDir, "api.ts"), content, "utf8");
628
+ await (0, import_promises3.writeFile)((0, import_node_path5.join)(outDir, "api.ts"), content, "utf8");
621
629
  }
622
630
  function splitName(name) {
623
631
  return name.split(".");
@@ -725,7 +733,7 @@ function rawResponseType(c, outDir) {
725
733
  return c.contractSource.response;
726
734
  }
727
735
  if (c.controllerRef) {
728
- let relPath = (0, import_node_path4.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
736
+ let relPath = (0, import_node_path5.relative)(outDir, c.controllerRef.filePath).replace(/\.ts$/, "");
729
737
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
730
738
  return `Awaited<ReturnType<import('${relPath}').${c.controllerRef.className}['${c.controllerRef.methodName}']>>`;
731
739
  }
@@ -1051,6 +1059,11 @@ function buildApiFile(routes, outDir, opts = {}) {
1051
1059
  config: {},
1052
1060
  project: () => {
1053
1061
  throw new Error("ExtensionContext.project() is unavailable in standalone emitApi.");
1062
+ },
1063
+ // No manifest is written on the standalone path, so there is nothing for
1064
+ // tracked dependencies to be recorded into — accept and discard them
1065
+ // rather than making every caller supply a sink.
1066
+ trackInput: () => {
1054
1067
  }
1055
1068
  };
1056
1069
  const importsByFile = /* @__PURE__ */ new Map();
@@ -1106,8 +1119,8 @@ function buildApiFile(routes, outDir, opts = {}) {
1106
1119
  const emittedNames = /* @__PURE__ */ new Set();
1107
1120
  for (const [filePath, names] of importsByFile) {
1108
1121
  let relPath;
1109
- if ((0, import_node_path4.isAbsolute)(filePath)) {
1110
- relPath = (0, import_node_path4.relative)(outDir, filePath).replace(/\.ts$/, "");
1122
+ if ((0, import_node_path5.isAbsolute)(filePath)) {
1123
+ relPath = (0, import_node_path5.relative)(outDir, filePath).replace(/\.ts$/, "");
1111
1124
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1112
1125
  } else {
1113
1126
  relPath = filePath;
@@ -1201,7 +1214,7 @@ function buildApiFile(routes, outDir, opts = {}) {
1201
1214
 
1202
1215
  // src/emit/emit-cache.ts
1203
1216
  var import_promises4 = require("fs/promises");
1204
- var import_node_path5 = require("path");
1217
+ var import_node_path6 = require("path");
1205
1218
  async function emitCache(pages, outDir) {
1206
1219
  await (0, import_promises4.mkdir)(outDir, { recursive: true });
1207
1220
  const entries = await Promise.all(
@@ -1215,19 +1228,19 @@ async function emitCache(pages, outDir) {
1215
1228
  })
1216
1229
  );
1217
1230
  const cache = { pages: entries };
1218
- await (0, import_promises4.writeFile)((0, import_node_path5.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1231
+ await (0, import_promises4.writeFile)((0, import_node_path6.join)(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
1219
1232
  `, "utf8");
1220
1233
  }
1221
1234
 
1222
1235
  // src/emit/emit-forms.ts
1223
1236
  var import_promises5 = require("fs/promises");
1224
- var import_node_path6 = require("path");
1237
+ var import_node_path7 = require("path");
1225
1238
  async function emitForms(routes, outDir, config, adapter) {
1226
1239
  if (config && config.enabled === false) return false;
1227
1240
  const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
1228
1241
  if (content === null) return false;
1229
1242
  await (0, import_promises5.mkdir)(outDir, { recursive: true });
1230
- await (0, import_promises5.writeFile)((0, import_node_path6.join)(outDir, "forms.ts"), content, "utf8");
1243
+ await (0, import_promises5.writeFile)((0, import_node_path7.join)(outDir, "forms.ts"), content, "utf8");
1231
1244
  return true;
1232
1245
  }
1233
1246
  function pascal(segment) {
@@ -1240,7 +1253,7 @@ function deriveBaseName(routeName) {
1240
1253
  return { method, full };
1241
1254
  }
1242
1255
  function relImport(outDir, filePath) {
1243
- let relPath = (0, import_node_path6.relative)(outDir, filePath).replace(/\.ts$/, "");
1256
+ let relPath = (0, import_node_path7.relative)(outDir, filePath).replace(/\.ts$/, "");
1244
1257
  if (!relPath.startsWith(".")) relPath = `./${relPath}`;
1245
1258
  return relPath;
1246
1259
  }
@@ -1514,7 +1527,7 @@ function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
1514
1527
 
1515
1528
  // src/emit/emit-index.ts
1516
1529
  var import_promises6 = require("fs/promises");
1517
- var import_node_path7 = require("path");
1530
+ var import_node_path8 = require("path");
1518
1531
  async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1519
1532
  await (0, import_promises6.mkdir)(outDir, { recursive: true });
1520
1533
  const exports2 = ["export * from './pages.js';", "export * from './routes.js';"];
@@ -1527,12 +1540,12 @@ async function emitIndex(outDir, hasContracts = false, hasForms = false) {
1527
1540
  const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports2, ""].join(
1528
1541
  "\n"
1529
1542
  );
1530
- await (0, import_promises6.writeFile)((0, import_node_path7.join)(outDir, "index.d.ts"), content, "utf8");
1543
+ await (0, import_promises6.writeFile)((0, import_node_path8.join)(outDir, "index.d.ts"), content, "utf8");
1531
1544
  }
1532
1545
 
1533
1546
  // src/emit/emit-mocks.ts
1534
1547
  var import_promises7 = require("fs/promises");
1535
- var import_node_path8 = require("path");
1548
+ var import_node_path9 = require("path");
1536
1549
 
1537
1550
  // src/ir/schema-node-to-json-schema.ts
1538
1551
  var DEFAULT_CTX = { refPrefix: "#/components/schemas/" };
@@ -1856,12 +1869,12 @@ async function emitMocks(routes, outDir, opts = {}) {
1856
1869
  await (0, import_promises7.mkdir)(outDir, { recursive: true });
1857
1870
  const content = buildMocksFile(routes, opts);
1858
1871
  const fileName = opts.fileName ?? "mocks.ts";
1859
- await (0, import_promises7.writeFile)((0, import_node_path8.join)(outDir, fileName), content, "utf8");
1872
+ await (0, import_promises7.writeFile)((0, import_node_path9.join)(outDir, fileName), content, "utf8");
1860
1873
  }
1861
1874
 
1862
1875
  // src/emit/emit-openapi.ts
1863
1876
  var import_promises8 = require("fs/promises");
1864
- var import_node_path9 = require("path");
1877
+ var import_node_path10 = require("path");
1865
1878
  var REF_PREFIX2 = "#/components/schemas/";
1866
1879
  function toOpenApiPath(path) {
1867
1880
  return path.replace(/:([^/]+)/g, "{$1}");
@@ -1982,13 +1995,13 @@ async function emitOpenApi(routes, outDir, opts = {}) {
1982
1995
  await (0, import_promises8.mkdir)(outDir, { recursive: true });
1983
1996
  const doc = buildOpenApiSpec(routes, opts);
1984
1997
  const fileName = opts.fileName ?? "openapi.json";
1985
- await (0, import_promises8.writeFile)((0, import_node_path9.join)(outDir, fileName), `${JSON.stringify(doc, null, 2)}
1998
+ await (0, import_promises8.writeFile)((0, import_node_path10.join)(outDir, fileName), `${JSON.stringify(doc, null, 2)}
1986
1999
  `, "utf8");
1987
2000
  }
1988
2001
 
1989
2002
  // src/emit/emit-pages.ts
1990
2003
  var import_promises9 = require("fs/promises");
1991
- var import_node_path10 = require("path");
2004
+ var import_node_path11 = require("path");
1992
2005
  async function emitPages(pages, outDir, _options = {}) {
1993
2006
  await (0, import_promises9.mkdir)(outDir, { recursive: true });
1994
2007
  const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
@@ -2009,7 +2022,7 @@ ${augBody}
2009
2022
  }
2010
2023
  ${sharedPropsBlock}}
2011
2024
  `;
2012
- await (0, import_promises9.writeFile)((0, import_node_path10.join)(outDir, "pages.d.ts"), content, "utf8");
2025
+ await (0, import_promises9.writeFile)((0, import_node_path11.join)(outDir, "pages.d.ts"), content, "utf8");
2013
2026
  }
2014
2027
  function buildSharedPropsBlock(sharedProps) {
2015
2028
  if (!sharedProps) return "";
@@ -2028,7 +2041,7 @@ ${propsBody}
2028
2041
  `;
2029
2042
  }
2030
2043
  function buildAugmentationType(page, outDir) {
2031
- let importPath = (0, import_node_path10.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
2044
+ let importPath = (0, import_node_path11.relative)(outDir, page.absolutePath).replace(/\.(tsx?|vue|svelte)$/, "");
2032
2045
  if (!importPath.startsWith(".")) {
2033
2046
  importPath = `./${importPath}`;
2034
2047
  }
@@ -2040,11 +2053,11 @@ function needsQuotes(name) {
2040
2053
 
2041
2054
  // src/emit/emit-routes.ts
2042
2055
  var import_promises10 = require("fs/promises");
2043
- var import_node_path11 = require("path");
2056
+ var import_node_path12 = require("path");
2044
2057
  async function emitRoutes(routes, outDir) {
2045
2058
  await (0, import_promises10.mkdir)(outDir, { recursive: true });
2046
2059
  const content = buildRoutesFile(routes);
2047
- await (0, import_promises10.writeFile)((0, import_node_path11.join)(outDir, "routes.ts"), content, "utf8");
2060
+ await (0, import_promises10.writeFile)((0, import_node_path12.join)(outDir, "routes.ts"), content, "utf8");
2048
2061
  }
2049
2062
  function buildRoutesFile(routes) {
2050
2063
  if (routes.length === 0) {
@@ -2172,8 +2185,8 @@ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differ
2172
2185
  }
2173
2186
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
2174
2187
  setCodegenDebug(config.debug);
2175
- const inputsHash = await computeInputsHash(config);
2176
2188
  const manifest = await readManifest(config.codegen.outDir);
2189
+ const inputsHash = await computeInputsHash(config, manifest?.extraInputs ?? []);
2177
2190
  if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
2178
2191
  console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
2179
2192
  return;
@@ -2194,7 +2207,8 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2194
2207
  }
2195
2208
  const extensions = config.extensions ?? [];
2196
2209
  let routes = inputRoutes;
2197
- const ctx = createExtensionContext(config, () => routes);
2210
+ const trackedInputs = /* @__PURE__ */ new Set();
2211
+ const ctx = createExtensionContext(config, () => routes, trackedInputs);
2198
2212
  if (extensions.length > 0) {
2199
2213
  routes = await applyTransformRoutes(routes, extensions, ctx);
2200
2214
  }
@@ -2248,31 +2262,34 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2248
2262
  if (extensions.length > 0) {
2249
2263
  const extraFiles = await collectEmittedFiles(extensions, ctx);
2250
2264
  for (const file of extraFiles) {
2251
- const dest = (0, import_node_path12.join)(config.codegen.outDir, file.path);
2252
- await (0, import_promises11.mkdir)((0, import_node_path12.dirname)(dest), { recursive: true });
2265
+ const dest = (0, import_node_path13.join)(config.codegen.outDir, file.path);
2266
+ await (0, import_promises11.mkdir)((0, import_node_path13.dirname)(dest), { recursive: true });
2253
2267
  await (0, import_promises11.writeFile)(dest, file.contents, "utf8");
2254
2268
  }
2255
2269
  }
2256
2270
  const outputFiles = await listOutputFiles(config.codegen.outDir);
2271
+ const extraInputs = [...trackedInputs].sort();
2272
+ const recordedHash = extraInputs.length > 0 ? await computeInputsHash(config, extraInputs) : inputsHash;
2257
2273
  await writeManifest(config.codegen.outDir, {
2258
2274
  version: VERSION,
2259
- hash: inputsHash,
2275
+ hash: recordedHash,
2260
2276
  entryPoint,
2261
2277
  configHash,
2262
2278
  configKeyHashes,
2263
- files: outputFiles
2279
+ files: outputFiles,
2280
+ ...extraInputs.length > 0 ? { extraInputs } : {}
2264
2281
  });
2265
2282
  }
2266
2283
 
2267
2284
  // src/watch/watcher.ts
2268
2285
  var import_promises14 = require("fs/promises");
2269
- var import_node_path16 = require("path");
2286
+ var import_node_path17 = require("path");
2270
2287
  var import_chokidar = __toESM(require("chokidar"), 1);
2271
2288
 
2272
2289
  // src/discovery/contracts-fast.ts
2273
- var import_node_path14 = require("path");
2290
+ var import_node_path15 = require("path");
2274
2291
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
2275
- var import_ts_morph9 = require("ts-morph");
2292
+ var import_ts_morph10 = require("ts-morph");
2276
2293
 
2277
2294
  // src/discovery/dto-type-resolver.ts
2278
2295
  var import_ts_morph7 = require("ts-morph");
@@ -2282,7 +2299,7 @@ var import_ts_morph4 = require("ts-morph");
2282
2299
 
2283
2300
  // src/discovery/type-ref-resolution.ts
2284
2301
  var import_node_fs = require("fs");
2285
- var import_node_path13 = require("path");
2302
+ var import_node_path14 = require("path");
2286
2303
  var import_ts_morph3 = require("ts-morph");
2287
2304
  var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
2288
2305
  var _ctxByProject = /* @__PURE__ */ new WeakMap();
@@ -2334,12 +2351,12 @@ function findTypeInFile(name, file) {
2334
2351
  }
2335
2352
  function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2336
2353
  if (moduleSpecifier.startsWith(".")) {
2337
- const dir = (0, import_node_path13.dirname)(sourceFile.getFilePath());
2354
+ const dir = (0, import_node_path14.dirname)(sourceFile.getFilePath());
2338
2355
  const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
2339
2356
  return [
2340
- (0, import_node_path13.resolve)(dir, `${noExt}.ts`),
2341
- (0, import_node_path13.resolve)(dir, `${moduleSpecifier}.ts`),
2342
- (0, import_node_path13.resolve)(dir, moduleSpecifier, "index.ts")
2357
+ (0, import_node_path14.resolve)(dir, `${noExt}.ts`),
2358
+ (0, import_node_path14.resolve)(dir, `${moduleSpecifier}.ts`),
2359
+ (0, import_node_path14.resolve)(dir, moduleSpecifier, "index.ts")
2343
2360
  ];
2344
2361
  }
2345
2362
  const ctx = _ctxFor(project);
@@ -2360,8 +2377,8 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
2360
2377
  const rest = moduleSpecifier.slice(prefix.length);
2361
2378
  const candidates = [];
2362
2379
  for (const mapping of mappings) {
2363
- const resolved = (0, import_node_path13.resolve)(baseUrl, mapping.replace("*", rest));
2364
- candidates.push(`${resolved}.ts`, (0, import_node_path13.resolve)(resolved, "index.ts"));
2380
+ const resolved = (0, import_node_path14.resolve)(baseUrl, mapping.replace("*", rest));
2381
+ candidates.push(`${resolved}.ts`, (0, import_node_path14.resolve)(resolved, "index.ts"));
2365
2382
  }
2366
2383
  dbg(" resolved candidates:", candidates);
2367
2384
  return candidates;
@@ -3251,6 +3268,12 @@ function toFilterFieldType(name, r) {
3251
3268
  }
3252
3269
 
3253
3270
  // src/discovery/filter-for.ts
3271
+ function resolveMixinEntityClass(mixin, project) {
3272
+ const entityArg = mixin?.classArgs[0];
3273
+ if (!entityArg) return void 0;
3274
+ const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3275
+ return file?.getClass(entityArg.name);
3276
+ }
3254
3277
  function classifyFilterForHint(typeInit) {
3255
3278
  if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
3256
3279
  switch (typeInit.getLiteralValue()) {
@@ -3324,7 +3347,9 @@ function extractFilterForHints(classDecl, project) {
3324
3347
  }
3325
3348
  return hints;
3326
3349
  }
3327
- function extractApplyFilterInfo(method, sourceFile, project) {
3350
+ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3351
+ const declFile = method.getSourceFile();
3352
+ const mixinEntity = resolveMixinEntityClass(mixin, project);
3328
3353
  for (const param of method.getParameters()) {
3329
3354
  const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
3330
3355
  if (!filterDecorator) continue;
@@ -3344,12 +3369,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
3344
3369
  }
3345
3370
  }
3346
3371
  const filterClassName = filterClassArg.getText();
3347
- const resolved = findType(filterClassName, sourceFile, project);
3348
- if (resolved && resolved.kind === "class") {
3349
- const classDecl = resolved.decl;
3372
+ const resolved = findType(filterClassName, declFile, project);
3373
+ const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
3374
+ if (classDecl) {
3350
3375
  let fieldTypes = extractClassPropertyTypes(classDecl, project);
3351
3376
  if (fieldTypes.length === 0) {
3352
- fieldTypes = extractFilterableEntityFields(classDecl, project);
3377
+ fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3353
3378
  }
3354
3379
  const filterForHints = extractFilterForHints(classDecl, project);
3355
3380
  if (filterForHints.size > 0) {
@@ -3434,22 +3459,29 @@ function extractClassPropertyTypes(classDecl, project) {
3434
3459
  }
3435
3460
  return fields;
3436
3461
  }
3437
- function extractFilterableEntityFields(filterClass, project) {
3462
+ function resolveLocalClassDeclaration(identifier) {
3463
+ if (!import_ts_morph6.Node.isIdentifier(identifier)) return void 0;
3464
+ return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && import_ts_morph6.Node.isClassDeclaration(n));
3465
+ }
3466
+ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3467
+ if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return void 0;
3468
+ const entityProp = optionsArg.getProperty("entity");
3469
+ if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return void 0;
3470
+ const entityInit = entityProp.getInitializer();
3471
+ if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return void 0;
3472
+ const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
3473
+ if (!resolved || resolved.kind !== "class") return void 0;
3474
+ return resolved.decl;
3475
+ }
3476
+ function extractFilterableEntityFields(filterClass, project, entityOverride) {
3438
3477
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3439
3478
  if (!filterableDecorator) return [];
3440
3479
  const args = filterableDecorator.getArguments();
3441
3480
  if (args.length === 0) return [];
3442
3481
  const optionsArg = args[0];
3443
3482
  if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
3444
- const entityProp = optionsArg.getProperty("entity");
3445
- if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return [];
3446
- const entityInit = entityProp.getInitializer();
3447
- if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
3448
- const entityName = entityInit.getText();
3449
- const filterSourceFile = filterClass.getSourceFile();
3450
- const resolvedEntity = findType(entityName, filterSourceFile, project);
3451
- if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
3452
- const entityDecl = resolvedEntity.decl;
3483
+ const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3484
+ if (!entityDecl) return [];
3453
3485
  const fields = collectEntityFields(
3454
3486
  entityDecl,
3455
3487
  entityDecl.getSourceFile(),
@@ -3874,9 +3906,9 @@ function unwrapNamedContainer(node, names) {
3874
3906
  }
3875
3907
  return node;
3876
3908
  }
3877
- function extractDtoContract(method, sourceFile, project) {
3909
+ function extractDtoContract(method, sourceFile, project, mixin) {
3878
3910
  let body = extractBodyType(method, sourceFile, project);
3879
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
3911
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
3880
3912
  const query = extractQueryType(method, sourceFile, project);
3881
3913
  const uploads = extractUploadedFiles(method);
3882
3914
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -3986,12 +4018,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
3986
4018
  return null;
3987
4019
  }
3988
4020
 
3989
- // src/discovery/zod-ast-to-ts.ts
4021
+ // src/discovery/heritage.ts
3990
4022
  var import_ts_morph8 = require("ts-morph");
4023
+ function unwrapExpression(node) {
4024
+ let current = node;
4025
+ while (import_ts_morph8.Node.isAsExpression(current) || import_ts_morph8.Node.isSatisfiesExpression(current) || import_ts_morph8.Node.isParenthesizedExpression(current) || import_ts_morph8.Node.isTypeAssertion(current)) {
4026
+ current = current.getExpression();
4027
+ }
4028
+ return current;
4029
+ }
4030
+ function resolveReturnedClass(factoryDecl) {
4031
+ const body = factoryDecl.getBody();
4032
+ if (!body) return void 0;
4033
+ const returns = body.getDescendants().filter((n) => import_ts_morph8.Node.isReturnStatement(n));
4034
+ for (const ret of returns) {
4035
+ const expr = ret.getExpression();
4036
+ if (!expr) continue;
4037
+ const inner = unwrapExpression(expr);
4038
+ if (import_ts_morph8.Node.isClassExpression(inner)) {
4039
+ return inner;
4040
+ }
4041
+ if (import_ts_morph8.Node.isIdentifier(inner)) {
4042
+ const name = inner.getText();
4043
+ const decl = body.getDescendants().find((n) => import_ts_morph8.Node.isClassDeclaration(n) && n.getName() === name);
4044
+ if (decl) return decl;
4045
+ }
4046
+ }
4047
+ return void 0;
4048
+ }
4049
+ function resolveInheritedMethods(cls) {
4050
+ const expr = cls.getExtends()?.getExpression();
4051
+ if (!expr || !import_ts_morph8.Node.isCallExpression(expr)) return void 0;
4052
+ const callee = expr.getExpression();
4053
+ if (!import_ts_morph8.Node.isIdentifier(callee)) return void 0;
4054
+ const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isFunctionDeclaration(n));
4055
+ if (!factoryDecl) return void 0;
4056
+ const returnedClass = resolveReturnedClass(factoryDecl);
4057
+ if (!returnedClass) return void 0;
4058
+ const classArgs = [];
4059
+ for (const arg of expr.getArguments()) {
4060
+ if (!import_ts_morph8.Node.isIdentifier(arg)) continue;
4061
+ const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isClassDeclaration(n));
4062
+ if (decl) {
4063
+ classArgs.push({
4064
+ name: decl.getName() ?? arg.getText(),
4065
+ filePath: decl.getSourceFile().getFilePath()
4066
+ });
4067
+ }
4068
+ }
4069
+ return {
4070
+ methods: returnedClass.getMethods(),
4071
+ factoryName: callee.getText(),
4072
+ factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
4073
+ classArgs
4074
+ };
4075
+ }
4076
+ var mixinTypeProject;
4077
+ function getMixinTypeProject() {
4078
+ mixinTypeProject ??= new import_ts_morph8.Project({
4079
+ skipAddingFilesFromTsConfig: true,
4080
+ compilerOptions: { strict: true }
4081
+ });
4082
+ return mixinTypeProject;
4083
+ }
4084
+ function clearMixinTypeProject() {
4085
+ mixinTypeProject = void 0;
4086
+ }
4087
+ function resolveInstantiatedReturnType(cls, methodName) {
4088
+ const filePath = cls.getSourceFile().getFilePath();
4089
+ const className = cls.getName();
4090
+ if (!className) return void 0;
4091
+ const project = getMixinTypeProject();
4092
+ const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
4093
+ const typedCls = sourceFile?.getClass(className);
4094
+ if (!typedCls) return void 0;
4095
+ const prop = typedCls.getType().getProperty(methodName);
4096
+ if (!prop) return void 0;
4097
+ const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
4098
+ if (!returnType) return void 0;
4099
+ const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
4100
+ return unwrapped.getText(typedCls);
4101
+ }
4102
+
4103
+ // src/discovery/zod-ast-to-ts.ts
4104
+ var import_ts_morph9 = require("ts-morph");
3991
4105
  function zodAstToTs(node) {
3992
- if (!import_ts_morph8.Node.isCallExpression(node)) return "unknown";
4106
+ if (!import_ts_morph9.Node.isCallExpression(node)) return "unknown";
3993
4107
  const expr = node.getExpression();
3994
- if (import_ts_morph8.Node.isPropertyAccessExpression(expr)) {
4108
+ if (import_ts_morph9.Node.isPropertyAccessExpression(expr)) {
3995
4109
  const methodName = expr.getName();
3996
4110
  const receiver = expr.getExpression();
3997
4111
  if (methodName === "optional") {
@@ -4015,17 +4129,17 @@ function zodAstToTs(node) {
4015
4129
  case "literal": {
4016
4130
  const lit = args[0];
4017
4131
  if (!lit) return "unknown";
4018
- if (import_ts_morph8.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4019
- if (import_ts_morph8.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4020
- if (lit.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword) return "true";
4021
- if (lit.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) return "false";
4132
+ if (import_ts_morph9.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4133
+ if (import_ts_morph9.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4134
+ if (lit.getKind() === import_ts_morph9.SyntaxKind.TrueKeyword) return "true";
4135
+ if (lit.getKind() === import_ts_morph9.SyntaxKind.FalseKeyword) return "false";
4022
4136
  return "unknown";
4023
4137
  }
4024
4138
  case "enum": {
4025
4139
  const arrArg = args[0];
4026
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4140
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4027
4141
  const members = arrArg.getElements().map(
4028
- (el) => import_ts_morph8.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4142
+ (el) => import_ts_morph9.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4029
4143
  );
4030
4144
  return members.join(" | ");
4031
4145
  }
@@ -4036,10 +4150,10 @@ function zodAstToTs(node) {
4036
4150
  }
4037
4151
  case "object": {
4038
4152
  const objArg = args[0];
4039
- if (!objArg || !import_ts_morph8.Node.isObjectLiteralExpression(objArg)) return "unknown";
4153
+ if (!objArg || !import_ts_morph9.Node.isObjectLiteralExpression(objArg)) return "unknown";
4040
4154
  const lines = [];
4041
4155
  for (const prop of objArg.getProperties()) {
4042
- if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
4156
+ if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
4043
4157
  const key = prop.getName();
4044
4158
  const valNode = prop.getInitializer();
4045
4159
  if (!valNode) continue;
@@ -4051,7 +4165,7 @@ function zodAstToTs(node) {
4051
4165
  }
4052
4166
  case "union": {
4053
4167
  const arrArg = args[0];
4054
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4168
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4055
4169
  return arrArg.getElements().map(zodAstToTs).join(" | ");
4056
4170
  }
4057
4171
  case "record": {
@@ -4061,7 +4175,7 @@ function zodAstToTs(node) {
4061
4175
  }
4062
4176
  case "tuple": {
4063
4177
  const arrArg = args[0];
4064
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4178
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4065
4179
  return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
4066
4180
  }
4067
4181
  default:
@@ -4071,18 +4185,18 @@ function zodAstToTs(node) {
4071
4185
  return "unknown";
4072
4186
  }
4073
4187
  function isOptionalChain(node) {
4074
- if (!import_ts_morph8.Node.isCallExpression(node)) return false;
4188
+ if (!import_ts_morph9.Node.isCallExpression(node)) return false;
4075
4189
  const expr = node.getExpression();
4076
- return import_ts_morph8.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4190
+ return import_ts_morph9.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4077
4191
  }
4078
4192
  function parseDefineContractCall(callExpr) {
4079
- if (!import_ts_morph8.Node.isCallExpression(callExpr)) return null;
4193
+ if (!import_ts_morph9.Node.isCallExpression(callExpr)) return null;
4080
4194
  const callee = callExpr.getExpression();
4081
- const calleeName = import_ts_morph8.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph8.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
4195
+ const calleeName = import_ts_morph9.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph9.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
4082
4196
  if (calleeName !== "defineContract") return null;
4083
4197
  const args = callExpr.getArguments();
4084
4198
  const optsArg = args[0];
4085
- if (!optsArg || !import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) return null;
4199
+ if (!optsArg || !import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) return null;
4086
4200
  let query = null;
4087
4201
  let body = null;
4088
4202
  let response = "unknown";
@@ -4090,7 +4204,7 @@ function parseDefineContractCall(callExpr) {
4090
4204
  let bodyZodText = null;
4091
4205
  let queryZodText = null;
4092
4206
  for (const prop of optsArg.getProperties()) {
4093
- if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
4207
+ if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
4094
4208
  const propName = prop.getName();
4095
4209
  const val = prop.getInitializer();
4096
4210
  if (!val) continue;
@@ -4111,18 +4225,18 @@ function parseDefineContractCall(callExpr) {
4111
4225
 
4112
4226
  // src/discovery/contracts-fast.ts
4113
4227
  function resolveTsconfigPath(cwd, tsconfig) {
4114
- return tsconfig ? (0, import_node_path14.resolve)(tsconfig) : (0, import_node_path14.join)(cwd, "tsconfig.json");
4228
+ return tsconfig ? (0, import_node_path15.resolve)(tsconfig) : (0, import_node_path15.join)(cwd, "tsconfig.json");
4115
4229
  }
4116
4230
  function createDiscoveryProject(tsconfigPath) {
4117
4231
  try {
4118
- return new import_ts_morph9.Project({
4232
+ return new import_ts_morph10.Project({
4119
4233
  tsConfigFilePath: tsconfigPath,
4120
4234
  skipAddingFilesFromTsConfig: true,
4121
4235
  skipLoadingLibFiles: true,
4122
4236
  skipFileDependencyResolution: true
4123
4237
  });
4124
4238
  } catch {
4125
- return new import_ts_morph9.Project({
4239
+ return new import_ts_morph10.Project({
4126
4240
  skipAddingFilesFromTsConfig: true,
4127
4241
  skipLoadingLibFiles: true,
4128
4242
  skipFileDependencyResolution: true,
@@ -4190,7 +4304,7 @@ var PersistentDiscovery = class _PersistentDiscovery {
4190
4304
  async rediscover(changedPaths) {
4191
4305
  if (changedPaths) {
4192
4306
  for (const p of changedPaths) {
4193
- const abs = (0, import_node_path14.resolve)(p);
4307
+ const abs = (0, import_node_path15.resolve)(p);
4194
4308
  const sf = this.project.getSourceFile(abs);
4195
4309
  if (sf) {
4196
4310
  await sf.refreshFromFileSystem();
@@ -4222,15 +4336,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
4222
4336
  runExtraction() {
4223
4337
  clearTypeResolutionCaches(this.project);
4224
4338
  clearEnumCache(this.project);
4339
+ clearMixinTypeProject();
4225
4340
  return extractRoutesFrom(this.project, this.controllerPaths);
4226
4341
  }
4227
4342
  };
4228
4343
  function decoratorStringArg(decoratorExpr) {
4229
4344
  if (!decoratorExpr) return void 0;
4230
- if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4231
- if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
4345
+ if (import_ts_morph10.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4346
+ if (import_ts_morph10.Node.isArrayLiteralExpression(decoratorExpr)) {
4232
4347
  const first = decoratorExpr.getElements()[0];
4233
- if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
4348
+ if (first && import_ts_morph10.Node.isStringLiteral(first)) return first.getLiteralValue();
4234
4349
  }
4235
4350
  return void 0;
4236
4351
  }
@@ -4252,7 +4367,8 @@ function joinPaths(prefix, suffix) {
4252
4367
  if (!prefix && !suffix) return "/";
4253
4368
  if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
4254
4369
  if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
4255
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4370
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
4371
+ const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
4256
4372
  const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
4257
4373
  const combined = p + s;
4258
4374
  return combined === "" ? "/" : combined;
@@ -4306,7 +4422,8 @@ function buildRoute(args) {
4306
4422
  methodAs,
4307
4423
  sourceFile,
4308
4424
  seenNames,
4309
- contractSource
4425
+ contractSource,
4426
+ mixin
4310
4427
  } = args;
4311
4428
  const routeName = resolveRouteName(className, methodName, classAs, methodAs);
4312
4429
  const qualifiedRef = `${className}.${methodName}`;
@@ -4322,7 +4439,12 @@ function buildRoute(args) {
4322
4439
  path: combinedPath,
4323
4440
  name: routeName,
4324
4441
  params: extractParams(combinedPath),
4325
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
4442
+ controllerRef: {
4443
+ className,
4444
+ methodName,
4445
+ filePath: sourceFile.getFilePath(),
4446
+ ...mixin ? { mixin } : {}
4447
+ },
4326
4448
  contract: { contractSource }
4327
4449
  };
4328
4450
  }
@@ -4336,16 +4458,17 @@ function extractContractRoute(args) {
4336
4458
  className,
4337
4459
  sourceFile,
4338
4460
  project,
4339
- seenNames
4461
+ seenNames,
4462
+ mixin
4340
4463
  } = args;
4341
4464
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
4342
4465
  if (!firstDecoratorArg) return null;
4343
4466
  let contractDef = null;
4344
4467
  let bodyZodRef = null;
4345
4468
  let queryZodRef = null;
4346
- if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
4469
+ if (import_ts_morph10.Node.isCallExpression(firstDecoratorArg)) {
4347
4470
  contractDef = parseDefineContractCall(firstDecoratorArg);
4348
- } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
4471
+ } else if (import_ts_morph10.Node.isIdentifier(firstDecoratorArg)) {
4349
4472
  const identName = firstDecoratorArg.getText();
4350
4473
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4351
4474
  if (!resolvedVar) {
@@ -4388,6 +4511,7 @@ function extractContractRoute(args) {
4388
4511
  methodAs,
4389
4512
  sourceFile,
4390
4513
  seenNames,
4514
+ mixin,
4391
4515
  contractSource: {
4392
4516
  query: contractDef.query,
4393
4517
  body: contractDef.body,
@@ -4404,13 +4528,14 @@ function extractContractRoute(args) {
4404
4528
  });
4405
4529
  }
4406
4530
  function extractDtoRoute(args) {
4407
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
4531
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
4408
4532
  if (!verb) return null;
4409
4533
  const combined = joinPaths(prefix, verb.handlerPath);
4410
4534
  const methodName = method.getName();
4411
4535
  const classAs = readAsDecorator(cls, `class ${className}`);
4412
4536
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4413
- const dtoContract = extractDtoContract(method, sourceFile, project);
4537
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4538
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4414
4539
  return buildRoute({
4415
4540
  className,
4416
4541
  methodName,
@@ -4420,10 +4545,11 @@ function extractDtoRoute(args) {
4420
4545
  methodAs,
4421
4546
  sourceFile,
4422
4547
  seenNames,
4548
+ mixin,
4423
4549
  contractSource: {
4424
4550
  query: dtoContract?.query ?? null,
4425
4551
  body: dtoContract?.body ?? null,
4426
- response: dtoContract?.response ?? "unknown",
4552
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
4427
4553
  error: dtoContract?.error ?? null,
4428
4554
  queryRef: dtoContract?.queryRef ?? null,
4429
4555
  bodyRef: dtoContract?.bodyRef ?? null,
@@ -4452,7 +4578,17 @@ function extractFromSourceFile(sourceFile, project) {
4452
4578
  const firstArg2 = controllerDecorator.getArguments()[0];
4453
4579
  const prefix = decoratorStringArg(firstArg2) ?? "";
4454
4580
  const className = cls.getName() ?? "Unknown";
4455
- for (const method of cls.getMethods()) {
4581
+ const inherited = resolveInheritedMethods(cls);
4582
+ const mixinBinding = inherited ? {
4583
+ factoryName: inherited.factoryName,
4584
+ factoryFilePath: inherited.factoryFilePath,
4585
+ classArgs: inherited.classArgs
4586
+ } : void 0;
4587
+ const methods = [
4588
+ ...cls.getMethods().map((method) => ({ method })),
4589
+ ...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
4590
+ ];
4591
+ for (const { method, mixin } of methods) {
4456
4592
  const verb = resolveVerb(method);
4457
4593
  const applyContractDecorator = method.getDecorator("ApplyContract");
4458
4594
  const route = applyContractDecorator ? extractContractRoute({
@@ -4464,7 +4600,8 @@ function extractFromSourceFile(sourceFile, project) {
4464
4600
  className,
4465
4601
  sourceFile,
4466
4602
  project,
4467
- seenNames
4603
+ seenNames,
4604
+ mixin
4468
4605
  }) : extractDtoRoute({
4469
4606
  cls,
4470
4607
  method,
@@ -4473,7 +4610,8 @@ function extractFromSourceFile(sourceFile, project) {
4473
4610
  className,
4474
4611
  sourceFile,
4475
4612
  project,
4476
- seenNames
4613
+ seenNames,
4614
+ mixin
4477
4615
  });
4478
4616
  if (route) routes.push(route);
4479
4617
  }
@@ -4484,7 +4622,7 @@ function extractFromSourceFile(sourceFile, project) {
4484
4622
  // src/watch/lock-file.ts
4485
4623
  var import_promises12 = require("fs/promises");
4486
4624
  var import_promises13 = require("fs/promises");
4487
- var import_node_path15 = require("path");
4625
+ var import_node_path16 = require("path");
4488
4626
  var LOCK_FILE = ".watcher.lock";
4489
4627
  function isProcessAlive(pid) {
4490
4628
  try {
@@ -4496,7 +4634,7 @@ function isProcessAlive(pid) {
4496
4634
  }
4497
4635
  async function acquireLock(outDir) {
4498
4636
  await (0, import_promises13.mkdir)(outDir, { recursive: true });
4499
- const lockPath = (0, import_node_path15.join)(outDir, LOCK_FILE);
4637
+ const lockPath = (0, import_node_path16.join)(outDir, LOCK_FILE);
4500
4638
  const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
4501
4639
  try {
4502
4640
  const fd = await (0, import_promises12.open)(lockPath, "wx");
@@ -4537,7 +4675,7 @@ async function watch(config, onChange, options = {}) {
4537
4675
  if (lock === null) {
4538
4676
  let holderPid = "unknown";
4539
4677
  try {
4540
- const raw = await (0, import_promises14.readFile)((0, import_node_path16.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
4678
+ const raw = await (0, import_promises14.readFile)((0, import_node_path17.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
4541
4679
  const data = JSON.parse(raw);
4542
4680
  if (data.pid !== void 0) holderPid = String(data.pid);
4543
4681
  } catch {
@@ -4590,7 +4728,7 @@ async function watch(config, onChange, options = {}) {
4590
4728
  }
4591
4729
  let pagesDebounceTimer;
4592
4730
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
4593
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, pagesGlob), {
4731
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path17.join)(config.codegen.cwd, pagesGlob), {
4594
4732
  ignoreInitial: true,
4595
4733
  persistent: true,
4596
4734
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4617,7 +4755,7 @@ async function watch(config, onChange, options = {}) {
4617
4755
  pagesWatcher.on("unlink", schedulePagesRegenerate);
4618
4756
  let contractsDebounceTimer;
4619
4757
  const pendingChangedPaths = /* @__PURE__ */ new Set();
4620
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.contracts.glob), {
4758
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path17.join)(config.codegen.cwd, config.contracts.glob), {
4621
4759
  ignoreInitial: true,
4622
4760
  persistent: true,
4623
4761
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4647,7 +4785,7 @@ async function watch(config, onChange, options = {}) {
4647
4785
  contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4648
4786
  contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4649
4787
  contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4650
- const formsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.forms.watch), {
4788
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path17.join)(config.codegen.cwd, config.forms.watch), {
4651
4789
  ignoreInitial: true,
4652
4790
  persistent: true,
4653
4791
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4674,7 +4812,7 @@ async function watch(config, onChange, options = {}) {
4674
4812
  }
4675
4813
 
4676
4814
  // src/index.ts
4677
- var VERSION = "0.15.0";
4815
+ var VERSION = "0.17.0";
4678
4816
 
4679
4817
  // src/generate-manifest.ts
4680
4818
  var MANIFEST_FILE = ".codegen-manifest.json";
@@ -4698,6 +4836,9 @@ function isManifestShape(value) {
4698
4836
  if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
4699
4837
  return false;
4700
4838
  }
4839
+ if (candidate.extraInputs !== void 0 && (!Array.isArray(candidate.extraInputs) || !candidate.extraInputs.every((entry) => typeof entry === "string"))) {
4840
+ return false;
4841
+ }
4701
4842
  if (!Array.isArray(candidate.files)) return false;
4702
4843
  return candidate.files.every((entry) => typeof entry === "string");
4703
4844
  }
@@ -4737,26 +4878,35 @@ async function discoverInputFiles(config) {
4737
4878
  const matched = await (0, import_fast_glob3.default)(globs, { cwd, absolute: true, onlyFiles: true });
4738
4879
  return [...new Set(matched)].sort();
4739
4880
  }
4740
- async function computeInputsHash(config) {
4881
+ async function computeInputsHash(config, extraInputs = []) {
4741
4882
  const hash = (0, import_node_crypto.createHash)("sha256");
4742
4883
  hash.update(`version:${VERSION}
4743
4884
  `);
4744
4885
  hash.update(`config:${serializeConfig(config)}
4745
4886
  `);
4746
- const inputFiles = await discoverInputFiles(config);
4747
4887
  const cwd = config.codegen.cwd;
4748
- for (const file of inputFiles) {
4888
+ const globbed = await discoverInputFiles(config);
4889
+ const globbedRelative = new Set(globbed.map((file) => (0, import_node_path18.relative)(cwd, file)));
4890
+ const extra = [...new Set(extraInputs)].filter((file) => !globbedRelative.has(file)).sort();
4891
+ for (const file of globbed) {
4749
4892
  const contents = await (0, import_promises15.readFile)(file, "utf8");
4750
- hash.update(`file:${(0, import_node_path17.relative)(cwd, file)}
4893
+ hash.update(`file:${(0, import_node_path18.relative)(cwd, file)}
4751
4894
  `);
4752
4895
  hash.update(contents);
4753
4896
  hash.update("\n");
4754
4897
  }
4898
+ for (const file of extra) {
4899
+ const contents = await (0, import_promises15.readFile)((0, import_node_path18.join)(cwd, file), "utf8").catch(() => null);
4900
+ hash.update(`extra:${file}
4901
+ `);
4902
+ hash.update(contents ?? "\0missing");
4903
+ hash.update("\n");
4904
+ }
4755
4905
  return hash.digest("hex");
4756
4906
  }
4757
4907
  async function readManifest(outDir) {
4758
4908
  try {
4759
- const raw = await (0, import_promises15.readFile)((0, import_node_path17.join)(outDir, MANIFEST_FILE), "utf8");
4909
+ const raw = await (0, import_promises15.readFile)((0, import_node_path18.join)(outDir, MANIFEST_FILE), "utf8");
4760
4910
  const parsed = JSON.parse(raw);
4761
4911
  if (!isManifestShape(parsed)) return null;
4762
4912
  return {
@@ -4765,6 +4915,7 @@ async function readManifest(outDir) {
4765
4915
  ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
4766
4916
  ...parsed.configHash ? { configHash: parsed.configHash } : {},
4767
4917
  ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
4918
+ ...parsed.extraInputs ? { extraInputs: parsed.extraInputs } : {},
4768
4919
  files: parsed.files
4769
4920
  };
4770
4921
  } catch {
@@ -4775,7 +4926,7 @@ function computeConfigHash(config) {
4775
4926
  return (0, import_node_crypto.createHash)("sha256").update(serializeConfig(config)).digest("hex");
4776
4927
  }
4777
4928
  async function writeManifest(outDir, manifest) {
4778
- await (0, import_promises15.writeFile)((0, import_node_path17.join)(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
4929
+ await (0, import_promises15.writeFile)((0, import_node_path18.join)(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
4779
4930
  `, "utf8");
4780
4931
  }
4781
4932
  async function listOutputFiles(outDir) {
@@ -4783,11 +4934,11 @@ async function listOutputFiles(outDir) {
4783
4934
  async function walk(dir) {
4784
4935
  const entries = await (0, import_promises15.readdir)(dir, { withFileTypes: true }).catch(() => []);
4785
4936
  for (const entry of entries) {
4786
- const abs = (0, import_node_path17.join)(dir, entry.name);
4937
+ const abs = (0, import_node_path18.join)(dir, entry.name);
4787
4938
  if (entry.isDirectory()) {
4788
4939
  await walk(abs);
4789
4940
  } else if (entry.isFile()) {
4790
- const rel = (0, import_node_path17.relative)(outDir, abs);
4941
+ const rel = (0, import_node_path18.relative)(outDir, abs);
4791
4942
  if (rel === MANIFEST_FILE || rel === LOCK_FILE2) continue;
4792
4943
  found.push(rel);
4793
4944
  }