UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

430 lines 15.2 kB
/** * @fileoverview OrdoJS CLI - Hot Module Replacement System * * Manages hot updates with WebSocket communication and state preservation. */ import { OrdoJSCompiler } from '@ordojs/core'; import { EventEmitter } from 'events'; import path from 'path'; import { WebSocket, WebSocketServer } from 'ws'; import { logger } from '../utils/index.js'; /** * HMR update types */ export var HMRUpdateType; (function (HMRUpdateType) { HMRUpdateType["COMPONENT_UPDATE"] = "component-update"; HMRUpdateType["STYLE_UPDATE"] = "style-update"; HMRUpdateType["ASSET_UPDATE"] = "asset-update"; HMRUpdateType["FULL_RELOAD"] = "full-reload"; HMRUpdateType["ERROR"] = "error"; })(HMRUpdateType || (HMRUpdateType = {})); /** * OrdoJSHMR class for managing hot module replacement */ export class OrdoJSHMR extends EventEmitter { options; wsServer; clients; compiler; updateQueue; debounceTimers; componentStates; isRunning; /** * Create a new OrdoJSHMR instance */ constructor(options = {}) { super(); this.options = { port: 24678, // Default HMR port preserveState: true, debounceMs: 100, maxClients: 50, verbose: false, ...options }; this.wsServer = null; this.clients = new Map(); this.updateQueue = new Map(); this.debounceTimers = new Map(); this.componentStates = new Map(); this.isRunning = false; // Initialize compiler with development settings this.compiler = new OrdoJSCompiler({ target: 'es2022', optimize: false, sourceMaps: true, minify: false }); } /** * Start the HMR system */ async start() { if (this.isRunning) { logger.warn('HMR system is already running'); return; } logger.info(`Starting HMR system on port ${this.options.port}...`); try { // Create WebSocket server this.wsServer = new WebSocketServer({ port: this.options.port, perMessageDeflate: false }); // Set up WebSocket event handlers this.setupWebSocketHandlers(); this.isRunning = true; logger.success(`HMR system started on ws://localhost:${this.options.port}`); // Emit ready event this.emit('ready'); } catch (error) { logger.error(`Failed to start HMR system: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Stop the HMR system */ async stop() { if (!this.isRunning) { logger.info('HMR system is not running'); return; } logger.info('Stopping HMR system...'); try { // Clear all debounce timers for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear(); // Close all client connections for (const client of this.clients.values()) { client.socket.close(1000, 'Server shutting down'); } this.clients.clear(); // Close WebSocket server if (this.wsServer) { await new Promise((resolve, reject) => { this.wsServer.close((error) => { if (error) { reject(error); } else { resolve(); } }); }); this.wsServer = null; } this.isRunning = false; logger.success('HMR system stopped'); // Emit stopped event this.emit('stopped'); } catch (error) { logger.error(`Error stopping HMR system: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Handle file change events from file watcher */ async handleFileChange(event) { if (!this.isRunning) { return; } const { filePath, type: changeType } = event; const fileExtension = path.extname(filePath); if (this.options.verbose) { logger.debug(`HMR: Processing file change - ${changeType}: ${path.relative(process.cwd(), filePath)}`); } // Clear existing debounce timer for this file const existingTimer = this.debounceTimers.get(filePath); if (existingTimer) { clearTimeout(existingTimer); } // Set new debounce timer const timer = setTimeout(async () => { this.debounceTimers.delete(filePath); await this.processFileUpdate(filePath, changeType, fileExtension); }, this.options.debounceMs); this.debounceTimers.set(filePath, timer); } /** * Get the number of connected clients */ getClientCount() { return this.clients.size; } /** * Get HMR statistics */ getStats() { return { isRunning: this.isRunning, clientCount: this.clients.size, port: this.options.port, updatesSent: this.updateQueue.size, componentsTracked: this.componentStates.size }; } /** * Process file update and generate HMR patch */ async processFileUpdate(filePath, changeType, fileExtension) { try { const relativePath = path.relative(process.cwd(), filePath); // Determine update type based on file extension let updateType; if (fileExtension === '.ordo') { updateType = HMRUpdateType.COMPONENT_UPDATE; } else if (['.css', '.scss', '.sass', '.less'].includes(fileExtension)) { updateType = HMRUpdateType.STYLE_UPDATE; } else if (['.js', '.ts', '.json'].includes(fileExtension)) { updateType = HMRUpdateType.ASSET_UPDATE; } else { updateType = HMRUpdateType.FULL_RELOAD; } // Generate update patch const update = await this.generateUpdatePatch(filePath, updateType, changeType); if (update) { // Store update in queue this.updateQueue.set(filePath, update); // Send update to all connected clients await this.broadcastUpdate(update); logger.info(`HMR: Sent ${update.type} for ${relativePath}`); } } catch (error) { logger.error(`HMR: Error processing file update for ${filePath}: ${error instanceof Error ? error.message : String(error)}`); // Send error update to clients const errorUpdate = { type: HMRUpdateType.ERROR, timestamp: Date.now(), file: filePath, error: error instanceof Error ? error.message : String(error) }; await this.broadcastUpdate(errorUpdate); } } /** * Generate HMR update patch for a file */ async generateUpdatePatch(filePath, updateType, changeType) { const timestamp = Date.now(); switch (updateType) { case HMRUpdateType.COMPONENT_UPDATE: return await this.generateComponentUpdate(filePath, timestamp, changeType); case HMRUpdateType.STYLE_UPDATE: return await this.generateStyleUpdate(filePath, timestamp); case HMRUpdateType.ASSET_UPDATE: return { type: HMRUpdateType.ASSET_UPDATE, timestamp, file: filePath, preserveState: false }; case HMRUpdateType.FULL_RELOAD: return { type: HMRUpdateType.FULL_RELOAD, timestamp, file: filePath, preserveState: false }; default: return null; } } /** * Generate component update patch */ async generateComponentUpdate(filePath, timestamp, changeType) { try { // Read and compile the component file const fs = await import('fs/promises'); const source = await fs.readFile(filePath, 'utf-8'); // Compile the component const result = this.compiler.compile(source); if (!result.success) { throw new Error(`Compilation failed: ${result.errors.join(', ')}`); } // Extract component name from file path const componentName = path.basename(filePath, '.ordo'); return { type: HMRUpdateType.COMPONENT_UPDATE, timestamp, file: filePath, componentName, code: result.output, preserveState: this.options.preserveState, affectedComponents: [componentName] }; } catch (error) { logger.error(`Failed to generate component update for ${filePath}: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * Generate style update patch */ async generateStyleUpdate(filePath, timestamp) { try { const fs = await import('fs/promises'); const css = await fs.readFile(filePath, 'utf-8'); return { type: HMRUpdateType.STYLE_UPDATE, timestamp, file: filePath, css, preserveState: true }; } catch (error) { logger.error(`Failed to generate style update for ${filePath}: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * Broadcast update to all connected clients */ async broadcastUpdate(update) { const message = JSON.stringify(update); const deadClients = []; for (const [clientId, client] of this.clients.entries()) { try { if (client.socket.readyState === WebSocket.OPEN) { client.socket.send(message); } else { deadClients.push(clientId); } } catch (error) { logger.debug(`Failed to send update to client ${clientId}: ${error instanceof Error ? error.message : String(error)}`); deadClients.push(clientId); } } // Clean up dead clients for (const clientId of deadClients) { this.clients.delete(clientId); } if (this.options.verbose && deadClients.length > 0) { logger.debug(`Cleaned up ${deadClients.length} dead client connections`); } } /** * Set up WebSocket server event handlers */ setupWebSocketHandlers() { if (!this.wsServer) return; this.wsServer.on('connection', (socket, request) => { const clientId = this.generateClientId(); const userAgent = request.headers['user-agent']; // Check client limit if (this.clients.size >= this.options.maxClients) { socket.close(1013, 'Server at capacity'); return; } // Create client record const client = { id: clientId, socket, connectedAt: Date.now(), lastPing: Date.now(), userAgent }; this.clients.set(clientId, client); if (this.options.verbose) { logger.debug(`HMR: Client connected - ${clientId} (${this.clients.size} total)`); } // Set up client event handlers socket.on('message', (data) => { this.handleClientMessage(clientId, data); }); socket.on('close', (code, reason) => { this.clients.delete(clientId); if (this.options.verbose) { logger.debug(`HMR: Client disconnected - ${clientId} (${code}: ${reason})`); } }); socket.on('error', (error) => { logger.debug(`HMR: Client error - ${clientId}: ${error.message}`); this.clients.delete(clientId); }); // Send welcome message const welcomeMessage = { type: 'welcome', timestamp: Date.now(), clientId, options: { preserveState: this.options.preserveState } }; socket.send(JSON.stringify(welcomeMessage)); }); this.wsServer.on('error', (error) => { logger.error(`HMR WebSocket server error: ${error.message}`); this.emit('error', error); }); } /** * Handle messages from HMR clients */ handleClientMessage(clientId, data) { try { const message = JSON.parse(data.toString()); const client = this.clients.get(clientId); if (!client) return; switch (message.type) { case 'ping': client.lastPing = Date.now(); client.socket.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); break; case 'state-snapshot': if (this.options.preserveState && message.snapshot) { this.storeComponentState(message.snapshot); } break; case 'error': logger.error(`HMR Client error from ${clientId}: ${message.error}`); break; default: if (this.options.verbose) { logger.debug(`HMR: Unknown message type from ${clientId}: ${message.type}`); } } } catch (error) { logger.debug(`HMR: Invalid message from client ${clientId}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Store component state snapshot for preservation */ storeComponentState(snapshot) { const key = `${snapshot.componentName}_${snapshot.componentId}`; this.componentStates.set(key, { ...snapshot, timestamp: Date.now() }); if (this.options.verbose) { logger.debug(`HMR: Stored state for ${snapshot.componentName}`); } } /** * Generate unique client ID */ generateClientId() { return `hmr_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`; } } //# sourceMappingURL=hmr.js.map