UNPKG

@razen-core/zenweb

Version:

A minimalist TypeScript framework for building reactive web applications with no virtual DOM

199 lines 7.16 kB
/** * ZenWeb Development Server * Hot-reload development server */ import * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; import { watch } from 'chokidar'; import { logger } from './logger.js'; import { scanRoutes } from './route-scanner.js'; /** * Start development server */ export async function startDevServer(options) { const { port, root, hotReload, routes, config } = options; // Middleware stack const middlewareStack = []; // Scan routes if file-based routing is enabled let routeManifest = null; if (config?.routing?.fileBasedRouting) { const pagesDir = path.join(process.cwd(), config.routing.pagesDir || 'src/pages'); routeManifest = scanRoutes(pagesDir); logger.info(`File-based routing enabled with ${routeManifest.routes.length} routes`); } const clients = []; // Add CORS middleware middlewareStack.push((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } next(); }); // Add logging middleware middlewareStack.push((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.debug(`${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`); }); next(); }); // Create HTTP server const server = http.createServer((req, res) => { // Run middleware stack let middlewareIndex = 0; const runMiddleware = () => { if (middlewareIndex >= middlewareStack.length) { handleRequest(req, res); return; } const middleware = middlewareStack[middlewareIndex++]; middleware(req, res, runMiddleware); }; runMiddleware(); }); // Request handler function handleRequest(req, res) { const url = req.url || '/'; // Handle SSE for hot reload if (hotReload && url === '/__zenweb_hmr') { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); clients.push(res); req.on('close', () => { const index = clients.indexOf(res); if (index !== -1) { clients.splice(index, 1); } }); return; } // Parse URL and query params const [pathname, queryString] = url.split('?'); // Check if URL matches a route let filePath = path.join(root, pathname === '/' ? 'index.html' : pathname); // Match against file-based routes first if (routeManifest && routeManifest.routes.length > 0) { const matchedRoute = matchRoute(pathname, routeManifest.routes); if (matchedRoute) { // For SPA, serve index.html and let client-side router handle it filePath = path.join(root, 'index.html'); logger.debug(`Matched file-based route ${pathname}`); } } else if (routes && routes.length > 0) { const matchedRoute = routes.find(route => route.path === pathname); if (matchedRoute && matchedRoute.component) { // Serve the component HTML file for this route filePath = path.join(root, matchedRoute.component); logger.debug(`Matched route ${pathname} to ${matchedRoute.component}`); } } // Check if file exists if (!fs.existsSync(filePath)) { // Try adding .html if (fs.existsSync(filePath + '.html')) { filePath = filePath + '.html'; } else { res.writeHead(404); res.end('404 Not Found'); return; } } // Determine content type const ext = path.extname(filePath); const contentTypes = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' }; const contentType = contentTypes[ext] || 'text/plain'; // Read and serve file fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(500); res.end('500 Internal Server Error'); return; } res.writeHead(200, { 'Content-Type': contentType }); // Inject HMR script into HTML if (hotReload && ext === '.html') { const html = data.toString(); const hmrScript = ` <script> const eventSource = new EventSource('/__zenweb_hmr'); eventSource.onmessage = (event) => { if (event.data === 'reload') { console.log('[ZenWeb] Reloading...'); window.location.reload(); } }; </script> `; const modifiedHtml = html.replace('</body>', `${hmrScript}</body>`); res.end(modifiedHtml); } else { res.end(data); } }); } // Helper function to match routes function matchRoute(pathname, routes) { for (const route of routes) { const pattern = routePathToRegex(route.path); if (pattern.test(pathname)) { return route; } } return null; } // Convert route path to regex function routePathToRegex(routePath) { let pattern = routePath; // Dynamic segments: :id -> ([^/]+) pattern = pattern.replace(/:([^/]+)/g, '([^/]+)'); // Wildcards: * -> (.*) pattern = pattern.replace(/\*/g, '(.*)'); return new RegExp(`^${pattern}$`); } // Watch for file changes if (hotReload) { const watcher = watch(root, { ignored: /(^|[\/\\])\../, // ignore dotfiles persistent: true }); watcher.on('change', (filePath) => { logger.info(`File changed: ${filePath}`); // Notify all connected clients clients.forEach(client => { client.write('data: reload\n\n'); }); }); } // Start server server.listen(port, () => { logger.success(`Dev server running at http://localhost:${port}`); logger.info(`Serving files from: ${root}`); if (hotReload) { logger.info('Hot reload enabled'); } }); } //# sourceMappingURL=dev-server.js.map