@dudousxd/nestjs-codegen 0.11.0 → 0.12.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.
@@ -33,11 +33,21 @@ interface CodegenModuleOptions extends Omit<UserConfig, 'validation'> {
33
33
  validation?: UserConfig['validation'];
34
34
  /**
35
35
  * Master switch for the boot-time watcher. When omitted, the watcher runs in every
36
- * environment EXCEPT production (`process.env.NODE_ENV === 'production'`) — codegen is a
37
- * dev/CI build step, not a production-runtime concern. Set `false` to disable entirely,
38
- * or `true` to force it on even in production.
36
+ * environment EXCEPT production — codegen is a dev/CI build step, not a
37
+ * production-runtime concern. Set `false` to disable entirely, or `true` to force it
38
+ * on even in production (overrides {@link runInProduction}).
39
39
  */
40
40
  enabled?: boolean;
41
+ /**
42
+ * Run the boot-time watcher even when `NODE_ENV` is `production`. Codegen is a
43
+ * dev-time concern — in production the artifacts are already built by the CLI in the
44
+ * build pipeline — so this defaults to `false` (skip in production). Set `true` only
45
+ * if you genuinely need to regenerate at production runtime. Ignored when `enabled`
46
+ * is set explicitly.
47
+ *
48
+ * @default false
49
+ */
50
+ runInProduction?: boolean;
41
51
  /** Project root used to resolve globs / `outDir`. Defaults to `process.cwd()`. */
42
52
  cwd?: string;
43
53
  }
@@ -45,7 +55,8 @@ interface CodegenModuleOptions extends Omit<UserConfig, 'validation'> {
45
55
  declare const CODEGEN_MODULE_OPTIONS: unique symbol;
46
56
  /**
47
57
  * Decide whether the boot-time watcher should start, given the module options and the
48
- * current `NODE_ENV`. Explicit `enabled` always wins; otherwise default on unless prod.
58
+ * current `NODE_ENV`. Explicit `enabled` always wins; otherwise default on everywhere
59
+ * except production, where it stays off unless `runInProduction` is set.
49
60
  */
50
61
  declare function shouldRun(options: CodegenModuleOptions, env: string | undefined): boolean;
51
62
  /**
@@ -33,11 +33,21 @@ interface CodegenModuleOptions extends Omit<UserConfig, 'validation'> {
33
33
  validation?: UserConfig['validation'];
34
34
  /**
35
35
  * Master switch for the boot-time watcher. When omitted, the watcher runs in every
36
- * environment EXCEPT production (`process.env.NODE_ENV === 'production'`) — codegen is a
37
- * dev/CI build step, not a production-runtime concern. Set `false` to disable entirely,
38
- * or `true` to force it on even in production.
36
+ * environment EXCEPT production — codegen is a dev/CI build step, not a
37
+ * production-runtime concern. Set `false` to disable entirely, or `true` to force it
38
+ * on even in production (overrides {@link runInProduction}).
39
39
  */
40
40
  enabled?: boolean;
41
+ /**
42
+ * Run the boot-time watcher even when `NODE_ENV` is `production`. Codegen is a
43
+ * dev-time concern — in production the artifacts are already built by the CLI in the
44
+ * build pipeline — so this defaults to `false` (skip in production). Set `true` only
45
+ * if you genuinely need to regenerate at production runtime. Ignored when `enabled`
46
+ * is set explicitly.
47
+ *
48
+ * @default false
49
+ */
50
+ runInProduction?: boolean;
41
51
  /** Project root used to resolve globs / `outDir`. Defaults to `process.cwd()`. */
42
52
  cwd?: string;
43
53
  }
