@dudousxd/nestjs-codegen 0.11.0 → 0.13.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.
@@ -174,8 +174,8 @@ function applyDefaults(userConfig, cwd) {
174
174
  }
175
175
 
176
176
  // src/watch/watcher.ts
177
- var import_promises14 = require("fs/promises");
178
- var import_node_path16 = require("path");
177
+ var import_promises15 = require("fs/promises");
178
+ var import_node_path17 = require("path");
179
179
  var import_chokidar = __toESM(require("chokidar"), 1);
180
180
 
181
181
  // src/discovery/contracts-fast.ts
@@ -1589,6 +1589,55 @@ function extractParamsType(method, sourceFile, project) {
1589
1589
  }
1590
1590
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
1591
1591
  }
1592
+ function extractUploadedFiles(method) {
1593
+ const FILE = "File | Blob";
1594
+ const entries = [];
1595
+ let multipart = false;
1596
+ const hasUploadedFileParam = method.getParameters().some(
1597
+ (p) => p.getDecorators().some((d) => {
1598
+ const name = d.getName();
1599
+ return name === "UploadedFile" || name === "UploadedFiles";
1600
+ })
1601
+ );
1602
+ for (const decorator of method.getDecorators()) {
1603
+ if (decorator.getName() !== "UseInterceptors") continue;
1604
+ for (const arg of decorator.getArguments()) {
1605
+ if (!import_ts_morph5.Node.isCallExpression(arg)) continue;
1606
+ const interceptor = arg.getExpression().getText();
1607
+ const callArgs = arg.getArguments();
1608
+ const firstArg2 = callArgs[0];
1609
+ if (interceptor === "FileInterceptor") {
1610
+ if (firstArg2 && import_ts_morph5.Node.isStringLiteral(firstArg2)) {
1611
+ entries.push(`${firstArg2.getLiteralValue()}: ${FILE}`);
1612
+ multipart = true;
1613
+ }
1614
+ } else if (interceptor === "FilesInterceptor") {
1615
+ if (firstArg2 && import_ts_morph5.Node.isStringLiteral(firstArg2)) {
1616
+ entries.push(`${firstArg2.getLiteralValue()}: Array<${FILE}>`);
1617
+ multipart = true;
1618
+ }
1619
+ } else if (interceptor === "FileFieldsInterceptor") {
1620
+ if (firstArg2 && import_ts_morph5.Node.isArrayLiteralExpression(firstArg2)) {
1621
+ for (const el of firstArg2.getElements()) {
1622
+ if (!import_ts_morph5.Node.isObjectLiteralExpression(el)) continue;
1623
+ const nameProp = el.getProperty("name");
1624
+ if (nameProp && import_ts_morph5.Node.isPropertyAssignment(nameProp)) {
1625
+ const init = nameProp.getInitializer();
1626
+ if (init && import_ts_morph5.Node.isStringLiteral(init)) {
1627
+ entries.push(`${init.getLiteralValue()}: Array<${FILE}>`);
1628
+ }
1629
+ }
1630
+ }
1631
+ multipart = true;
1632
+ }
1633
+ } else if (interceptor === "AnyFilesInterceptor") {
1634
+ multipart = true;
1635
+ }
1636
+ }
1637
+ }
1638
+ if (hasUploadedFileParam) multipart = true;
1639
+ return { fields: entries.length > 0 ? entries.join("; ") : null, multipart };
1640
+ }
1592
1641
  function extractResponseType(method, sourceFile, project) {
1593
1642
  const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
1594
1643
  if (apiResponseDecorator) {
@@ -1723,6 +1772,11 @@ function extractDtoContract(method, sourceFile, project) {
1723
1772
  let body = extractBodyType(method, sourceFile, project);
1724
1773
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
1725
1774
  const query = extractQueryType(method, sourceFile, project);
1775
+ const uploads = extractUploadedFiles(method);
1776
+ if (uploads.fields) {
1777
+ const fileObject = `{ ${uploads.fields} }`;
1778
+ body = body ? `(${body}) & ${fileObject}` : fileObject;
1779
+ }
1726
1780
  const streamElement = detectStreamElement(method);
1727
1781
  const isStream = streamElement !== null;
1728
1782
  if (filterInfo && filterInfo.source === "body") {
@@ -1732,7 +1786,7 @@ function extractDtoContract(method, sourceFile, project) {
1732
1786
  const paramsType = extractParamsType(method, sourceFile, project);
1733
1787
  const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
1734
1788
  const errorInfo = extractErrorType(method, sourceFile, project);
1735
- if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
1789
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart) {
1736
1790
  return null;
1737
1791
  }
1738
1792
  let bodyRef = null;
@@ -1805,7 +1859,8 @@ function extractDtoContract(method, sourceFile, project) {
1805
1859
  formWarnings,
1806
1860
  bodySchema,
1807
1861
  querySchema,
1808
- stream: isStream
1862
+ stream: isStream,
1863
+ multipart: uploads.multipart
1809
1864
  };
1810
1865
  }
1811
1866
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -2272,7 +2327,8 @@ function extractDtoRoute(args) {
2272
2327
  formWarnings: dtoContract?.formWarnings ?? [],
2273
2328
  bodySchema: dtoContract?.bodySchema ?? null,
2274
2329
  querySchema: dtoContract?.querySchema ?? null,
2275
- stream: dtoContract?.stream ?? false
2330
+ stream: dtoContract?.stream ?? false,
2331
+ multipart: dtoContract?.multipart ?? false
2276
2332
  }
2277
2333
  });
2278
2334
  }
