UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

458 lines 15.7 kB
/** * @fileoverview OrdoJS CLI - Development Server * * Main development server implementation with lifecycle management. */ import { readdir } from 'fs/promises'; import http from 'http'; import path from 'path'; import { fileExists, readFile } from '../utils/fs.js'; import { logger } from '../utils/index.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; status; actualPort; serverState; /** * Create a new OrdoJSDevServer instance * * @param options - Server options */ constructor(options) { this.options = { ...options, dir: options.dir || '.', host: options.host || 'localhost', port: options.port || 3000, hmr: options.hmr !== false }; this.server = null; this.portManager = new PortManager(Number(this.options.port)); this.processManager = new ProcessManager(); this.status = ServerStatus.STOPPED; this.actualPort = 0; 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 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)); // Create HTTP server this.server = http.createServer(this.requestHandler.bind(this)); // Start the server await this.startServer(); // Set up HMR if enabled if (this.options.hmr) { // HMR setup will be implemented in task 9.2 logger.info('HMR enabled (implementation pending)'); } 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 the HTTP server if (this.server) { await this.stopServer(); } // Clean up resources if (this.actualPort) { this.portManager.releasePort(this.actualPort); } // 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; } } /** * 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 */ async requestHandler(req, res) { const url = req.url || '/'; logger.debug(`${req.method} ${url}`); try { // Handle different file types if (url.endsWith('.ordo')) { await this.handleOrdoFile(req, res, url); } else if (url === '/' || url === '/index.html') { await this.handleIndex(req, res); } else { await this.handleStaticFile(req, res, url); } } catch (error) { logger.error(`Request handler error: ${error instanceof Error ? error.message : String(error)}`); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); } } /** * Handle OrdoJS file compilation */ async handleOrdoFile(req, res, url) { const filePath = path.join(this.options.dir, url); try { // Read the OrdoJS file const source = await readFile(filePath); // Compile the OrdoJS file to HTML/JS const html = this.compileOrdoToHTML(source, path.basename(filePath, '.ordo')); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); } catch (error) { logger.error(`Failed to compile OrdoJS file: ${error instanceof Error ? error.message : String(error)}`); res.writeHead(500, { 'Content-Type': 'text/html' }); res.end(` <!DOCTYPE html> <html> <head><title>Compilation Error</title></head> <body> <h1>OrdoJS Compilation Error</h1> <p>Failed to compile ${url}: ${error instanceof Error ? error.message : String(error)}</p> </body> </html> `); } } /** * Handle index page */ async handleIndex(req, res) { // First try to serve public/index.html const indexHtmlPath = path.join(this.options.dir, 'public/index.html'); try { const exists = await fileExists(indexHtmlPath); if (exists) { // Serve the index.html file const content = await readFile(indexHtmlPath); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(content); return; } } catch (error) { // Continue to fallback } // Fallback: Look for app.ordo in the current directory const appOrdoPath = path.join(this.options.dir, 'src/app.ordo'); try { const exists = await fileExists(appOrdoPath); if (exists) { // Redirect to the app.ordo file res.writeHead(302, { 'Location': '/src/app.ordo' }); res.end(); } else { // Show directory listing or default page await this.showDirectoryListing(req, res); } } catch (error) { await this.showDirectoryListing(req, res); } } /** * Handle static files */ async handleStaticFile(req, res, url) { const filePath = path.join(this.options.dir, url); try { const exists = await fileExists(filePath); if (!exists) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('File not found'); return; } const content = await readFile(filePath); const ext = path.extname(filePath); let contentType = 'text/plain'; switch (ext) { case '.html': contentType = 'text/html'; break; case '.css': contentType = 'text/css'; break; case '.js': contentType = 'application/javascript'; break; case '.json': contentType = 'application/json'; break; case '.png': contentType = 'image/png'; break; case '.jpg': case '.jpeg': contentType = 'image/jpeg'; break; case '.gif': contentType = 'image/gif'; break; case '.svg': contentType = 'image/svg+xml'; break; } res.writeHead(200, { 'Content-Type': contentType }); res.end(content); } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); } } /** * Show directory listing */ async showDirectoryListing(req, res) { try { const files = await readdir(this.options.dir); const ordoFiles = files.filter(file => file.endsWith('.ordo')); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <!DOCTYPE html> <html> <head> <title>OrdoJS Development Server</title> <style> body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; } h1 { color: #0066cc; } .file-list { list-style: none; padding: 0; } .file-list li { margin: 0.5rem 0; } .file-list a { color: #0066cc; text-decoration: none; } .file-list a:hover { text-decoration: underline; } </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> ${ordoFiles.length > 0 ? ` <h2>Available OrdoJS Files:</h2> <ul class="file-list"> ${ordoFiles.map(file => `<li><a href="/${file}">${file}</a></li>`).join('')} </ul> ` : '<p>No .ordo files found in the current directory.</p>'} </body> </html> `); } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Failed to read directory'); } } /** * Compile OrdoJS to HTML */ compileOrdoToHTML(source, componentName) { try { // Simple compilation for now - just wrap the content in HTML // This is a basic implementation that will be enhanced return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${componentName} - OrdoJS</title> <style> body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; } .component { border: 1px solid #ddd; padding: 20px; margin: 20px 0; border-radius: 8px; } .source { background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0; } pre { margin: 0; white-space: pre-wrap; } </style> </head> <body> <h1>${componentName}</h1> <div class="component"> <h2>Compiled Component</h2> <p>This is a placeholder for the compiled OrdoJS component.</p> <p>The full compilation pipeline will be implemented in upcoming tasks.</p> </div> <div class="source"> <h3>Source Code:</h3> <pre><code>${source.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre> </div> <script> console.log('OrdoJS component: ${componentName}'); console.log('Source:', \`${source.replace(/`/g, '\\`')}\`); </script> </body> </html> `; } catch (error) { return ` <!DOCTYPE html> <html> <head><title>Compilation Error</title></head> <body> <h1>OrdoJS Compilation Error</h1> <p>Failed to compile component: ${error instanceof Error ? error.message : String(error)}</p> </body> </html> `; } } } //# sourceMappingURL=server.js.map