@@ -45,7 +55,8 @@ interface CodegenModuleOptions extends Omit<UserConfig, 'validation'> {
45
55
  declare const CODEGEN_MODULE_OPTIONS: unique symbol;
46
56
  /**
47
57
  * Decide whether the boot-time watcher should start, given the module options and the
48
- * current `NODE_ENV`. Explicit `enabled` always wins; otherwise default on unless prod.
58
+ * current `NODE_ENV`. Explicit `enabled` always wins; otherwise default on everywhere
59
+ * except production, where it stays off unless `runInProduction` is set.
49
60
  */
50
61
  declare function shouldRun(options: CodegenModuleOptions, env: string | undefined): boolean;
51
62
  /**
@@ -138,8 +138,8 @@ function applyDefaults(userConfig, cwd) {
138
138
  }
139
139
 
140
140
  // src/watch/watcher.ts
141
- import { readFile as readFile3 } from "fs/promises";
142
- import { join as join15 } from "path";
141
+ import { readFile as readFile4 } from "fs/promises";
142
+ import { join as join16 } from "path";
143
143
  import chokidar from "chokidar";
144
144
 
145
145
  // src/discovery/contracts-fast.ts
@@ -2294,8 +2294,8 @@ function extractFromSourceFile(sourceFile, project) {
2294
2294
  }
2295
2295
 
2296
2296
  // src/generate.ts
2297
- import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
2298
- import { dirname as dirname3, join as join13 } from "path";
2297
+ import { mkdir as mkdir10, writeFile as writeFile10 } from "fs/promises";
2298
+ import { dirname as dirname3, join as join15 } from "path";
2299
2299
 
2300
2300
  // src/discovery/pages.ts
2301
2301
  import { readFile } from "fs/promises";
@@ -4190,9 +4190,159 @@ function buildEmpty() {
4190
4190
  ].join("\n");
4191
4191
  }
4192
4192
 
4193
+ // src/generate-manifest.ts
4194
+ import { createHash } from "crypto";
4195
+ import { readFile as readFile3, readdir, writeFile as writeFile9 } from "fs/promises";
4196
+ import { join as join14, relative as relative6 } from "path";
4197
+ import fg3 from "fast-glob";
4198
+
4199
+ // src/watch/lock-file.ts
4200
+ import { open } from "fs/promises";
4201
+ import { mkdir as mkdir9, readFile as readFile2, unlink } from "fs/promises";
4202
+ import { join as join13 } from "path";
4203
+ var LOCK_FILE = ".watcher.lock";
4204
+ function isProcessAlive(pid) {
4205
+ try {
4206
+ process.kill(pid, 0);
4207
+ return true;
4208
+ } catch {
4209
+ return false;
4210
+ }
4211
+ }
4212
+ async function acquireLock(outDir) {
4213
+ await mkdir9(outDir, { recursive: true });
4214
+ const lockPath = join13(outDir, LOCK_FILE);
4215
+ const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
4216
+ try {
4217
+ const fd = await open(lockPath, "wx");
4218
+ await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
4219
+ `, "utf8");
4220
+ await fd.close();
4221
+ } catch (err) {
4222
+ if (err.code === "EEXIST") {
4223
+ try {
4224
+ const raw = await readFile2(lockPath, "utf8");
4225
+ const existing = JSON.parse(raw);
4226
+ if (isProcessAlive(existing.pid)) return null;
4227
+ await unlink(lockPath);
4228
+ return acquireLock(outDir);
4229
+ } catch {
4230
+ return null;
4231
+ }
4232
+ }
4233
+ return null;
4234
+ }
4235
+ return {
4236
+ release: async () => {
4237
+ try {
4238
+ await unlink(lockPath);
4239
+ } catch {
4240
+ }
4241
+ }
4242
+ };
4243
+ }
4244
+
4245
+ // src/index.ts
4246
+ var VERSION = "0.12.0";
4247
+
4248
+ // src/generate-manifest.ts
4249
+ var MANIFEST_FILE = ".codegen-manifest.json";
4250
+ var LOCK_FILE2 = ".watcher.lock";
4251
+ function isManifestShape(value) {
4252
+ if (typeof value !== "object" || value === null) return false;
4253
+ const candidate = value;
4254
+ if (typeof candidate.version !== "string") return false;
4255
+ if (typeof candidate.hash !== "string") return false;
4256
+ if (!Array.isArray(candidate.files)) return false;
4257
+ return candidate.files.every((entry) => typeof entry === "string");
4258
+ }
4259
+ function serializeConfig(config) {
4260
+ try {
4261
+ return JSON.stringify(config, (_key, value) => {
4262
+ if (typeof value === "function") return `[fn:${value.name}]${value.toString()}`;
4263
+ return value;
4264
+ });
4265
+ } catch {
4266
+ return `unserializable:${config.codegen.outDir}:${config.contracts.glob}`;
4267
+ }
4268
+ }
4269
+ async function discoverInputFiles(config) {
4270
+ const globs = [config.contracts.glob, config.forms.watch];
4271
+ if (config.pages) globs.push(config.pages.glob);
4272
+ const cwd = config.codegen.cwd;
4273
+ const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
4274
+ return [...new Set(matched)].sort();
4275
+ }
4276
+ async function computeInputsHash(config) {
4277
+ const hash = createHash("sha256");
4278
+ hash.update(`version:${VERSION}
4279
+ `);
4280
+ hash.update(`config:${serializeConfig(config)}
4281
+ `);
4282
+ const inputFiles = await discoverInputFiles(config);
4283
+ const cwd = config.codegen.cwd;
4284
+ for (const file of inputFiles) {
4285
+ const contents = await readFile3(file, "utf8");
4286
+ hash.update(`file:${relative6(cwd, file)}
4287
+ `);
4288
+ hash.update(contents);
4289
+ hash.update("\n");
4290
+ }
4291
+ return hash.digest("hex");
4292
+ }
4293
+ async function readManifest(outDir) {
4294
+ try {
4295
+ const raw = await readFile3(join14(outDir, MANIFEST_FILE), "utf8");
4296
+ const parsed = JSON.parse(raw);
4297
+ if (!isManifestShape(parsed)) return null;
4298
+ return { version: parsed.version, hash: parsed.hash, files: parsed.files };
4299
+ } catch {
4300
+ return null;
4301
+ }
4302
+ }
4303
+ async function writeManifest(outDir, manifest) {
4304
+ await writeFile9(join14(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
4305
+ `, "utf8");
4306
+ }
4307
+ async function listOutputFiles(outDir) {
4308
+ const found = [];
4309
+ async function walk(dir) {
4310
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
4311
+ for (const entry of entries) {
4312
+ const abs = join14(dir, entry.name);
4313
+ if (entry.isDirectory()) {
4314
+ await walk(abs);
4315
+ } else if (entry.isFile()) {
4316
+ const rel = relative6(outDir, abs);
4317
+ if (rel === MANIFEST_FILE || rel === LOCK_FILE2) continue;
4318
+ found.push(rel);
4319
+ }
4320
+ }
4321
+ }
4322
+ await walk(outDir);
4323
+ return found.sort();
4324
+ }
4325
+ async function allOutputsExist(outDir, files) {
4326
+ const present = new Set(await listOutputFiles(outDir));
4327
+ return files.every((file) => present.has(file));
4328
+ }
4329
+ async function isManifestFresh(outDir, manifest, inputsHash) {
4330
+ if (manifest === null) return false;
4331
+ if (manifest.version !== VERSION) return false;
4332
+ if (manifest.hash !== inputsHash) return false;
4333
+ if (manifest.files.length === 0) return false;
4334
+ return allOutputsExist(outDir, manifest.files);
4335
+ }
4336
+
4193
4337
  // src/generate.ts
4194
4338
  async function generate(config, inputRoutes = []) {
4195
4339
  setCodegenDebug(config.debug);
4340
+ const inputsHash = await computeInputsHash(config);
4341
+ const manifest = await readManifest(config.codegen.outDir);
4342
+ if (await isManifestFresh(config.codegen.outDir, manifest, inputsHash)) {
4343
+ console.log(`[nestjs-codegen] ${config.codegen.outDir} up to date, skipped`);
4344
+ return;
4345
+ }
4196
4346
  const extensions = config.extensions ?? [];
4197
4347
  let routes = inputRoutes;
4198
4348
  const ctx = createExtensionContext(config, () => routes);
@@ -4249,69 +4399,29 @@ async function generate(config, inputRoutes = []) {
4249
4399
  if (extensions.length > 0) {
4250
4400
  const extraFiles = await collectEmittedFiles(extensions, ctx);
4251
4401
  for (const file of extraFiles) {
4252
- const dest = join13(config.codegen.outDir, file.path);
4253
- await mkdir9(dirname3(dest), { recursive: true });
4254
- await writeFile9(dest, file.contents, "utf8");
4402
+ const dest = join15(config.codegen.outDir, file.path);
4403
+ await mkdir10(dirname3(dest), { recursive: true });
4404
+ await writeFile10(dest, file.contents, "utf8");
4255
4405
  }
4256
4406
  }
4257
- }
4258
-
4259
- // src/watch/lock-file.ts
4260
- import { open } from "fs/promises";
4261
- import { mkdir as mkdir10, readFile as readFile2, unlink } from "fs/promises";
4262
- import { join as join14 } from "path";
4263
- var LOCK_FILE = ".watcher.lock";
4264
- function isProcessAlive(pid) {
4265
- try {
4266
- process.kill(pid, 0);
4267
- return true;
4268
- } catch {
4269
- return false;
4270
- }
4271
- }
4272
- async function acquireLock(outDir) {
4273
- await mkdir10(outDir, { recursive: true });
4274
- const lockPath = join14(outDir, LOCK_FILE);
4275
- const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
4276
- try {
4277
- const fd = await open(lockPath, "wx");
4278
- await fd.writeFile(`${JSON.stringify(lockData, null, 2)}
4279
- `, "utf8");
4280
- await fd.close();
4281
- } catch (err) {
4282
- if (err.code === "EEXIST") {
4283
- try {
4284
- const raw = await readFile2(lockPath, "utf8");
4285
- const existing = JSON.parse(raw);
4286
- if (isProcessAlive(existing.pid)) return null;
4287
- await unlink(lockPath);
4288
- return acquireLock(outDir);
4289
- } catch {
4290
- return null;
4291
- }
4292
- }
4293
- return null;
4294
- }
4295
- return {
4296
- release: async () => {
4297
- try {
4298
- await unlink(lockPath);
4299
- } catch {
4300
- }
4301
- }
4302
- };
4407
+ const outputFiles = await listOutputFiles(config.codegen.outDir);
4408
+ await writeManifest(config.codegen.outDir, {
4409
+ version: VERSION,
4410
+ hash: inputsHash,
4411
+ files: outputFiles
4412
+ });
4303
4413
  }
4304
4414
 
4305
4415
  // src/watch/watcher.ts
4306
4416
  var PAGES_DEBOUNCE_MS = 150;
4307
4417
  var NO_OP_WATCHER = { close: async () => {
4308
4418
  } };
4309
- async function watch(config, onChange) {
4419
+ async function watch(config, onChange, options = {}) {
4310
4420
  const lock = await acquireLock(config.codegen.outDir);
4311
4421
  if (lock === null) {
4312
4422
  let holderPid = "unknown";
4313
4423
  try {
4314
- const raw = await readFile3(join15(config.codegen.outDir, ".watcher.lock"), "utf8");
4424
+ const raw = await readFile4(join16(config.codegen.outDir, ".watcher.lock"), "utf8");
4315
4425
  const data = JSON.parse(raw);
4316
4426
  if (data.pid !== void 0) holderPid = String(data.pid);
4317
4427
  } catch {
@@ -4334,22 +4444,33 @@ async function watch(config, onChange) {
4334
4444
  }
4335
4445
  return discovery;
4336
4446
  }
4337
- try {
4338
- const initialRoutes = (await getDiscovery()).discover();
4339
- lastRoutes = initialRoutes;
4340
- await generate(config, initialRoutes);
4341
- } catch (err) {
4342
- console.warn(
4343
- `[nestjs-codegen] Initial route discovery failed, falling back to pages-only: ${err instanceof Error ? err.message : String(err)}`
4344
- );
4447
+ async function runInitialPass() {
4345
4448
  try {
4346
- await generate(config, lastRoutes);
4347
- } catch {
4449
+ const initialRoutes = (await getDiscovery()).discover();
4450
+ lastRoutes = initialRoutes;
4451
+ await generate(config, initialRoutes);
4452
+ } catch (err) {
4453
+ console.warn(
4454
+ `[nestjs-codegen] Initial route discovery failed, falling back to pages-only: ${err instanceof Error ? err.message : String(err)}`
4455
+ );
4456
+ try {
4457
+ await generate(config, lastRoutes);
4458
+ } catch {
4459
+ }
4348
4460
  }
4349
4461
  }
4462
+ if (options.deferInitialGenerate) {
4463
+ void runInitialPass().catch((err) => {
4464
+ console.warn(
4465
+ `[nestjs-codegen] Background initial generate failed: ${err instanceof Error ? err.message : String(err)}`
4466
+ );
4467
+ });
4468
+ } else {
4469
+ await runInitialPass();
4470
+ }
4350
4471
  let pagesDebounceTimer;
4351
4472
  const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
4352
- const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
4473
+ const pagesWatcher = chokidar.watch(join16(config.codegen.cwd, pagesGlob), {
4353
4474
  ignoreInitial: true,
4354
4475
  persistent: true,
4355
4476
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4376,7 +4497,7 @@ async function watch(config, onChange) {
4376
4497
  pagesWatcher.on("unlink", schedulePagesRegenerate);
4377
4498
  let contractsDebounceTimer;
4378
4499
  const pendingChangedPaths = /* @__PURE__ */ new Set();
4379
- const contractsWatcher = chokidar.watch(join15(config.codegen.cwd, config.contracts.glob), {
4500
+ const contractsWatcher = chokidar.watch(join16(config.codegen.cwd, config.contracts.glob), {
4380
4501
  ignoreInitial: true,
4381
4502
  persistent: true,
4382
4503
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4406,7 +4527,7 @@ async function watch(config, onChange) {
4406
4527
  contractsWatcher.on("add", (p) => scheduleContractsRegenerate(p));
4407
4528
  contractsWatcher.on("change", (p) => scheduleContractsRegenerate(p));
4408
4529
  contractsWatcher.on("unlink", (p) => scheduleContractsRegenerate(p));
4409
- const formsWatcher = chokidar.watch(join15(config.codegen.cwd, config.forms.watch), {
4530
+ const formsWatcher = chokidar.watch(join16(config.codegen.cwd, config.forms.watch), {
4410
4531
  ignoreInitial: true,
4411
4532
  persistent: true,
4412
4533
  awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
@@ -4434,9 +4555,13 @@ async function watch(config, onChange) {
4434
4555
 
4435
4556
  // src/nest/module.ts
4436
4557
  var CODEGEN_MODULE_OPTIONS = Symbol("NESTJS_CODEGEN_MODULE_OPTIONS");
4558
+ function isProductionEnv(env) {
4559
+ return (env ?? "").trim().toLowerCase() === "production";
4560
+ }
4437
4561
  function shouldRun(options, env) {
4438
4562
  if (options.enabled !== void 0) return options.enabled;
4439
- return env !== "production";
4563
+ if (isProductionEnv(env)) return options.runInProduction === true;
4564
+ return true;
4440
4565
  }
4441
4566
  var NestjsCodegenService = class {
4442
4567
  constructor(options) {
@@ -4445,11 +4570,21 @@ var NestjsCodegenService = class {
4445
4570
  logger = new Logger("NestjsCodegen");
4446
4571
  watcher = null;
4447
4572
  async onApplicationBootstrap() {
4448
- if (!shouldRun(this.options, process.env.NODE_ENV)) return;
4449
- const { enabled: _enabled, cwd, ...userConfig } = this.options;
4573
+ if (!shouldRun(this.options, process.env.NODE_ENV)) {
4574
+ if (this.options.enabled === void 0 && isProductionEnv(process.env.NODE_ENV)) {
4575
+ this.logger.log("Skipped in production (set runInProduction: true to override).");
4576
+ }
4577
+ return;
4578
+ }
4579
+ const {
4580
+ enabled: _enabled,
4581
+ runInProduction: _runInProduction,
4582
+ cwd,
4583
+ ...userConfig
4584
+ } = this.options;
4450
4585
  try {
4451
4586
  const config = resolveConfig(userConfig, cwd ?? process.cwd());
4452
- this.watcher = await watch(config);
4587
+ this.watcher = await watch(config, void 0, { deferInitialGenerate: true });
4453
4588
  this.logger.log(`Watching ${config.contracts.glob} \u2192 ${config.codegen.outDir}`);
4454
4589
  } catch (err) {
4455
4590
  this.logger.warn(