rynex
Version:
A minimalist TypeScript framework for building reactive web applications with no virtual DOM
201 lines • 7.25 kB
JavaScript
/**
* 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