veko
Version:
Ultra-lightweight Node.js framework with ZERO dependencies - VSV components, Tailwind CSS, asset imports, SSR
657 lines (570 loc) • 18.3 kB
JavaScript
/**
* VekoJS - Zero Dependencies Framework
* Pure Node.js HTTP Server with VSV support
*/
const http = require('http');
const https = require('https');
const path = require('path');
const fs = require('fs');
const url = require('url');
const { EventEmitter } = require('events');
const crypto = require('crypto');
/**
* Simple Logger (no dependencies)
*/
class Logger {
constructor(options = {}) {
this.silent = options.silent || false;
this.verbose = options.verbose || false;
}
log(type, message, details = '') {
if (this.silent) return;
const colors = {
info: '\x1b[36m',
success: '\x1b[32m',
warning: '\x1b[33m',
error: '\x1b[31m',
reset: '\x1b[0m'
};
const color = colors[type] || colors.info;
const symbol = { info: 'ℹ', success: '✓', warning: '⚠', error: '✗' }[type] || '●';
console.log(`${color}${symbol} ${message}${details ? ` ${details}` : ''}${colors.reset}`);
}
}
/**
* Simple Rate Limiter (no dependencies)
*/
class RateLimiter {
constructor(options = {}) {
this.windowMs = options.windowMs || 15 * 60 * 1000;
this.max = options.max || 100;
this.message = options.message || 'Too many requests';
this.requests = new Map();
}
check(ip) {
const now = Date.now();
const record = this.requests.get(ip);
if (!record || now - record.start > this.windowMs) {
this.requests.set(ip, { start: now, count: 1 });
return true;
}
if (record.count >= this.max) {
return false;
}
record.count++;
return true;
}
middleware() {
return (req, res, next) => {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
if (!this.check(ip)) {
res.statusCode = 429;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: this.message }));
return;
}
next();
};
}
}
/**
* Request/Response Helpers
*/
function parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk;
if (body.length > 10 * 1024 * 1024) { // 10MB limit
reject(new Error('Request body too large'));
}
});
req.on('end', () => {
try {
const contentType = req.headers['content-type'] || '';
if (contentType.includes('application/json')) {
resolve(JSON.parse(body || '{}'));
} else if (contentType.includes('application/x-www-form-urlencoded')) {
resolve(Object.fromEntries(new URLSearchParams(body)));
} else {
resolve(body);
}
} catch (e) {
resolve(body);
}
});
req.on('error', reject);
});
}
function parseCookies(cookieHeader) {
const cookies = {};
if (!cookieHeader) return cookies;
cookieHeader.split(';').forEach(cookie => {
const [name, ...rest] = cookie.trim().split('=');
cookies[name] = rest.join('=');
});
return cookies;
}
/**
* Static File Server
*/
const MIME_TYPES = {
'.html': 'text/html',
'.htm': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.mp3': 'audio/mpeg',
'.wav': 'audio/wav',
'.pdf': 'application/pdf',
'.zip': 'application/zip',
'.txt': 'text/plain',
'.xml': 'application/xml',
'.vsv': 'text/html',
'.jsv': 'text/html'
};
function serveStatic(staticDir, maxAge = 0) {
const root = path.resolve(process.cwd(), staticDir);
return async (req, res, next) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
return next();
}
const urlPath = decodeURIComponent(url.parse(req.url).pathname);
// Prevent directory traversal
if (urlPath.includes('..') || urlPath.includes('\0')) {
return next();
}
const filePath = path.join(root, urlPath);
// Make sure we're still in the static directory
if (!filePath.startsWith(root)) {
return next();
}
try {
const stat = await fs.promises.stat(filePath);
if (stat.isDirectory()) {
// Try index.html
const indexPath = path.join(filePath, 'index.html');
try {
await fs.promises.access(indexPath);
return serveFile(res, indexPath, maxAge);
} catch {
return next();
}
}
return serveFile(res, filePath, maxAge);
} catch {
return next();
}
};
}
async function serveFile(res, filePath, maxAge = 0) {
const ext = path.extname(filePath).toLowerCase();
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
const stat = await fs.promises.stat(filePath);
res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Length', stat.size);
res.setHeader('Cache-Control', maxAge > 0 ? `public, max-age=${maxAge}` : 'no-cache');
const stream = fs.createReadStream(filePath);
stream.pipe(res);
}
/**
* Router
*/
class Router {
constructor() {
this.routes = [];
this.middlewares = [];
}
use(pathOrFn, fn) {
if (typeof pathOrFn === 'function') {
this.middlewares.push({ path: '/', handler: pathOrFn });
} else {
this.middlewares.push({ path: pathOrFn, handler: fn });
}
}
add(method, path, ...handlers) {
const pattern = this.pathToRegex(path);
this.routes.push({ method: method.toUpperCase(), path, pattern, handlers });
}
get(path, ...handlers) { this.add('GET', path, ...handlers); }
post(path, ...handlers) { this.add('POST', path, ...handlers); }
put(path, ...handlers) { this.add('PUT', path, ...handlers); }
delete(path, ...handlers) { this.add('DELETE', path, ...handlers); }
patch(path, ...handlers) { this.add('PATCH', path, ...handlers); }
all(path, ...handlers) { this.add('ALL', path, ...handlers); }
pathToRegex(path) {
if (path instanceof RegExp) return { regex: path, keys: [] };
const keys = [];
const pattern = path
.replace(/\*/g, '.*')
.replace(/:(\w+)/g, (_, key) => {
keys.push(key);
return '([^/]+)';
});
return { regex: new RegExp(`^${pattern}/?$`), keys };
}
match(method, pathname) {
for (const route of this.routes) {
if (route.method !== 'ALL' && route.method !== method) continue;
const match = pathname.match(route.pattern.regex);
if (match) {
const params = {};
route.pattern.keys.forEach((key, i) => {
params[key] = match[i + 1];
});
return { route, params };
}
}
return null;
}
}
/**
* Main App Class
*/
class App extends EventEmitter {
constructor(options = {}) {
super();
this.options = {
port: options.port || 3000,
host: options.host || '0.0.0.0',
staticDir: options.staticDir || 'public',
viewsDir: options.viewsDir || 'views',
isDev: options.isDev || process.env.NODE_ENV !== 'production',
...options
};
this.router = new Router();
this.logger = new Logger({ verbose: this.options.isDev });
this.server = null;
this.vsv = null;
// Rate limiter
if (options.rateLimit !== false) {
this.rateLimiter = new RateLimiter(options.rateLimit || {});
this.use(this.rateLimiter.middleware());
}
// Static files
if (this.options.staticDir) {
this.use(serveStatic(this.options.staticDir, this.options.isDev ? 0 : 86400));
}
}
// Middleware
use(pathOrFn, fn) {
this.router.use(pathOrFn, fn);
return this;
}
// Routes
get(path, ...handlers) { this.router.get(path, ...handlers); return this; }
post(path, ...handlers) { this.router.post(path, ...handlers); return this; }
put(path, ...handlers) { this.router.put(path, ...handlers); return this; }
delete(path, ...handlers) { this.router.delete(path, ...handlers); return this; }
patch(path, ...handlers) { this.router.patch(path, ...handlers); return this; }
all(path, ...handlers) { this.router.all(path, ...handlers); return this; }
// Backward compatibility
createRoute(method, path, ...handlers) {
const m = method.toLowerCase();
if (this[m]) {
this[m](path, ...handlers);
} else {
this.router.add(method, path, ...handlers);
}
return this;
}
// Request handler
async handleRequest(req, res) {
const startTime = Date.now();
// Parse URL
const parsedUrl = url.parse(req.url, true);
req.pathname = parsedUrl.pathname;
req.query = parsedUrl.query;
req.cookies = parseCookies(req.headers.cookie);
// Enhanced response methods
res.json = (data) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data));
};
res.html = (html) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(html);
};
res.send = (data) => {
if (typeof data === 'object') {
res.json(data);
} else {
res.setHeader('Content-Type', 'text/plain');
res.end(String(data));
}
};
res.redirect = (location, statusCode = 302) => {
res.statusCode = statusCode;
res.setHeader('Location', location);
res.end();
};
res.status = (code) => {
res.statusCode = code;
return res;
};
res.setCookie = (name, value, options = {}) => {
let cookie = `${name}=${encodeURIComponent(value)}`;
if (options.maxAge) cookie += `; Max-Age=${options.maxAge}`;
if (options.path) cookie += `; Path=${options.path}`;
if (options.httpOnly) cookie += '; HttpOnly';
if (options.secure) cookie += '; Secure';
if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
res.setHeader('Set-Cookie', cookie);
};
// Security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('X-XSS-Protection', '1; mode=block');
// Parse body for POST/PUT/PATCH
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
try {
req.body = await parseBody(req);
} catch (e) {
res.statusCode = 400;
res.json({ error: 'Invalid request body' });
return;
}
}
// Run middlewares
let middlewareIndex = 0;
const middlewares = this.router.middlewares;
const runNext = async () => {
if (middlewareIndex < middlewares.length) {
const mw = middlewares[middlewareIndex++];
if (req.pathname.startsWith(mw.path)) {
await new Promise((resolve, reject) => {
try {
const result = mw.handler(req, res, (err) => {
if (err) reject(err);
else resolve();
});
if (result instanceof Promise) {
result.then(resolve).catch(reject);
}
} catch (e) {
reject(e);
}
});
if (!res.writableEnded) {
await runNext();
}
} else {
await runNext();
}
}
};
try {
await runNext();
if (res.writableEnded) return;
// Match route
const match = this.router.match(req.method, req.pathname);
if (match) {
req.params = match.params;
for (const handler of match.route.handlers) {
if (res.writableEnded) break;
await handler(req, res);
}
} else {
// 404
res.statusCode = 404;
res.html(`<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5;">
<div style="text-align: center;">
<h1 style="font-size: 72px; margin: 0; color: #333;">404</h1>
<p style="color: #666;">Page not found</p>
</div>
</body>
</html>`);
}
} catch (error) {
this.handleError(error, req, res);
}
// Log request
const duration = Date.now() - startTime;
if (this.options.isDev) {
const statusColor = res.statusCode >= 400 ? '\x1b[31m' : '\x1b[32m';
console.log(`${statusColor}${req.method}\x1b[0m ${req.pathname} ${res.statusCode} ${duration}ms`);
}
}
// Error handler
handleError(error, req, res) {
console.error('\x1b[31mError:\x1b[0m', error.message);
if (this.options.isDev) {
console.error(error.stack);
}
res.statusCode = 500;
res.html(`<!DOCTYPE html>
<html>
<head>
<title>500 - Server Error</title>
<style>
body { font-family: system-ui; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #fff; margin: 0; min-height: 100vh; display: flex; justify-content: center; align-items: center; }
.container { max-width: 800px; padding: 40px; }
h1 { color: #ff6b6b; margin-bottom: 20px; }
.error-box { background: rgba(255,107,107,0.1); border: 1px solid #ff6b6b; border-radius: 8px; padding: 20px; }
pre { background: #0d1117; padding: 15px; border-radius: 6px; overflow-x: auto; font-size: 14px; }
.stack { color: #8b949e; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<h1>⚠️ Server Error</h1>
<div class="error-box">
<strong>${this.escapeHtml(error.message)}</strong>
${this.options.isDev ? `<pre class="stack">${this.escapeHtml(error.stack)}</pre>` : ''}
</div>
</div>
</body>
</html>`);
}
escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
// VSV Support
async enableVSV(options = {}) {
const VSV = require('./vsv');
this.vsv = new VSV(this, options);
await this.vsv.init();
return this;
}
isVSVEnabled() {
return this.vsv !== null;
}
vsvRoute(routePath, component, options = {}) {
if (!this.isVSVEnabled()) {
throw new Error('VSV not enabled. Call enableVSV() first.');
}
this.get(routePath, async (req, res) => {
const props = {
...options.props,
params: req.params,
query: req.query,
path: req.pathname
};
if (options.getProps) {
const fetchedProps = await options.getProps(req);
Object.assign(props, fetchedProps);
}
const html = await this.vsv.renderPage(component, props, {
title: options.title || component,
seo: options.seo,
_req: req,
_res: res,
...options
});
res.html(html);
});
return this;
}
async renderVSV(component, props = {}, options = {}) {
if (!this.isVSVEnabled()) {
throw new Error('VSV not enabled. Call enableVSV() first.');
}
return this.vsv.render(component, props, options);
}
/**
* Route to a PHP-like template file
* Usage: app.phpRoute('/about', 'about.php', { title: 'About' })
*/
phpRoute(routePath, template, options = {}) {
if (!this.isVSVEnabled()) {
throw new Error('VSV not enabled. Call enableVSV() first.');
}
this.get(routePath, async (req, res) => {
const data = {
...options.data,
params: req.params,
query: req.query,
path: req.pathname,
title: options.title || template
};
if (options.getData) {
const fetched = await options.getData(req, res);
Object.assign(data, fetched);
}
try {
const html = await this.vsv.php.renderFile(template, data, req, res);
if (!res.writableEnded) {
res.html(html);
}
} catch (e) {
if (e.__redirect) return; // redirect was handled
throw e;
}
});
return this;
}
/**
* Render a PHP-like template string
*/
async renderPHP(template, data = {}, req = null, res = null) {
if (!this.isVSVEnabled()) {
throw new Error('VSV not enabled. Call enableVSV() first.');
}
return this.vsv.php.renderString(template, data, req, res);
}
/**
* Register a global variable for PHP templates
*/
phpGlobal(name, value) {
if (!this.isVSVEnabled()) {
throw new Error('VSV not enabled. Call enableVSV() first.');
}
this.vsv.php.setGlobal(name, value);
return this;
}
/**
* Register a custom PHP-like function
*/
phpFunction(name, fn) {
if (!this.isVSVEnabled()) {
throw new Error('VSV not enabled. Call enableVSV() first.');
}
this.vsv.php.registerFunction(name, fn);
return this;
}
// Listen
listen(port, callback) {
// Support listen(callback) without port
if (typeof port === 'function') {
callback = port;
port = null;
}
const finalPort = Number(port) || this.options.port;
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
this.server.listen(finalPort, this.options.host, () => {
this.logger.log('success', `Server running at http://localhost:${finalPort}`);
if (callback) callback();
this.emit('listening', finalPort);
});
return this.server;
}
// Close
close() {
if (this.server) {
this.server.close();
}
}
}
module.exports = App;