UNPKG

fortify2-js

Version:

MOST POWERFUL JavaScript Security Library! Military-grade cryptography + 19 enhanced object methods + quantum-resistant algorithms + perfect TypeScript support. More powerful than Lodash with built-in security.

739 lines (735 loc) 29.7 kB
'use strict'; var childProcess = require('child_process'); var events = require('events'); var fs = require('fs'); var path = require('path'); var FileWatcher_config = require('../../const/FileWatcher.config.js'); var TypeScriptExecutor = require('./exec/TypeScriptExecutor.js'); var Logger = require('../../utils/Logger.js'); /** * Enhanced Hot Reloader with QuickDev Integration * Integrates with powerful Go-based QuickDev service with fallback to local implementation * Enables real hot reload by restarting the entire process */ class HotReloader extends events.EventEmitter { constructor(config = {}, FWConfig) { super(); this.isRunning = false; this.restartCount = 0; this.lastRestart = 0; this.isQuickDevAvailable = false; this.useQuickDev = true; // Prefer QuickDev by default this.config = { enabled: true, script: process.argv[1] || "index.js", args: process.argv.slice(2), env: { ...process.env }, cwd: process.cwd(), restartDelay: 500, maxRestarts: FileWatcher_config.DEFAULT_FW_CONFIG.maxRestarts, gracefulShutdownTimeout: 5000, verbose: false, typescript: { enabled: true, runner: "auto", runnerArgs: [], fallbackToNode: true, autoDetectRunner: true, enableRuntimeCompilation: true, }, ...config, }; this.FWConfig = FWConfig; // Initialize logger this.logger = new Logger.Logger({ enabled: true, level: this.config.verbose ? "debug" : "info", components: { fileWatcher: true }, types: { hotReload: true, startup: true, warnings: true, errors: true, }, }); // Initialize TypeScript executor this.tsExecutor = new TypeScriptExecutor.TypeScriptExecutor({ verbose: this.config.verbose, fallbackToNode: this.config.typescript?.fallbackToNode ?? true, compilerOptions: this.config.typescript?.compilerOptions, }); // Check for standalone executable this.checkStandaloneExecutable(); // Check QuickDev availability (synchronous check first) this.checkQuickDevAvailabilitySync(); // Then do async check for more thorough detection this.checkQuickDevAvailability(); } /** * Check if QuickDev service is available (synchronous check) */ checkQuickDevAvailabilitySync() { try { // Quick synchronous check using which/where command const { execSync } = require("child_process"); const command = process.platform === "win32" ? "where quickdev" : "which quickdev"; execSync(command, { stdio: "pipe" }); this.isQuickDevAvailable = true; this.logger.debug("fileWatcher", "QuickDev service detected (sync check)"); } catch (error) { this.isQuickDevAvailable = false; this.logger.debug("fileWatcher", "QuickDev service not found (sync check)"); } } /** * Check if QuickDev service is available (async verification) */ async checkQuickDevAvailability() { try { // Check if quickdev is installed globally const checkProcess = childProcess.spawn("quickdev", ["--help"], { stdio: "pipe", shell: true, }); checkProcess.on("close", (code) => { // Only update if sync check didn't already detect it if (!this.isQuickDevAvailable) { this.isQuickDevAvailable = code === 0; } if (this.isQuickDevAvailable) { this.logger.debug("fileWatcher", "QuickDev service detected and available"); } else { this.logger.debug("fileWatcher", "QuickDev service not available, will use fallback"); } }); checkProcess.on("error", () => { // Only update if sync check didn't already detect it if (!this.isQuickDevAvailable) { this.isQuickDevAvailable = false; this.logger.debug("fileWatcher", "QuickDev service not available, will use fallback"); } }); // Timeout after 2 seconds setTimeout(() => { if (checkProcess.pid) { checkProcess.kill(); // Only update if sync check didn't already detect it if (!this.isQuickDevAvailable) { this.isQuickDevAvailable = false; } } }, 2000); } catch (error) { this.isQuickDevAvailable = false; this.logger.debug("fileWatcher", "QuickDev service check failed, will use fallback"); } } /** * Check for standalone TypeScript executable */ checkStandaloneExecutable() { const executablePaths = [ path.join(process.cwd(), "dist", "tsr", process.platform === "win32" ? "tsr.cjs" : "tsr"), path.join(process.cwd(), "dist", "tsr", "tsr.cmd"), // Windows batch file path.join(__dirname, "executable", "bin", process.platform === "win32" ? "tsr.exe" : "tsr"), ]; for (const path of executablePaths) { if (fs.existsSync(path)) { this.standaloneExecutablePath = path; if (this.config.verbose) { console.log(`Found standalone TSR executable: ${path}`); } break; } } if (!this.standaloneExecutablePath && this.config.verbose) { console.log("No standalone TSR executable found, using TypeScript executor"); } } /** * Create QuickDev configuration file */ async createQuickDevConfig() { const configPath = path.join(process.cwd(), "quickdev.config.json"); const quickdevConfig = { script: this.config.script, watch: this.FWConfig.fileWatcher?.watchPaths || ["src", "config"], ignore: this.FWConfig.fileWatcher?.ignorePaths || [ "node_modules", "dist", "coverage", ".git", "build", ], extensions: this.FWConfig.fileWatcher?.extensions || [ ".ts", ".js", ".jsx", ".tsx", ], gracefulShutdown: true, gracefulShutdownTimeout: Math.floor(this.config.gracefulShutdownTimeout / 1000), maxRestarts: this.config.maxRestarts, resetRestartsAfter: 60000, restartDelay: this.config.restartDelay, batchChanges: true, batchTimeout: 300, enableHashing: true, usePolling: false, pollingInterval: 100, followSymlinks: false, watchDotFiles: false, ignoreFile: ".quickdevignore", parallelProcessing: true, memoryLimit: 500, maxFileSize: 10, excludeEmptyFiles: true, debounceMs: 250, healthCheck: true, healthCheckInterval: 30, clearScreen: true, typescriptRunner: this.config.typescript?.runner === "auto" ? undefined : this.config.typescript?.runner, tsNodeFlags: this.config.typescript?.runnerArgs?.join(" "), }; try { await fs.promises.writeFile(configPath, JSON.stringify(quickdevConfig, null, 2)); this.logger.debug("fileWatcher", `Created QuickDev config: ${configPath}`); this.quickdevConfigPath = configPath; return configPath; } catch (error) { this.logger.warn("fileWatcher", "Failed to create QuickDev config file:", error); throw error; } } /** * Start the hot reloader with QuickDev integration */ async start() { if (!this.config.enabled) { this.logger.info("fileWatcher", "Hot reloader disabled"); return; } if (this.isRunning) { this.logger.debug("fileWatcher", "Hot reloader already running"); return; } try { this.logger.info("fileWatcher", "Starting hot reloader..."); // Wait for QuickDev availability check to complete await new Promise((resolve) => setTimeout(resolve, 500)); if (this.useQuickDev && this.isQuickDevAvailable) { await this.startQuickDev(); } else { await this.startChildProcess(); } this.isRunning = true; this.logger.info("fileWatcher", "Hot reloader started"); } catch (error) { this.logger.error("fileWatcher", "Failed to start hot reloader:", error.message); // Try fallback if QuickDev fails if (this.useQuickDev && this.isQuickDevAvailable && !this.childProcess) { this.logger.info("fileWatcher", "QuickDev failed, trying fallback..."); this.isQuickDevAvailable = false; await this.startChildProcess(); this.isRunning = true; } else { throw error; } } } /** * Start QuickDev service */ async startQuickDev() { try { // Create configuration file await this.createQuickDevConfig(); this.logger.debug("fileWatcher", "Starting QuickDev service..."); // Start QuickDev process this.quickdevProcess = childProcess.spawn("quickdev", ["-script", this.config.script], { cwd: this.config.cwd, env: this.config.env, stdio: this.config.verbose ? "inherit" : "pipe", shell: true, }); return new Promise((resolve, reject) => { let resolved = false; // Set a timeout to prevent hanging const timeout = setTimeout(() => { if (!resolved) { resolved = true; this.logger.warn("fileWatcher", "QuickDev startup timeout, assuming success"); resolve(); } }, 5000); // 5 second timeout this.quickdevProcess.on("spawn", () => { if (!resolved) { resolved = true; clearTimeout(timeout); this.logger.debug("fileWatcher", `QuickDev service started (PID: ${this.quickdevProcess?.pid})`); resolve(); } }); this.quickdevProcess.on("error", (error) => { if (!resolved) { resolved = true; clearTimeout(timeout); this.logger.error("fileWatcher", "QuickDev service error:", error.message); reject(error); } }); this.quickdevProcess.on("exit", (code, signal) => { this.logger.debug("fileWatcher", `QuickDev service exited (code: ${code}, signal: ${signal})`); this.emit("process:exit", { code, signal }); // If process exits immediately, it might be an error if (!resolved && code !== 0) { resolved = true; clearTimeout(timeout); reject(new Error(`QuickDev process exited with code ${code}`)); return; } // Auto-restart on unexpected exit if (this.isRunning && code !== 0 && !signal) { this.logger.info("fileWatcher", "Unexpected exit, restarting..."); setTimeout(() => this.restart(), this.config.restartDelay); } }); // Also resolve if the process is spawned successfully (alternative check) setTimeout(() => { if (!resolved && this.quickdevProcess && this.quickdevProcess.pid) { resolved = true; clearTimeout(timeout); this.logger.debug("fileWatcher", `QuickDev service confirmed running (PID: ${this.quickdevProcess.pid})`); resolve(); } }, 1000); // Check after 1 second }); } catch (error) { throw new Error(`Failed to start QuickDev service: ${error}`); } } /** * Stop the hot reloader */ async stop() { if (!this.isRunning) return; try { this.logger.debug("fileWatcher", "Stopping hot reloader..."); if (this.quickdevProcess) { await this.stopQuickDev(); } if (this.childProcess) { await this.stopChildProcess(); } this.isRunning = false; this.logger.debug("fileWatcher", "Hot reloader stopped"); } catch (error) { this.logger.error("fileWatcher", "Error stopping hot reloader:", error.message); } } /** * Stop QuickDev service gracefully */ async stopQuickDev() { if (!this.quickdevProcess) return; return new Promise((resolve) => { const timeout = setTimeout(() => { if (this.quickdevProcess) { this.logger.debug("fileWatcher", "Force killing QuickDev service..."); this.quickdevProcess.kill("SIGKILL"); } resolve(); }, this.config.gracefulShutdownTimeout); this.quickdevProcess.on("exit", () => { clearTimeout(timeout); resolve(); }); // Try graceful shutdown first this.logger.debug("fileWatcher", "Sending SIGTERM to QuickDev service..."); this.quickdevProcess.kill("SIGTERM"); }); } /** * Restart the child process (hot reload) */ async restart() { if (!this.isRunning) { this.logger.info("fileWatcher", "Hot reloader not running, starting..."); await this.start(); return; } // Check restart limits const now = Date.now(); if (now - this.lastRestart < this.config.restartDelay) { this.logger.debug("fileWatcher", "Restart too soon, waiting..."); return; } if (this.restartCount >= this.config.maxRestarts) { this.logger.warn("fileWatcher", `Maximum restarts (${this.config.maxRestarts}) reached`); return; } try { this.logger.hotReload("fileWatcher", "Hot reloading process..."); const startTime = Date.now(); // Stop current process if (this.quickdevProcess) { await this.stopQuickDev(); } if (this.childProcess) { await this.stopChildProcess(); } // Wait for restart delay await new Promise((resolve) => setTimeout(resolve, this.config.restartDelay)); // Start new process if (this.useQuickDev && this.isQuickDevAvailable) { await this.startQuickDev(); } else { await this.startChildProcess(); } const duration = Date.now() - startTime; this.restartCount++; this.lastRestart = now; this.emit("restart:completed", { duration, restartCount: this.restartCount, }); this.logger.hotReload("fileWatcher", `Process hot reloaded (${duration}ms)`); } catch (error) { this.emit("restart:failed", { error: error.message }); this.logger.error("fileWatcher", "Hot reload failed:", error.message); } } /** * Check if the script is a TypeScript file */ isTypeScriptFile(script) { return script.endsWith(".ts") || script.endsWith(".tsx"); } /** * Get the appropriate runtime and arguments for the script using TypeScript executor */ async getRuntimeConfig() { const isTS = this.isTypeScriptFile(this.config.script); const tsConfig = this.config.typescript; // If TypeScript is disabled or not a TS file, use default behavior if (!tsConfig?.enabled || !isTS) { const runtime = process.execPath.includes("bun") ? "bun" : "node"; return { runtime, args: [this.config.script, ...this.config.args], }; } // Try standalone executable first (most reliable) if (this.standaloneExecutablePath) { this.logger.debug("fileWatcher", `Using standalone TSR executable: ${this.standaloneExecutablePath}`); // For Windows .cmd files, use node to execute the .cjs file if (this.standaloneExecutablePath.endsWith(".cmd")) { const cjsPath = this.standaloneExecutablePath.replace(".cmd", ".cjs"); return { runtime: "node", args: [cjsPath, this.config.script, ...this.config.args], }; } else if (this.standaloneExecutablePath.endsWith(".cjs")) { return { runtime: "node", args: [ this.standaloneExecutablePath, this.config.script, ...this.config.args, ], }; } else { // Unix executable return { runtime: this.standaloneExecutablePath, args: [this.config.script, ...this.config.args], }; } } try { // Use the TypeScript executor to determine the best execution method const result = await this.tsExecutor.executeTypeScript(this.config.script, this.config.args); if (result.success) { this.logger.debug("fileWatcher", `Using ${result.method}: ${result.runtime} ${result.args.join(" ")}`); return { runtime: result.runtime, args: result.args, }; } else { throw new Error(result.error || "TypeScript execution failed"); } } catch (error) { this.logger.warn("fileWatcher", `TypeScript executor failed: ${error.message}`); // Fallback to node if TypeScript executor fails if (tsConfig.fallbackToNode) { this.logger.warn("fileWatcher", "Falling back to node (may fail for TypeScript files)"); return { runtime: "node", args: [this.config.script, ...this.config.args], }; } else { throw error; } } } /** * Start child process */ async startChildProcess() { try { const { runtime, args } = await this.getRuntimeConfig(); this.logger.debug("fileWatcher", `Starting process with: ${runtime} ${args.join(" ")}`); return new Promise((resolve, reject) => { let resolved = false; this.childProcess = childProcess.spawn(runtime, args, { cwd: this.config.cwd, env: { ...this.config.env, FORTIFY_CHILD_PROCESS: "true", // Mark child processes }, stdio: "inherit", detached: false, }); // Set a timeout to prevent hanging const timeout = setTimeout(() => { if (!resolved) { resolved = true; this.logger.warn("fileWatcher", "Child process startup timeout, assuming success"); resolve(); } }, 5000); // 5 second timeout this.childProcess.on("spawn", () => { if (!resolved) { resolved = true; clearTimeout(timeout); this.logger.debug("fileWatcher", `Child process started (PID: ${this.childProcess?.pid})`); resolve(); } }); this.childProcess.on("error", async (error) => { if (!resolved) { resolved = true; clearTimeout(timeout); // Handle TypeScript runner not found error if (error.message.includes("ENOENT") && this.isTypeScriptFile(this.config.script)) { const tsConfig = this.config.typescript; if (tsConfig?.fallbackToNode) { this.logger.warn("fileWatcher", "TypeScript runner failed, falling back to node (this will likely fail for .ts files)"); this.logger.warn("fileWatcher", "Install a TypeScript runner: npm install -g tsx"); // Retry with node this.childProcess = childProcess.spawn("node", [this.config.script, ...this.config.args], { cwd: this.config.cwd, env: { ...this.config.env, FORTIFY_CHILD_PROCESS: "true", // Mark child processes }, stdio: "inherit", detached: false, }); return; } } this.logger.error("fileWatcher", "Child process error:", error.message); // Provide helpful error messages for common issues if (error.message.includes("ENOENT")) { try { const { runtime: errorRuntime } = await this.getRuntimeConfig(); this.logger.error("fileWatcher", `Runtime '${errorRuntime}' not found. Please install it:`); switch (errorRuntime) { case "tsx": this.logger.error("fileWatcher", " npm install -g tsx"); break; case "ts-node": this.logger.error("fileWatcher", " npm install -g ts-node"); break; case "bun": this.logger.error("fileWatcher", " Visit: https://bun.sh/docs/installation"); break; default: this.logger.error("fileWatcher", ` Make sure '${errorRuntime}' is installed and available in PATH`); } } catch { this.logger.error("fileWatcher", " Make sure the runtime is installed and available in PATH"); } } reject(error); } }); this.childProcess.on("exit", (code, signal) => { this.logger.debug("fileWatcher", `Child process exited (code: ${code}, signal: ${signal})`); this.emit("process:exit", { code, signal }); // If process exits immediately, it might be an error if (!resolved && code !== 0) { resolved = true; clearTimeout(timeout); reject(new Error(`Child process exited with code ${code}`)); return; } // Auto-restart on unexpected exit if (this.isRunning && code !== 0 && !signal) { this.logger.info("fileWatcher", "Unexpected exit, restarting..."); setTimeout(() => this.restart(), this.config.restartDelay); } }); // Also resolve if the process is spawned successfully (alternative check) setTimeout(() => { if (!resolved && this.childProcess && this.childProcess.pid) { resolved = true; clearTimeout(timeout); this.logger.debug("fileWatcher", `Child process confirmed running (PID: ${this.childProcess.pid})`); resolve(); } }, 1000); // Check after 1 second }); } catch (error) { throw error; } } /** * Stop child process gracefully */ async stopChildProcess() { if (!this.childProcess) return; return new Promise((resolve) => { const timeout = setTimeout(() => { if (this.childProcess) { // console.log('Force killing child process...'); this.childProcess.kill("SIGKILL"); } resolve(); }, this.config.gracefulShutdownTimeout); this.childProcess.on("exit", () => { clearTimeout(timeout); resolve(); }); // Try graceful shutdown first this.logger.debug("fileWatcher", "Sending SIGTERM to child process..."); this.childProcess.kill("SIGTERM"); }); } /** * Get hot reloader status */ getStatus() { let service = "none"; if (this.isRunning) { if (this.quickdevProcess) { service = "quickdev"; } else if (this.childProcess) { service = "local"; } } return { isRunning: this.isRunning, restartCount: this.restartCount, lastRestart: this.lastRestart, childPid: this.childProcess?.pid, quickdevPid: this.quickdevProcess?.pid, service, isQuickDevAvailable: this.isQuickDevAvailable, config: this.config, }; } /** * Reset restart counter */ resetRestartCount() { this.restartCount = 0; this.lastRestart = 0; // console.log('Restart counter reset'); } /** * Update configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.emit("config:updated", this.config); } /** * Check if process is healthy */ isHealthy() { if (!this.isRunning) return false; if (this.quickdevProcess) { return (!this.quickdevProcess.killed && this.restartCount < this.config.maxRestarts); } if (this.childProcess) { return (!this.childProcess.killed && this.restartCount < this.config.maxRestarts); } return false; } /** * Get QuickDev availability status */ getQuickDevStatus() { return { available: this.isQuickDevAvailable, inUse: this.isRunning && !!this.quickdevProcess, pid: this.quickdevProcess?.pid, }; } /** * Force use of local reloader (disable QuickDev) */ async useLocalReloader() { if (this.isRunning && this.quickdevProcess) { await this.stop(); } this.useQuickDev = false; if (this.isRunning) { await this.start(); } this.logger.info("fileWatcher", "Forced to use local reloader"); } /** * Enable QuickDev if available */ async enableQuickDev() { this.useQuickDev = true; if (this.isRunning && !this.quickdevProcess && this.isQuickDevAvailable) { await this.restart(); } this.logger.info("fileWatcher", "QuickDev enabled"); } } exports.HotReloader = HotReloader; exports.default = HotReloader; //# sourceMappingURL=HotReloader.js.map