UNPKG

@avleon/cli

Version:

> **🚧 This project is in active development.** > > It is **not stable** and **not ready** for live environments. > Use **only for testing, experimentation, or internal evaluation**. > > ####❗ Risks of using this in production: > > - 🔄 Breaking changes

291 lines (290 loc) 10 kB
#!/usr/bin/env node const chokidar = require("chokidar"); const { spawn } = require("child_process"); const path = require("path"); const fs = require("fs"); const { transformFileSync } = require("@swc/core"); const SRC_DIR = path.resolve(process.cwd(), "../src"); const DIST_DIR = path.resolve(process.cwd(), "../dist"); const ENTRY_FILE = path.join(DIST_DIR, "server.js"); let nodeProcess = null; let isRestarting = false; // Unified logging with colors and timestamps function log(category, message, color = '\x1b[37m') { const timestamp = new Date().toTimeString().split(' ')[0]; console.log(`\x1b[90m[${timestamp}]\x1b[0m ${color}[${category}]\x1b[0m ${message}`); } function logCompile(message) { log('SWC', message, '\x1b[36m'); } function logServer(message) { log('SERVER', message, '\x1b[32m'); } function logWatch(message) { log('WATCH', message, '\x1b[33m'); } function logError(category, message) { log(category, message, '\x1b[31m'); } // compile single TS file function compileFile(filePath) { try { const rel = path.relative(SRC_DIR, filePath); const outFile = path.join(DIST_DIR, rel).replace(/\.ts$/, ".js"); const { code } = transformFileSync(filePath, { jsc: { parser: { syntax: "typescript", decorators: true }, target: "es2022", transform: { decoratorMetadata: true }, }, module: { type: "commonjs" }, sourceMaps: false, }); fs.mkdirSync(path.dirname(outFile), { recursive: true }); fs.writeFileSync(outFile, code, "utf8"); logCompile(`✓ ${rel}`); return true; } catch (error) { logError('SWC', `✗ ${path.relative(SRC_DIR, filePath)} - ${error.message}`); return false; } } // compile all TS function compileAll(dir = SRC_DIR) { if (!fs.existsSync(dir)) { logError('SWC', `Source directory ${dir} does not exist`); return 0; } let compiled = 0; const files = fs.readdirSync(dir, { withFileTypes: true }); for (const f of files) { const full = path.join(dir, f.name); if (f.isDirectory()) { compiled += compileAll(full); } else if (f.name.endsWith(".ts")) { if (f.name.includes('.test.') || f.name.includes('.spec.')) { logWatch(`⏭️ Skipping test file: ${path.relative(SRC_DIR, full)}`); continue; } if (compileFile(full)) compiled++; } } return compiled; } // start Node with integrated output function startNode() { if (nodeProcess) { logServer("⚠️ Process already running, skipping start"); return; } if (isRestarting) { // Reset the flag when actually starting isRestarting = false; } if (!fs.existsSync(ENTRY_FILE)) { logError('SERVER', `Entry file ${ENTRY_FILE} does not exist`); return; } logServer("🚀 Starting server..."); try { nodeProcess = spawn("node", [ENTRY_FILE], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, FORCE_COLOR: '1' }, detached: false // Ensure process stays attached }); // Forward stdout with server prefix nodeProcess.stdout?.on('data', (data) => { const lines = data.toString().split('\n').filter(line => line.trim()); lines.forEach(line => logServer(line)); }); // Forward stderr with server prefix nodeProcess.stderr?.on('data', (data) => { const lines = data.toString().split('\n').filter(line => line.trim()); lines.forEach(line => logError('SERVER', line)); }); nodeProcess.on("exit", (code, signal) => { const wasRestarting = isRestarting; nodeProcess = null; if (wasRestarting) { // This is expected during restart return; } if (code && code !== 0) { logError('SERVER', `Process exited with code ${code}`); } else if (signal) { logServer(`Process terminated by signal ${signal}`); } else { // logServer("Process exited normally"); } isRestarting = false; }); nodeProcess.on("error", (error) => { logError('SERVER', `Process error: ${error.message}`); nodeProcess = null; isRestarting = false; }); // Add a small delay to let the server start setTimeout(() => { if (nodeProcess) { logServer("✅ Server started successfully"); } }, 500); } catch (error) { logError('SERVER', `Failed to start server: ${error.message}`); nodeProcess = null; isRestarting = false; } } // kill Node function killNode() { return new Promise((resolve) => { if (!nodeProcess) return resolve(); logServer("🛑 Stopping server..."); let resolved = false; const cleanup = () => { if (!resolved) { resolved = true; nodeProcess = null; logServer("✓ Server stopped"); resolve(); } }; nodeProcess.once("exit", cleanup); nodeProcess.once("close", cleanup); // Try graceful shutdown first try { nodeProcess.kill("SIGTERM"); } catch (error) { logError('SERVER', `Error sending SIGTERM: ${error.message}`); } // Force kill after 1 second if still running const forceKillTimeout = setTimeout(() => { if (nodeProcess && !resolved) { logServer("⚠️ Force killing server..."); try { nodeProcess.kill("SIGKILL"); } catch (error) { logError('SERVER', `Error sending SIGKILL: ${error.message}`); } // Ensure cleanup happens even if SIGKILL fails setTimeout(cleanup, 100); } }, 1000); // Fallback timeout - always resolve within 3 seconds setTimeout(() => { clearTimeout(forceKillTimeout); if (!resolved) { logError('SERVER', 'Force cleanup - process may still be running'); nodeProcess = null; resolved = true; resolve(); } }, 3000); }); } // restart Node async function restartNode() { if (isRestarting) { logServer("⏳ Already restarting, skipping..."); return; } isRestarting = true; logServer("🔄 Restarting server..."); try { await killNode(); // Add delay to ensure process is fully cleaned up await new Promise(resolve => setTimeout(resolve, 200)); startNode(); } catch (error) { logError('SERVER', `Restart error: ${error.message}`); isRestarting = false; } } // watch src function watch() { logWatch(`👀 Watching ${path.relative(process.cwd(), SRC_DIR)} for changes...`); // Add debug logging logWatch(`📂 Source directory: ${SRC_DIR}`); logWatch(`📂 Output directory: ${DIST_DIR}`); const watcher = chokidar.watch(SRC_DIR, { ignoreInitial: true, ignored: /(^|[\/\\])\../, // ignore dotfiles persistent: true, followSymlinks: false, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 } }); // Debug: log when watcher is ready watcher.on('ready', () => { logWatch('✅ Initial scan complete. Ready for changes'); }); watcher.on("add", (file) => { const rel = path.relative(SRC_DIR, file); logWatch(`📄 File added: ${rel}`); if (compileFile(file)) { // For new files, restart if it's a .ts file if (file.endsWith('.ts')) { setTimeout(() => restartNode(), 100); } } }); watcher.on("change", async (file) => { const rel = path.relative(SRC_DIR, file); logWatch(`📝 File changed: ${rel}`); if (file.endsWith('.ts') && compileFile(file)) { await restartNode(); } }); watcher.on("unlink", async (file) => { const rel = path.relative(SRC_DIR, file); logWatch(`🗑️ File removed: ${rel}`); const outFile = path .join(DIST_DIR, path.relative(SRC_DIR, file)) .replace(/\.ts$/, ".js"); if (fs.existsSync(outFile)) { fs.unlinkSync(outFile); logCompile(`✗ Removed ${path.relative(DIST_DIR, outFile)}`); } await restartNode(); }); // Debug: log all watcher events watcher.on('all', (eventName, path) => { if (!['add', 'change', 'unlink'].includes(eventName)) { logWatch(`🔍 Debug: ${eventName} -> ${path}`); } }); watcher.on("error", (error) => { logError('WATCH', `Watcher error: ${error.message}`); }); } // Graceful shutdown process.on("SIGINT", async () => { console.log(); logWatch("🛑 Shutting down..."); await killNode(); process.exit(0); }); process.on("SIGTERM", async () => { console.log(); logWatch("🛑 Shutting down..."); await killNode(); process.exit(0); }); // Main execution (async () => { console.log('\x1b[1m\x1b[36m🔧 Development Server\x1b[0m'); console.log('━'.repeat(50)); logWatch("🚀 Starting development server..."); const compiled = compileAll(); if (compiled > 0) { logCompile(`✅ Compiled ${compiled} file${compiled !== 1 ? 's' : ''}`); } startNode(); watch(); console.log('━'.repeat(50)); logWatch("✨ Ready! Press Ctrl+C to stop"); })();