@@ -2315,8 +2371,8 @@ function extractFromSourceFile(sourceFile, project) {
2315
2371
  }
2316
2372
 
2317
2373
  // src/generate.ts
2318
- var import_promises11 = require("fs/promises");
2319
- var import_node_path14 = require("path");
2374
+ var import_promises14 = require("fs/promises");
2375
+ var import_node_path16 = require("path");
2320
2376
 
2321
2377
  // src/discovery/pages.ts
2322
2378
  var import_promises2 = require("fs/promises");
@@ -2918,6 +2974,7 @@ function buildRequestModel(c) {
2918
2974
  const optsParts = [];
2919
2975
  if (hasQuery) optsParts.push("query: input?.query as Record<string, unknown> | undefined");
2920
2976
  if (hasBody) optsParts.push("body: input?.body");
2977
+ if (hasBody && c.contractSource.multipart) optsParts.push("multipart: true");
2921
2978
  const optsExpr = optsParts.length ? `{ ${optsParts.join(", ")} }` : "{}";
2922
2979
  return {
2923
2980
  routeName: c.name,
@@ -4211,9 +4268,159 @@ function buildEmpty() {
4211
4268
  ].join("\n");
4212
4269
  }
4213
4270
 
4271
+ // src/generate-manifest.ts
4272
+ var import_node_crypto = require("crypto");
4273
+ var import_promises13 = require("fs/promises");
4274
+ var import_node_path15 = require("path");
4275
+ var import_fast_glob3 = __toESM(require("fast-glob"), 1);
4276
+
4277
+ // src/watch/lock-file.ts
4278
+ var import_promises11 = require("fs/promises");
4279
+ var import_promises12 = require("fs/promises");
4280
+ var import_node_path14 = require("path");
4281
+ var LOCK_FILE = ".watcher.lock";
4282
+ function isProcessAlive(pid) {
4283
+ try {
4284
+ process.kill(pid, 0);
4285
+ return true;
4286
+ } catch {
4287
+ return false;
4288
+ }
4289
+ }
4290
+ async function acquireLock(outDir) {
4291
+ await (0, import_promises12.mkdir)(outDir, { recursive: true });
4292
+ const lockPath = (0, import_node_path14.join)(outDir, LOCK_FILE);
4293
+ const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
4294
+ try {
4295
+ const fd = await (0, import_promises11.open)(lockPath, "wx");
4296
+ await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
4297
+ `, "utf8");
4298
+ await fd.close();
4299
+ } catch (err) {
4300
+ if (err.code === "EEXIST") {
4301
+ try {
4302
+ const raw = await (0, import_promises12.readFile)(lockPath, "utf8");
4303
+ const existing = JSON.parse(raw);
4304
+ if (isProcessAlive(existing.pid)) return null;
4305
+ await (0, import_promises12.unlink)(lockPath);
4306
+ return acquireLock(outDir);
4307
+ } catch {
4308
+ return null;
4309
+ }
4310
+ }
4311
+ return null;
4312
+ }
4313
+ return {
4314
+ release: async () => {
4315
+ try {
4316
+ await (0, import_promises12.unlink)(lockPath);
4317
+ } catch {
4318
+ }
4319
+ }
4320
+ };
4321
+ }
4322
+
4323
+ // src/index.ts
4324
+ var VERSION = "0.13.0";
4325
+
4326
+ // src/generate-manifest.ts
4327
+ var MANIFEST_FILE = ".codegen-manifest.json";
4328
+ var LOCK_FILE2 = ".watcher.lock";
4329
+ function isManifestShape(value) {
4330
+ if (typeof value !== "object" || value === null) return false;
4331
+ const candidate = value;
4332
+ if (typeof candidate.version !== "string") return false;
4333
+ if (typeof candidate.hash !== "string") return false;
4334
+ if (!Array.isArray(candidate.files)) return false;
4335
+ return candidate.files.every((entry) => typeof entry === "string");
4336
+ }
4337
+ function serializeConfig(config) {
4338
+ try {
4339
+ return JSON.stringify(config, (_key, value) => {
4340
+ if (typeof value === "function") return `[fn:${value.name}]${value.toString()}`;
4341
+ return value;
4342
+ });
4343
+ } catch {
4344
+ return `unserializable:${config.codegen.outDir}:${config.contracts.glob}`;
4345
+ }
4346
+ }
4347
+ async function discoverInputFiles(config) {
4348
+ const globs = [config.contracts.glob, config.forms.watch];
4349
+ if (config.pages) globs.push(config.pages.glob);
4350
+ const cwd = config.codegen.cwd;
4351
+ const matched = await (0, import_fast_glob3.default)(globs, { cwd, absolute: true, onlyFiles: true });
4352
+ return [...new Set(matched)].sort();
4353
+ }
4354
+ async function computeInputsHash(config) {
4355
+ const hash = (0, import_node_crypto.createHash)("sha256");
4356
+ hash.update(`version:${VERSION}
4357
+ `);
4358
+ hash.update(`config:${serializeConfig(config)}
4359
+ `);
4360
+ const inputFiles = await discoverInputFiles(config);
4361
+ const cwd = config.codegen.cwd;
4362
+ for (const file of inputFiles) {
4363
+ const contents = await (0, import_promises13.readFile)(file, "utf8");
4364
+ hash.update(`file:${(0, import_node_path15.relative)(cwd, file)}
4365
+ `);
4366
+ hash.update(contents);
4367
+ hash.update("\n");
4368
+ }
4369
+ return hash.digest("hex");
4370
+ }
4371
+ async function readManifest(outDir) {
4372
+ try {
4373
+ const raw = await (0, import_promises13.readFile)((0, import_node_path15.join)(outDir, MANIFEST_FILE), "utf8");
4374
+ const parsed = JSON.parse(raw);
4375
+ if (!isManifestShape(parsed)) return null;
4376
+ return { version: parsed.version, hash: parsed.hash, files: parsed.files };
4377
+ } catch {
4378
+ return null;
4379
+ }
4380
+ }
4381
+ async function writeManifest(outDir, manifest) {
4382
+ await (0, import_promises13.writeFile)((0, import_node_path15.join)(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
4383
+ `, "utf8");
4384
+ }
4385
+ async function listOutputFiles(outDir) {
4386
+ const found = [];
4387
+ async function walk(dir) {
4388
+ const entries = await (0, import_promises13.readdir)(dir, { withFileTypes: true }).catch(() => []);
4389
+ for (const entry of entries) {
4390
+ const abs = (0, import_node_path15.join)(dir, entry.name);
4391
+ if (entry.isDirectory()) {
4392
+ await walk(abs);
4393
+ } else if (entry.isFile()) {
4394
+ const rel = (0, import_node_path15.relative)(outDir, abs);
4395
+ if (rel === MANIFEST_FILE || rel === LOCK_FILE2) continue;
4396
+ found.push(rel);
4397
+ }
4398
+ }
4399
+ }
4400
+ await walk(outDir);
4401
+ return found.sort();
4402
+ }
4403
+ async function allOutputsExist(outDir, files) {
4404
+ const present = new Set(await listOutputFiles(outDir));
4405
+ return files.every((file) => present.has(file));
4406
+ }
4407
+ async function isManifestFresh(outDir, manifest, inputsHash) {
4408
+ if (manifest === null) return false;
4409
+ if (manifest.version !== VERSION) return false;
4410
+ if (manifest.hash !== inputsHash) return false;
4411
+ if (manifest.files.length === 0) return false;
4412
+ return allOutputsExist(outDir, manifest.files);
4413
+ }
4414
+
4214
4415
  // src/generate.ts
4215
4416
  async function generate(config, inputRoutes = []) {
4216
4417
  setCodegenDebug(config.debug);
4418
+ const inputsHash = await computeInputsHash(config);
4419
+ const manifest = await readManifest(config.codegen.outDir);
4420
+ if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
4421
+ console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
4422
+ return;
4423
+ }
4217
4424
  const extensions = config.extensions ?? [];
4218
4425
  let routes = inputRoutes;
4219
4426
  const ctx = createExtensionContext(config, () => routes);
@@ -4270,69 +4477,29 @@ async function generate(config, inputRoutes = []) {
4270
4477
  if (extensions.length > 0) {
4271
4478
  const extraFiles = await collectEmittedFiles(extensions, ctx);
4272
4479
  for (const file of extraFiles) {
4273
- const dest = (0, import_node_path14.join)(config.codegen.outDir, file.path);
4274
- await (0, import_promises11.mkdir)((0, import_node_path14.dirname)(dest), { recursive: true });
4275
- await (0, import_promises11.writeFile)(dest, file.contents, "utf8");
4276
- }
4277
- }
4278
- }
4279
-
4280
- // src/watch/lock-file.ts
4281
- var import_promises12 = require("fs/promises");
4282
- var import_promises13 = require("fs/promises");
4283
- var import_node_path15 = require("path");
4284
- var LOCK_FILE = ".watcher.lock";
4285
- function isProcessAlive(pid) {
4286
- try {
4287
- process.kill(pid, 0);
4288
- return true;
4289
- } catch {
4290
- return false;
4291
- }
4292
- }
4293
- async function acquireLock(outDir) {
4294
- await (0, import_promises13.mkdir)(outDir, { recursive: true });
4295
- const lockPath = (0, import_node_path15.join)(outDir, LOCK_FILE);
4296
- const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
4297
- try {
4298
- const fd = await (0, import_promises12.open)(lockPath, "wx");
4299
- await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
4300
- `, "utf8");
4301
- await fd.close();
4302
- } catch (err) {
4303
- if (err.code === "EEXIST") {
4304
- try {
4305
- const raw = await (0, import_promises13.readFile)(lockPath, "utf8");
4306
- const existing = JSON.parse(raw);
4307
- if (isProcessAlive(existing.pid)) return null;
4308
- await (0, import_promises13.unlink)(lockPath);
4309
- return acquireLock(outDir);
4310
- } catch {
4311
- return null;
4312
- }
4480
+ const dest = (0, import_node_path16.join)(config.codegen.outDir, file.path);
4481
+ await (0, import_promises14.mkdir)((0, import_node_path16.dirname)(dest), { recursive: true });
4482
+ await (0, import_promises14.writeFile)(dest, file.contents, "utf8");
4313
4483
  }
4314
- return null;
4315
4484
  }
4316
- return {
4317
- release: async () => {
4318
- try {
4319
- await (0, import_promises13.unlink)(lockPath);
4320
- } catch {
4321
- }
4322
- }
4323
- };
4485
+ const outputFiles = await listOutputFiles(config.codegen.outDir);
4486
+ await writeManifest(config.codegen.outDir, {
4487
+ version: VERSION,
4488
+ hash: inputsHash,
4489
+ files: outputFiles
4490
+ });
4324
4491
  }
4325
4492
 
4326
4493
  // src/watch/watcher.ts
4327
4494
  var PAGES_DEBOUNCE_MS = 150;
4328
4495
  var NO_OP_WATCHER = { close: async () => {
4329
4496
  } };
4330
- async function watch(config, onChange) {
4497
+ async function watch(config, onChange, options = {}) {
4331
4498
  const lock = await acquireLock(config.codegen.outDir);
4332
4499
  if (lock === null) {
4333
4500
  let holderPid = "unknown";
4334
4501
  try {
4335
- const raw = await (0, import_promises14.readFile)((0, import_node_path16.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
4502
+ const raw = await (0, import_promises15.readFile)((0, import_node_path17.join)(config.codegen.outDir, ".watcher.lock"), "utf8");
4336
4503
  const data = JSON.parse(raw);
4337
4504
  if (data.pid !== void 0) holderPid = String(data.pid);
4338
4505
  } catch {
@@ -4355,22 +4522,33 @@ async function watch(config, onChange) {
4355
4522
  }
4356
4523
  return discovery;
4357
4524
  }
4358
- try {
4359
- const initialRoutes = (await getDiscovery()).discover();
4360
- lastRoutes = initialRoutes;
4361
- await generate(config, initialRoutes);
4362
- } catch (err) {
4363
- console.warn(
4364
- `[nestjs-codegen] Initial route discovery failed, falling back to pages-only: ${err instanceof Error ? err.message : String(err)}`
4365
- );
4525
+ async function runInitialPass() {
4366
4526
  try {
4367
- await generate(config, lastRoutes);
4368
- } catch {
4527
+ const initialRoutes = (await getDiscovery()).discover();
4528
+ lastRoutes = initialRoutes;
4529
+ await generate(config, initialRoutes);
4530
+ } catch (err) {
4531
+ console.warn(
4532
+ `[nestjs-codegen] Initial route discovery failed, falling back to pages-only: ${err instanceof Error ? err.message : String(err)}`
4533
+ );
4534
+ try {
4535
+ await generate(config, lastRoutes);
4536
+ } catch {
4537
+ }
4369
4538
  }
4370
4539
  }
4540
+ if (options.deferInitialGenerate) {
4541
+ void runInitialPass().catch((err) => {
4542
+ console.warn(
4543
+ `[nestjs-codegen] Background initial generate failed: ${err instanceof Error ? err.message : String(err)}`
4544
+ );
4545
+ });
4546
+ } else {
4547
+ await runInitialPass();
4548
+ }
4371
4549
  let pagesDebounceTimer;
4372
4550
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
4373
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, pagesGlob), {
4551
+ const pagesWatcher = import_chokidar.default.watch((0, import_node_path17.join)(config.codegen.cwd, pagesGlob), {
4374
4552
  ignoreInitial: true,
4375
4553
  persistent: true,
4376
4554
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4397,7 +4575,7 @@ async function watch(config, onChange) {
4397
4575
  pagesWatcher.on("unlink", schedulePagesRegenerate);
4398
4576
  let contractsDebounceTimer;
4399
4577
  const pendingChangedPaths = /* @__PURE__ */ new Set();
4400
- const contractsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.contracts.glob), {
4578
+ const contractsWatcher = import_chokidar.default.watch((0, import_node_path17.join)(config.codegen.cwd, config.contracts.glob), {
4401
4579
  ignoreInitial: true,
4402
4580
  persistent: true,
4403
4581
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4427,7 +4605,7 @@ async function watch(config, onChange) {
4427
4605
  contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4428
4606
  contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4429
4607
  contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4430
- const formsWatcher = import_chokidar.default.watch((0, import_node_path16.join)(config.codegen.cwd, config.forms.watch), {
4608
+ const formsWatcher = import_chokidar.default.watch((0, import_node_path17.join)(config.codegen.cwd, config.forms.watch), {
4431
4609
  ignoreInitial: true,
4432
4610
  persistent: true,
4433
4611
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4455,9 +4633,13 @@ async function watch(config, onChange) {
4455
4633
 
4456
4634
  // src/nest/module.ts
4457
4635
  var CODEGEN_MODULE_OPTIONS = Symbol("NESTJS_CODEGEN_MODULE_OPTIONS");
4636
+ function isProductionEnv(env) {
4637
+ return (env ?? "").trim().toLowerCase() === "production";
4638
+ }
4458
4639
  function shouldRun(options, env) {
4459
4640
  if (options.enabled !== void 0) return options.enabled;
4460
- return env !== "production";
4641
+ if (isProductionEnv(env)) return options.runInProduction === true;
4642
+ return true;
4461
4643
  }
4462
4644
  var NestjsCodegenService = class {
4463
4645
  constructor(options) {
@@ -4466,11 +4648,21 @@ var NestjsCodegenService = class {
4466
4648
  logger = new import_common.Logger("NestjsCodegen");
4467
4649
  watcher = null;
4468
4650
  async onApplicationBootstrap() {
4469
- if (!shouldRun(this.options, process.env.NODE_ENV)) return;
4470
- const { enabled: _enabled, cwd, ...userConfig } = this.options;
4651
+ if (!shouldRun(this.options, process.env.NODE_ENV)) {
4652
+ if (this.options.enabled === void 0 && isProductionEnv(process.env.NODE_ENV)) {
4653
+ this.logger.log("Skipped in production (set runInProduction: true to override).");
4654
+ }
4655
+ return;
4656
+ }
4657
+ const {
4658
+ enabled: _enabled,
4659
+ runInProduction: _runInProduction,
4660
+ cwd,
4661
+ ...userConfig
4662
+ } = this.options;
4471
4663
  try {
4472
4664
  const config = resolveConfig(userConfig, cwd ?? process.cwd());
4473
- this.watcher = await watch(config);
4665
+ this.watcher = await watch(config, void 0, { deferInitialGenerate: true });
4474
4666
  this.logger.log(`Watching ${config.contracts.glob} \u2192 ${config.codegen.outDir}`);
4475
4667
  } catch (err) {
4476
4668
  this.logger.warn(