UNPKG

accs-cli

Version:

ACCS CLI — Full-featured developer tool for scaffolding, running, building, and managing multi-language projects

266 lines (236 loc) 7.93 kB
/** * Serve command - Local development server */ import express from 'express'; import path from 'path'; import chalk from 'chalk'; import { logger } from '../utils/logger.js'; import { FileUtils } from '../utils/file-utils.js'; import { configManager } from '../config/config-manager.js'; import { createServer } from 'http'; import os from 'os'; export function serveCommand(program) { program .command('serve') .option('-p, --port <port>', 'Port to serve on', configManager.get('defaultPort', 3000)) .option('-d, --dir <directory>', 'Directory to serve', 'dist') .option('-h, --host <host>', 'Host to serve on (e.g., 0.0.0.0 for all interfaces)', 'localhost') .option('-o, --open', 'Open in browser', true) .option('-v, --verbose', 'Verbose logging') .description('Serve the built project locally') .action(async (options) => { try { await serveProject(options); } catch (error) { logger.error('Failed to start server:', error.message); process.exit(1); } }); } async function serveProject(options) { const projectRoot = FileUtils.getProjectRoot(); const serveDir = path.join(projectRoot, options.dir); const port = options.port; const host = options.host; // Validate port if (isNaN(port) || port < 1 || port > 65535) { throw new Error('Invalid port number'); } // Check if directory exists if (!FileUtils.exists(serveDir)) { logger.warn(`Directory "${options.dir}" doesn't exist. Creating it...`); await FileUtils.createDir(serveDir); // Create a simple index.html if directory is empty const indexPath = path.join(serveDir, 'index.html'); const indexContent = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ACCS Server</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 800px; margin: 2rem auto; padding: 2rem; background: #f5f5f5; } .container { background: white; padding: 3rem; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); text-align: center; } h1 { color: #333; margin-bottom: 1rem; } p { color: #666; line-height: 1.6; } .code { background: #f8f9fa; padding: 0.5rem; border-radius: 4px; font-family: monospace; } </style> </head> <body> <div class="container"> <h1>🚀 ACCS Development Server</h1> <p>Your server is running successfully!</p> <p>To get started, add your files to the <span class="code">${options.dir}</span> directory.</p> <p>Run <span class="code">accs build</span> to build your project.</p> </div> </body> </html>`; await import('fs').then(fs => fs.promises.writeFile(indexPath, indexContent, 'utf8') ); } const app = express(); // Middleware for logging if (options.verbose) { app.use((req, res, next) => { const timestamp = new Date().toLocaleTimeString(); logger.info(`[${timestamp}] ${req.method} ${req.url}`); next(); }); } // Serve static files app.use(express.static(serveDir, { extensions: ['html', 'htm'], index: ['index.html', 'index.htm'] })); // API endpoint for server info app.get('/__server_info__', (req, res) => { res.json({ name: 'ACCS Development Server', version: '1.0.0', directory: serveDir, port: port, uptime: process.uptime() }); }); // 404 handler app.use((req, res) => { const html404 = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>404 - Not Found</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #f5f5f5; } .error-container { text-align: center; background: white; padding: 3rem; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } h1 { color: #e74c3c; margin-bottom: 1rem; } p { color: #666; } a { color: #3498db; text-decoration: none; } a:hover { text-decoration: underline; } </style> </head> <body> <div class="error-container"> <h1>404 - File Not Found</h1> <p>The requested file <code>${req.url}</code> could not be found.</p> <p><a href="/">← Back to Home</a></p> </div> </body> </html>`; res.status(404).send(html404); }); // Error handler app.use((err, req, res) => { logger.error('Server error:', err.message); res.status(500).json({ error: 'Internal Server Error' }); }); // Start server const server = app.listen(port, host, () => { let displayHost = host; if (host === '0.0.0.0') { const networkInterfaces = os.networkInterfaces(); const addresses = []; for (const interfaceName in networkInterfaces) { for (const iface of networkInterfaces[interfaceName]) { if (iface.family === 'IPv4' && !iface.internal) { addresses.push(iface.address); } } } if (addresses.length > 0) { logger.success(`Server running on all network interfaces. Access via:`); addresses.forEach(addr => { logger.success(` ${chalk.cyan(`http://${addr}:${port}`)}`); }); displayHost = addresses[0]; // Use the first address for opening in browser } else { logger.warn(`Server running on 0.0.0.0 but no network interfaces found. Access via: ${chalk.cyan(`http://localhost:${port}`)}`); displayHost = 'localhost'; } } else { logger.success(`Server running at ${chalk.cyan(`http://${displayHost}:${port}`)}`); } logger.info(`Serving directory: ${chalk.yellow(path.relative(process.cwd(), serveDir))}`); logger.separator(); logger.info('Press Ctrl+C to stop the server'); // Open browser if requested if (options.open) { openBrowser(`http://${displayHost}:${port}`); } }); // Graceful shutdown const shutdown = (signal) => { logger.info(`\nReceived ${signal}, shutting down gracefully...`); server.close(() => { logger.success('Server closed'); process.exit(0); }); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); // Handle server errors server.on('error', (err) => { if (err.code === 'EADDRINUSE') { logger.error(`Port ${port} is already in use`); suggestAlternativePort(port); } else { logger.error('Server error:', err.message); } process.exit(1); }); } async function openBrowser(url) { try { const { default: open } = await import('open'); await open(url); logger.success('Browser opened automatically'); } catch (error) { logger.warn('Could not open browser automatically'); logger.info(`Manual: ${url}`); } } async function suggestAlternativePort(currentPort) { logger.info('Trying to find an available port...'); for (let port = currentPort + 1; port <= currentPort + 10; port++) { if (await isPortAvailable(port)) { logger.info(`Try using port ${port}: ${chalk.cyan(`accs serve --port ${port}`)}`); break; } } } function isPortAvailable(port) { return new Promise((resolve) => { const server = createServer(); server.listen(port, () => { server.close(() => resolve(true)); }); server.on('error', () => resolve(false)); }); }