UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

371 lines 12.9 kB
/** * @fileoverview OrdoJS CLI - Development Server with File Watching * * Main development server implementation with lifecycle management and file watching. */ import http from 'http'; import { AddressInfo } from 'net'; import path from 'path'; import { fileExists } from '../utils/fs.js'; import { logger } from '../utils/index.js'; import { FileChangeEvent, FileWatcher } from './file-watcher.js'; import { generateHMRClientCode } from './hmr-client.js'; import { OrdoJSHMR } from './hmr.js'; import { PortManager } from './port-manager.js'; import { ProcessManager } from './process-manager.js'; /** * Server status enum */ export var ServerStatus; (function (ServerStatus) { ServerStatus["STOPPED"] = "stopped"; ServerStatus["STARTING"] = "starting"; ServerStatus["RUNNING"] = "running"; ServerStatus["STOPPING"] = "stopping"; ServerStatus["ERROR"] = "error"; ServerStatus["RESTARTING"] = "restarting"; })(ServerStatus || (ServerStatus = {})); /** * OrdoJSDevServer class for managing the development server lifecycle */ export class OrdoJSDevServer { options; server; portManager; processManager; fileWatcher; hmr; status; actualPort; hmrPort; serverState; /** * Create a new OrdoJSDevServer instance * * @param options - Server options */ constructor(options) { this.options = { dir: '.', host: 'localhost', port: 3000, hmr: true, ...options }; this.server = null; this.portManager = new PortManager(Number(this.options.port)); this.processManager = new ProcessManager(); this.fileWatcher = null; this.hmr = null; this.status = ServerStatus.STOPPED; this.actualPort = 0; this.hmrPort = this.options.hmrPort || (Number(this.options.port) + 1); this.serverState = {}; } /** * Get the current server status */ getStatus() { return this.status; } /** * Get the actual port the server is running on */ getPort() { return this.actualPort; } /** * Get the HMR port */ getHMRPort() { return this.hmrPort; } /** * Get the current server state * * @returns The current server state */ getServerState() { return { ...this.serverState }; } /** * Start the development server */ async start() { if (this.status === ServerStatus.RUNNING) { logger.info('Server is already running'); return; } if (this.status === ServerStatus.STARTING) { logger.info('Server is already starting'); return; } this.status = ServerStatus.STARTING; logger.info('Starting development server...'); try { // Validate the directory exists const dirExists = await fileExists(this.options.dir); if (!dirExists) { throw new Error(`Directory not found: ${this.options.dir}`); } // Allocate a port this.actualPort = await this.portManager.allocatePort('dev-server', Number(this.options.port)); // Allocate HMR port this.hmrPort = await this.portManager.allocatePort('hmr-server', this.hmrPort); // Create HTTP server this.server = http.createServer(this.requestHandler.bind(this)); // Start the server await this.startServer(); // Set up file watching and HMR if enabled if (this.options.hmr) { await this.setupHMR(); logger.info(`HMR enabled on port ${this.hmrPort}`); } this.status = ServerStatus.RUNNING; logger.success(`Development server running at http://${this.options.host}:${this.actualPort}`); } catch (error) { this.status = ServerStatus.ERROR; logger.error(`Failed to start server: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Stop the development server */ async stop() { if (this.status === ServerStatus.STOPPED) { logger.info('Server is already stopped'); return; } if (this.status === ServerStatus.STOPPING) { logger.info('Server is already stopping'); return; } this.status = ServerStatus.STOPPING; logger.info('Stopping development server...'); try { // Stop HMR system if (this.hmr) { await this.hmr.stop(); this.hmr = null; } // Stop file watching if (this.fileWatcher) { await this.fileWatcher.stop(); this.fileWatcher = null; } // Stop the HTTP server if (this.server) { await this.stopServer(); } // Clean up resources if (this.actualPort) { this.portManager.releasePort(this.actualPort); } if (this.hmrPort) { this.portManager.releasePort(this.hmrPort); } // Clean up child processes await this.processManager.cleanup(); this.status = ServerStatus.STOPPED; logger.success('Development server stopped'); } catch (error) { this.status = ServerStatus.ERROR; logger.error(`Failed to stop server: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Restart the development server */ async restart() { if (this.status === ServerStatus.RESTARTING) { logger.info('Server is already restarting'); return; } this.status = ServerStatus.RESTARTING; logger.info('Restarting development server...'); try { // Preserve the server state before stopping const preservedState = { ...this.serverState }; // Stop the server but keep the state await this.stop(); // Restore the preserved state this.serverState = preservedState; // Start the server with the preserved state await this.start(); logger.success('Development server restarted successfully'); } catch (error) { this.status = ServerStatus.ERROR; logger.error(`Failed to restart server: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Set up HMR system and file watching */ async setupHMR() { // Set up file watcher this.fileWatcher = new FileWatcher({ watchDir: this.options.dir, patterns: ['**/*.ordo', '**/*.ts', '**/*.js', '**/*.css', '**/*.html'], ignorePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**'], debounceMs: 100 }); // Set up HMR system this.hmr = new OrdoJSHMR({ port: this.hmrPort, preserveState: true, verbose: false }); // Connect file watcher to HMR system this.fileWatcher.on('change', async (event) => { // Forward file changes to HMR system await this.hmr?.handleFileChange(event); // Also handle locally for logging this.handleFileChange(event); }); this.fileWatcher.on('error', this.handleFileWatcherError.bind(this)); // Start HMR system await this.hmr.start(); // Start file watcher await this.fileWatcher.start(); logger.success('HMR system and file watching started successfully'); } /** * Handle file change events */ async handleFileChange(event) { logger.info(`File ${event.type}: ${event.relativePath}`); try { // Get affected files const affectedFiles = this.fileWatcher?.getAffectedFiles(event.filePath) || [event.filePath]; logger.debug(`Affected files: ${affectedFiles.map(f => path.relative(this.options.dir, f)).join(', ')}`); } catch (error) { logger.error(`Error handling file change: ${error instanceof Error ? error.message : String(error)}`); } } /** * Handle file watcher errors */ handleFileWatcherError(error) { logger.error(`File watcher error: ${error.message}`); // Attempt to restart file watching if (this.options.hmr && this.status === ServerStatus.RUNNING) { logger.info('Attempting to restart file watcher and HMR...'); this.setupHMR().catch(restartError => { logger.error(`Failed to restart HMR: ${restartError instanceof Error ? restartError.message : String(restartError)}`); }); } } /** * Start the HTTP server */ startServer() { return new Promise((resolve, reject) => { if (!this.server) { reject(new Error('Server not initialized')); return; } // Handle server errors this.server.once('error', (error) => { const e = error; if (e.code === 'EADDRINUSE') { reject(new Error(`Port ${this.actualPort} is already in use`)); } else { reject(error); } }); // Start listening this.server.listen(this.actualPort, this.options.host, () => { const address = this.server?.address(); this.actualPort = address.port; resolve(); }); }); } /** * Stop the HTTP server */ stopServer() { return new Promise((resolve, reject) => { if (!this.server) { resolve(); return; } this.server.close((error) => { if (error) { reject(error); } else { this.server = null; resolve(); } }); }); } /** * HTTP request handler */ requestHandler(req, res) { const url = req.url || '/'; logger.debug(`${req.method} ${url}`); // Inject HMR client script for HTML requests if (this.options.hmr && (url === '/' || url.endsWith('.html'))) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <!DOCTYPE html> <html> <head> <title>OrdoJS Dev Server</title> <style> body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; } h1 { color: #0066cc; } code { background: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 3px; } </style> <script> ${generateHMRClientCode(this.hmrPort)} </script> </head> <body> <h1>OrdoJS Development Server</h1> <p>Server is running at <code>http://${this.options.host}:${this.actualPort}</code></p> <p>Serving directory: <code>${path.resolve(this.options.dir)}</code></p> <p>HMR: <code>${this.options.hmr ? 'enabled' : 'disabled'}</code></p> <p>File Watching: <code>${this.fileWatcher ? 'active' : 'inactive'}</code></p> <p>HMR WebSocket: <code>ws://${this.options.host}:${this.hmrPort}</code></p> </body> </html> `); return; } // Handle other requests res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <!DOCTYPE html> <html> <head> <title>OrdoJS Dev Server</title> <style> body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; } h1 { color: #0066cc; } code { background: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 3px; } </style> </head> <body> <h1>OrdoJS Development Server</h1> <p>Server is running at <code>http://${this.options.host}:${this.actualPort}</code></p> <p>Serving directory: <code>${path.resolve(this.options.dir)}</code></p> <p>HMR: <code>${this.options.hmr ? 'enabled' : 'disabled'}</code></p> <p>File Watching: <code>${this.fileWatcher ? 'active' : 'inactive'}</code></p> </body> </html> `); } } //# sourceMappingURL=server-with-watcher.js.map