- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
291 lines
8.1 KiB
JavaScript
291 lines
8.1 KiB
JavaScript
/**
|
|
* Hot Reload Client for Development
|
|
* Connects to SSE stream and handles reload events
|
|
*/
|
|
export class HotReload {
|
|
constructor(options = {}) {
|
|
this.url = options.url || '/dev/hot-reload';
|
|
this.reconnectDelay = options.reconnectDelay || 5000;
|
|
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
|
|
this.debug = options.debug || false;
|
|
|
|
this.eventSource = null;
|
|
this.reconnectAttempts = 0;
|
|
this.reconnectTimer = null;
|
|
|
|
// Only enable in development
|
|
if (this.isDevelopment()) {
|
|
this.connect();
|
|
this.setupVisibilityHandlers();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if we're in development mode
|
|
*/
|
|
isDevelopment() {
|
|
return window.location.hostname === 'localhost'
|
|
|| window.location.hostname === '127.0.0.1'
|
|
|| window.location.hostname.includes('.local');
|
|
}
|
|
|
|
/**
|
|
* Connect to SSE stream
|
|
*/
|
|
connect() {
|
|
if (this.eventSource) {
|
|
return;
|
|
}
|
|
|
|
this.log('Connecting to Hot Reload server...');
|
|
|
|
this.eventSource = new EventSource(this.url);
|
|
|
|
// Connection opened
|
|
this.eventSource.addEventListener('open', () => {
|
|
this.log('Hot Reload connected');
|
|
this.reconnectAttempts = 0;
|
|
this.showNotification('Hot Reload Connected', 'success');
|
|
});
|
|
|
|
// Handle reload events
|
|
this.eventSource.addEventListener('reload', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
this.handleReload(data);
|
|
});
|
|
|
|
// Handle heartbeat
|
|
this.eventSource.addEventListener('heartbeat', () => {
|
|
this.log('Heartbeat received');
|
|
});
|
|
|
|
// Handle close
|
|
this.eventSource.addEventListener('close', () => {
|
|
this.log('Server requested close');
|
|
this.disconnect();
|
|
});
|
|
|
|
// Handle errors
|
|
this.eventSource.addEventListener('error', (error) => {
|
|
this.log('Connection error', error);
|
|
this.disconnect();
|
|
this.scheduleReconnect();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Disconnect from SSE stream
|
|
*/
|
|
disconnect() {
|
|
if (this.eventSource) {
|
|
this.eventSource.close();
|
|
this.eventSource = null;
|
|
}
|
|
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer);
|
|
this.reconnectTimer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule reconnection attempt
|
|
*/
|
|
scheduleReconnect() {
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
this.log('Max reconnection attempts reached');
|
|
this.showNotification('Hot Reload disconnected', 'error');
|
|
return;
|
|
}
|
|
|
|
this.reconnectAttempts++;
|
|
const delay = Math.min(this.reconnectDelay * this.reconnectAttempts, 30000);
|
|
|
|
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
|
|
this.reconnectTimer = setTimeout(() => {
|
|
this.connect();
|
|
}, delay);
|
|
}
|
|
|
|
/**
|
|
* Handle reload event from server
|
|
*/
|
|
handleReload(data) {
|
|
this.log('Reload event received', data);
|
|
|
|
switch (data.type) {
|
|
case 'css':
|
|
this.reloadCSS(data.file);
|
|
break;
|
|
|
|
case 'hmr':
|
|
this.handleHMR(data);
|
|
break;
|
|
|
|
case 'partial':
|
|
this.reloadPartial(data);
|
|
break;
|
|
|
|
case 'full':
|
|
default:
|
|
this.reloadPage();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reload CSS without page refresh
|
|
*/
|
|
reloadCSS(file) {
|
|
this.log('Reloading CSS:', file);
|
|
|
|
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
const timestamp = new Date().getTime();
|
|
|
|
links.forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
if (href) {
|
|
// Remove existing timestamp
|
|
const cleanHref = href.split('?')[0];
|
|
|
|
// Add new timestamp to force reload
|
|
link.setAttribute('href', `${cleanHref}?t=${timestamp}`);
|
|
}
|
|
});
|
|
|
|
this.showNotification('CSS updated', 'info', 2000);
|
|
}
|
|
|
|
/**
|
|
* Handle Hot Module Replacement
|
|
*/
|
|
handleHMR(data) {
|
|
this.log('HMR not yet implemented, falling back to full reload');
|
|
this.reloadPage();
|
|
}
|
|
|
|
/**
|
|
* Reload partial content
|
|
*/
|
|
reloadPartial(data) {
|
|
this.log('Partial reload not yet implemented, falling back to full reload');
|
|
this.reloadPage();
|
|
}
|
|
|
|
/**
|
|
* Reload the entire page
|
|
*/
|
|
reloadPage() {
|
|
this.log('Reloading page...');
|
|
this.showNotification('Reloading...', 'info', 500);
|
|
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 500);
|
|
}
|
|
|
|
/**
|
|
* Show notification to user
|
|
*/
|
|
showNotification(message, type = 'info', duration = 3000) {
|
|
// Remove existing notification
|
|
const existing = document.getElementById('hot-reload-notification');
|
|
if (existing) {
|
|
existing.remove();
|
|
}
|
|
|
|
// Create notification element
|
|
const notification = document.createElement('div');
|
|
notification.id = 'hot-reload-notification';
|
|
notification.textContent = message;
|
|
notification.className = `hot-reload-notification hot-reload-notification--${type}`;
|
|
|
|
// Style the notification
|
|
notification.style.cssText = `
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
padding: 12px 20px;
|
|
border-radius: 6px;
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
z-index: 999999;
|
|
transition: opacity 0.3s ease;
|
|
pointer-events: none;
|
|
max-width: 300px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
`;
|
|
|
|
// Type-specific styles
|
|
switch(type) {
|
|
case 'success':
|
|
notification.style.backgroundColor = '#10b981';
|
|
notification.style.color = '#ffffff';
|
|
break;
|
|
case 'error':
|
|
notification.style.backgroundColor = '#ef4444';
|
|
notification.style.color = '#ffffff';
|
|
break;
|
|
case 'warning':
|
|
notification.style.backgroundColor = '#f59e0b';
|
|
notification.style.color = '#ffffff';
|
|
break;
|
|
default:
|
|
notification.style.backgroundColor = '#3b82f6';
|
|
notification.style.color = '#ffffff';
|
|
}
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// Auto-remove after duration
|
|
if (duration > 0) {
|
|
setTimeout(() => {
|
|
notification.style.opacity = '0';
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 300);
|
|
}, duration);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup visibility handlers for reconnection
|
|
*/
|
|
setupVisibilityHandlers() {
|
|
// Reconnect when tab becomes visible
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden && !this.eventSource) {
|
|
this.log('Tab became visible, reconnecting...');
|
|
this.connect();
|
|
}
|
|
});
|
|
|
|
// Disconnect when window is closed
|
|
window.addEventListener('beforeunload', () => {
|
|
this.disconnect();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Debug logging
|
|
*/
|
|
log(...args) {
|
|
if (this.debug) {
|
|
console.log('[HotReload]', ...args);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto-initialize if in development
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
// Only initialize if not already initialized
|
|
if (!window.__hotReload) {
|
|
window.__hotReload = new HotReload({
|
|
debug: true // Enable debug in development
|
|
});
|
|
}
|
|
});
|
|
} |