UNPKG

rynex

Version:

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

201 lines 7.25 kB
/** * Production Server * Uses Express if available, falls back to native HTTP */ import * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; import { logger } from './logger.js'; import { scanRoutes } from './route-scanner.js'; /** * Try to load Express, return null if not available */ async function tryLoadExpress() { try { const express = await import('express'); return express.default; } catch (error) { return null; } } /** * Start production server with Express */ function startWithExpress(express, options, routeManifest) { const app = express(); const { port, root } = options; // Logging middleware app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; const status = res.statusCode; const color = status >= 500 ? '\x1b[31m' : status >= 400 ? '\x1b[33m' : '\x1b[32m'; logger.debug(`${req.method} ${req.url} ${color}${status}\x1b[0m - ${duration}ms`); }); next(); }); // Compression (if available) tryLoadExpress().then(async () => { try { const compression = await import('compression'); app.use(compression.default()); logger.info('Compression enabled'); } catch (e) { logger.debug('Compression not available'); } }); // Static files with proper headers app.use(express.static(root, { maxAge: '1d', etag: true, setHeaders: (res, filePath) => { // Set proper content types if (filePath.endsWith('.js') || filePath.endsWith('.mjs')) { res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); } else if (filePath.endsWith('.css')) { res.setHeader('Content-Type', 'text/css; charset=utf-8'); } // Prevent HTML caching to ensure updates are always fetched if (filePath.endsWith('.html')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } } })); // SPA fallback for file-based routing if (routeManifest && routeManifest.routes.length > 0) { app.get('*', (req, res) => { const indexPath = path.join(root, 'index.html'); if (fs.existsSync(indexPath)) { res.sendFile(indexPath); } else { res.status(404).send('404 Not Found'); } }); } app.listen(port, () => { logger.success(`Production server running at http://localhost:${port}`); logger.info(`Serving files from: ${root}`); logger.info('Using Express server'); }); } /** * Start production server with native HTTP */ function startWithNativeHTTP(options, routeManifest) { const { port, root } = options; const server = http.createServer((req, res) => { const url = req.url || '/'; const [pathname] = url.split('?'); // Determine file path let filePath = path.join(root, pathname === '/' ? 'index.html' : pathname); // Check if file exists if (!fs.existsSync(filePath)) { // Try adding .html if (fs.existsSync(filePath + '.html')) { filePath = filePath + '.html'; } else if (routeManifest && routeManifest.routes.length > 0) { // SPA fallback filePath = path.join(root, 'index.html'); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('404 Not Found'); return; } } // Check if it's a directory if (fs.statSync(filePath).isDirectory()) { filePath = path.join(filePath, 'index.html'); if (!fs.existsSync(filePath)) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('404 Not Found'); return; } } // Determine content type const ext = path.extname(filePath); const contentTypes = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.mjs': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.webp': 'image/webp', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject', '.map': 'application/json' }; const contentType = contentTypes[ext] || 'application/octet-stream'; // Read and serve file fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('500 Internal Server Error'); return; } // Set caching headers const isHTML = ext === '.html'; const cacheControl = isHTML ? 'no-cache, no-store, must-revalidate' : 'public, max-age=86400'; const headers = { 'Content-Type': contentType, 'Cache-Control': cacheControl, 'ETag': `"${data.length}-${fs.statSync(filePath).mtime.getTime()}"` }; if (isHTML) { headers['Pragma'] = 'no-cache'; headers['Expires'] = '0'; } res.writeHead(200, headers); res.end(data); }); }); server.listen(port, () => { logger.success(`Production server running at http://localhost:${port}`); logger.info(`Serving files from: ${root}`); logger.info('Using native HTTP server'); }); } /** * Start production server */ export async function startProductionServer(options) { const { root, config } = options; // 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'); if (fs.existsSync(pagesDir)) { routeManifest = scanRoutes(pagesDir); logger.info(`File-based routing enabled with ${routeManifest.routes.length} routes`); } } // Try to use Express const express = await tryLoadExpress(); if (express) { startWithExpress(express, options, routeManifest); } else { logger.warning('Express not found, using native HTTP server'); logger.info('Install Express for better performance: pnpm add express'); startWithNativeHTTP(options, routeManifest); } } //# sourceMappingURL=prod-server.js.map