From 39de1796c2462c037e573b7f6a0ccb0668860f8d Mon Sep 17 00:00:00 2001 From: ErnestHysa Date: Sat, 30 May 2026 22:06:57 +0100 Subject: [PATCH] feat: add hot reload for development mode --- package.json | 3 +- src/watch.ts | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/watch.ts diff --git a/package.json b/package.json index 3310fd40..b8a2165c 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "pm2:delete": "pm2 delete process.json", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "dev": "npm run build && node --enable-source-maps dist/watch.js" }, "dependencies": { "@discordjs/rest": "^2.6.0", diff --git a/src/watch.ts b/src/watch.ts new file mode 100644 index 00000000..75c32961 --- /dev/null +++ b/src/watch.ts @@ -0,0 +1,117 @@ +import { spawn } from 'node:child_process'; +import { watch } from 'node:fs'; +import { basename } from 'node:path'; + +const BOT_ENTRY = 'dist/start-bot.js'; +const SRC_DIR = 'src'; + +// Track running bot process +let botProcess: ReturnType | null = null; +let isRestarting = false; + +function log(message: string): void { + console.log(`[Watch] ${message}`); +} + +function startBot(): void { + if (botProcess) { + log('Restarting bot...'); + botProcess.kill('SIGTERM'); + botProcess = null; + } + + log('Starting bot...'); + botProcess = spawn('node', ['--enable-source-maps', BOT_ENTRY], { + stdio: 'inherit', + shell: true, + }); + + botProcess.on('exit', (code, signal) => { + if (signal === 'SIGTERM' || code === 143) { + // Expected termination (our restart) + return; + } + log(`Bot exited with code ${code}, signal ${signal}`); + if (!isRestarting) { + log('Bot crashed. Fix the error and save a file to restart.'); + } + }); + + botProcess.on('error', (err) => { + log(`Bot process error: ${err.message}`); + }); +} + +function buildAndStart(): void { + if (isRestarting) return; + isRestarting = true; + + log('File change detected, rebuilding...'); + + const build = spawn('npm', ['run', 'build'], { + stdio: 'inherit', + shell: true, + }); + + build.on('exit', (code) => { + if (code === 0) { + startBot(); + } else { + log(`Build failed with code ${code}. Waiting for file changes to retry...`); + } + isRestarting = false; + }); + + build.on('error', (err) => { + log(`Build error: ${err.message}`); + isRestarting = false; + }); +} + +// Watch source directory +log(`Watching ${SRC_DIR}/ for changes...`); + +const watcher = watch(SRC_DIR, { recursive: true }, (eventType, filename) => { + if (!filename) return; + + // Ignore non-TypeScript files and generated files + const ignored = [ + '.js.map', + '.d.ts', + '.d.ts.map', + ]; + + const ext = filename.split('.').pop(); + if (ext && !filename.endsWith('.ts') && ext !== 'cts' && ext !== 'mts') { + return; + } + + for (const ignore of ignored) { + if (filename.endsWith(ignore)) return; + } + + log(`Change detected in ${filename} (${eventType}), rebuilding...`); + buildAndStart(); +}); + +// Handle graceful shutdown +process.on('SIGINT', () => { + log('Shutting down watcher...'); + if (botProcess) { + botProcess.kill('SIGTERM'); + } + watcher.close(); + process.exit(0); +}); + +process.on('SIGTERM', () => { + log('Shutting down watcher...'); + if (botProcess) { + botProcess.kill('SIGTERM'); + } + watcher.close(); + process.exit(0); +}); + +// Start the bot initially +startBot();