@ordojs/cli
Version:
Command-line interface for OrdoJS framework
498 lines (426 loc) • 12.7 kB
JavaScript
/**
* @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