UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

498 lines (426 loc) 12.7 kB
/** * @fileoverview OrdoJS HMR Client Runtime * * Client-side hot module replacement runtime that communicates with the dev server. * This code gets injected into the browser during development. */ /** * Generate HMR client runtime code */ export function generateHMRClientCode(port) { return ` (function() { 'use strict'; // HMR Client Configuration const HMR_PORT = ${port}; const RECONNECT_DELAY = 1000; const MAX_RECONNECT_ATTEMPTS = 10; const PING_INTERVAL = 30000; // HMR Client State let socket = null; let reconnectAttempts = 0; let pingTimer = null; let isConnected = false; let componentRegistry = new Map(); let stateSnapshots = new Map(); // HMR Update Types const HMRUpdateType = { COMPONENT_UPDATE: 'component-update', STYLE_UPDATE: 'style-update', ASSET_UPDATE: 'asset-update', FULL_RELOAD: 'full-reload', ERROR: 'error' }; /** * Initialize HMR client */ function initHMR() { console.log('[HMR] Initializing hot module replacement...'); // Connect to HMR server connect(); // Set up global HMR interface window.__ORDOJS_HMR__ = { registerComponent, updateComponent, preserveState, restoreState, isConnected: () => isConnected }; // Override console.error to capture runtime errors const originalError = console.error; console.error = function(...args) { originalError.apply(console, args); // Send error to HMR server for better debugging if (isConnected && socket) { try { socket.send(JSON.stringify({ type: 'error', timestamp: Date.now(), error: args.join(' ') })); } catch (e) { // Ignore send errors } } }; } /** * Connect to HMR WebSocket server */ function connect() { try { socket = new WebSocket(\`ws://localhost:\${HMR_PORT}\`); socket.onopen = function() { console.log('[HMR] Connected to dev server'); isConnected = true; reconnectAttempts = 0; // Start ping timer startPingTimer(); }; socket.onmessage = function(event) { try { const message = JSON.parse(event.data); handleMessage(message); } catch (error) { console.error('[HMR] Failed to parse message:', error); } }; socket.onclose = function(event) { console.log('[HMR] Disconnected from dev server'); isConnected = false; stopPingTimer(); // Attempt to reconnect if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { setTimeout(() => { reconnectAttempts++; console.log(\`[HMR] Reconnecting... (attempt \${reconnectAttempts})\`); connect(); }, RECONNECT_DELAY); } else { console.warn('[HMR] Max reconnection attempts reached. Please refresh the page.'); } }; socket.onerror = function(error) { console.error('[HMR] WebSocket error:', error); }; } catch (error) { console.error('[HMR] Failed to connect:', error); } } /** * Handle messages from HMR server */ function handleMessage(message) { switch (message.type) { case 'welcome': console.log('[HMR] Welcome message received'); break; case 'pong': // Pong response to keep connection alive break; case HMRUpdateType.COMPONENT_UPDATE: handleComponentUpdate(message); break; case HMRUpdateType.STYLE_UPDATE: handleStyleUpdate(message); break; case HMRUpdateType.ASSET_UPDATE: handleAssetUpdate(message); break; case HMRUpdateType.FULL_RELOAD: handleFullReload(message); break; case HMRUpdateType.ERROR: handleError(message); break; default: console.log('[HMR] Unknown message type:', message.type); } } /** * Handle component update */ function handleComponentUpdate(message) { console.log(\`[HMR] Updating component: \${message.componentName}\`); try { // Preserve component state if enabled if (message.preserveState) { preserveComponentStates(message.componentName); } // Execute the new component code const newComponentFactory = new Function('return ' + message.code)(); // Find and update all instances of this component const instances = findComponentInstances(message.componentName); for (const instance of instances) { updateComponentInstance(instance, newComponentFactory, message.preserveState); } console.log(\`[HMR] Successfully updated \${instances.length} instance(s) of \${message.componentName}\`); } catch (error) { console.error(\`[HMR] Failed to update component \${message.componentName}:\`, error); // Fall back to full reload on error window.location.reload(); } } /** * Handle style update */ function handleStyleUpdate(message) { console.log('[HMR] Updating styles...'); try { // Find existing style elements for this file const existingStyles = document.querySelectorAll(\`style[data-hmr-file="\${message.file}"]\`); // Remove existing styles existingStyles.forEach(style => style.remove()); // Create new style element const styleElement = document.createElement('style'); styleElement.setAttribute('data-hmr-file', message.file); styleElement.textContent = message.css; // Append to head document.head.appendChild(styleElement); console.log('[HMR] Styles updated successfully'); } catch (error) { console.error('[HMR] Failed to update styles:', error); } } /** * Handle asset update */ function handleAssetUpdate(message) { console.log(\`[HMR] Asset updated: \${message.file}\`); // For now, we'll do a full reload for asset updates // In the future, this could be more intelligent window.location.reload(); } /** * Handle full reload */ function handleFullReload(message) { console.log('[HMR] Full reload requested'); window.location.reload(); } /** * Handle error from server */ function handleError(message) { console.error('[HMR] Server error:', message.error); // Display error overlay showErrorOverlay(message.error, message.file); } /** * Register a component with HMR */ function registerComponent(name, factory, element) { if (!componentRegistry.has(name)) { componentRegistry.set(name, []); } const instances = componentRegistry.get(name); const instance = { name, factory, element, component: null, id: generateInstanceId() }; instances.push(instance); // Create and mount the component instance.component = factory(); if (instance.component && instance.component.mount) { instance.component.mount(element); } return instance; } /** * Update a component instance */ function updateComponent(name, newFactory) { const instances = componentRegistry.get(name); if (!instances) return; for (const instance of instances) { updateComponentInstance(instance, newFactory, true); } } /** * Update a single component instance */ function updateComponentInstance(instance, newFactory, preserveState) { try { // Preserve state if requested let savedState = null; if (preserveState && instance.component && instance.component.state) { savedState = { ...instance.component.state }; } // Unmount old component if (instance.component && instance.component.unmount) { instance.component.unmount(); } // Create new component instance instance.factory = newFactory; instance.component = newFactory(); // Restore state if preserved if (savedState && instance.component && instance.component.state) { Object.assign(instance.component.state, savedState); } // Mount new component if (instance.component && instance.component.mount) { instance.component.mount(instance.element); } } catch (error) { console.error('[HMR] Failed to update component instance:', error); throw error; } } /** * Find all instances of a component */ function findComponentInstances(componentName) { return componentRegistry.get(componentName) || []; } /** * Preserve component states */ function preserveComponentStates(componentName) { const instances = findComponentInstances(componentName); for (const instance of instances) { if (instance.component && instance.component.state) { const snapshot = { componentId: instance.id, componentName: componentName, state: { ...instance.component.state }, props: instance.component.props || {}, timestamp: Date.now() }; stateSnapshots.set(instance.id, snapshot); // Send snapshot to server if (isConnected && socket) { try { socket.send(JSON.stringify({ type: 'state-snapshot', timestamp: Date.now(), snapshot })); } catch (error) { console.debug('[HMR] Failed to send state snapshot:', error); } } } } } /** * Preserve state for a specific component */ function preserveState(componentId) { const snapshot = stateSnapshots.get(componentId); return snapshot ? snapshot.state : null; } /** * Restore state for a specific component */ function restoreState(componentId, state) { if (state) { stateSnapshots.set(componentId, { componentId, componentName: 'unknown', state, props: {}, timestamp: Date.now() }); } } /** * Show error overlay */ function showErrorOverlay(error, file) { // Remove existing overlay const existingOverlay = document.getElementById('ordojs-hmr-error-overlay'); if (existingOverlay) { existingOverlay.remove(); } // Create error overlay const overlay = document.createElement('div'); overlay.id = 'ordojs-hmr-error-overlay'; overlay.innerHTML = \` <div style=" position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); color: white; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 14px; z-index: 999999; padding: 20px; box-sizing: border-box; overflow: auto; "> <div style="max-width: 800px; margin: 0 auto;"> <h2 style="color: #ff6b6b; margin-top: 0;">OrdoJS HMR Error</h2> <p><strong>File:</strong> \${file || 'Unknown'}</p> <pre style=" background: #1a1a1a; padding: 15px; border-radius: 5px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; ">\${error}</pre> <button onclick="this.parentElement.parentElement.remove()" style=" background: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; margin-top: 15px; ">Dismiss</button> </div> </div> \`; document.body.appendChild(overlay); // Auto-dismiss after 10 seconds setTimeout(() => { if (overlay.parentElement) { overlay.remove(); } }, 10000); } /** * Start ping timer to keep connection alive */ function startPingTimer() { stopPingTimer(); pingTimer = setInterval(() => { if (isConnected && socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); } }, PING_INTERVAL); } /** * Stop ping timer */ function stopPingTimer() { if (pingTimer) { clearInterval(pingTimer); pingTimer = null; } } /** * Generate unique instance ID */ function generateInstanceId() { return 'hmr_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 8); } // Initialize HMR when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initHMR); } else { initHMR(); } })(); `; } //# sourceMappingURL=hmr-client.js.map