@ordojs/cli
Version:
Command-line interface for OrdoJS framework
430 lines • 15.2 kB
JavaScript
/**
* @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