UNPKG

nutraj

Version:

A lightweight Express‑like framework with multi‑domain support, middleware, dynamic routing, static file serving, and built‑in utilities such as cookie parsing, logging, CORS, sessions, and optional API statistics.

397 lines (363 loc) 11.6 kB
// nutraj.js const http = require('http'); const fs = require('fs'); const path = require('path'); const querystring = require('querystring'); const url = require('url'); const formidable = require('formidable'); // For multipart/form-data parsing function Nutraj() { const routes = []; const middlewares = []; const domains = {}; // In-memory session store. const sessionStore = {}; function generateSessionId() { return Math.random().toString(36).substring(2) + Date.now().toString(36); } // Helper to compile parameterized routes like '/user/:id' function compilePath(routePath) { const keys = []; const regexString = routePath .replace(/([.+*?=^!:${}()[\]|\/\\])/g, '\\$1') .replace(/\\:(\w+)/g, (match, key) => { keys.push(key); return '([^/]+)'; }); const regex = new RegExp('^' + regexString + '$'); return { regex, keys }; } // --- Built-in Middlewares --- // Cookie parser: populates req.cookies with key/value pairs. function cookieParser(req, res, next) { req.cookies = {}; const cookieHeader = req.headers.cookie; if (cookieHeader) { cookieHeader.split(';').forEach(cookie => { const parts = cookie.split('='); req.cookies[parts[0].trim()] = decodeURIComponent(parts[1] || ''); }); } next(); } // Logger: logs request method, URL, status code and duration. function logger(req, res, next) { const start = Date.now(); console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); res.on('finish', () => { const duration = Date.now() - start; console.log( `[${new Date().toISOString()}] ${req.method} ${req.url} ${res.statusCode} - ${duration}ms` ); }); next(); } // CORS: sets Access-Control headers. function cors(options = {}) { return function (req, res, next) { res.setHeader('Access-Control-Allow-Origin', options.origin || '*'); res.setHeader( 'Access-Control-Allow-Methods', options.methods || 'GET,HEAD,PUT,PATCH,POST,DELETE' ); res.setHeader( 'Access-Control-Allow-Headers', options.headers || 'Origin, X-Requested-With, Content-Type, Accept, Authorization' ); if (req.method === 'OPTIONS') { res.writeHead(204); return res.end(); } next(); }; } /** * Body parser middleware: * - Parses JSON if Content-Type includes "application/json" * - Parses URL-encoded data if Content-Type includes "application/x-www-form-urlencoded" * - Uses formidable to parse multipart/form-data (for file uploads) */ function bodyParser(req, res, next) { const contentType = req.headers['content-type'] || ''; const method = req.method.toLowerCase(); if (['post', 'put', 'patch'].includes(method)) { if (contentType.includes('multipart/form-data')) { const form = formidable({ multiples: true }); form.parse(req, (err, fields, files) => { if (err) { next(err); return; } req.body = fields; req.files = files; next(); }); return; } else { let data = []; req.on('data', chunk => data.push(chunk)); req.on('end', () => { const rawData = Buffer.concat(data).toString(); if (contentType.includes('application/json')) { try { req.body = JSON.parse(rawData); } catch (e) { req.body = {}; } } else if (contentType.includes('application/x-www-form-urlencoded')) { req.body = querystring.parse(rawData); } next(); }); return; } } next(); } /** * Session middleware: * - Looks for a cookie "nutraj_session"; if missing, creates one. * - Attaches a session object to req.session and saves it in memory. */ function sessionMiddleware(req, res, next) { const cookies = req.cookies || {}; let sessionId = cookies['nutraj_session']; if (!sessionId) { sessionId = generateSessionId(); res.setHeader('Set-Cookie', `nutraj_session=${sessionId}; HttpOnly`); } req.session = sessionStore[sessionId] || {}; res.on('finish', () => { sessionStore[sessionId] = req.session; }); next(); } /** * Static file serving middleware generator. * Use as: app.use(app.static('public')) */ function staticMiddleware(staticDir) { return function (req, res, next) { const filePath = path.join(staticDir, req.url); fs.stat(filePath, (err, stats) => { if (err || !stats.isFile()) { return next(); } const stream = fs.createReadStream(filePath); stream.on('error', next); stream.pipe(res); }); }; } /** * User-Agent parsing middleware. * Attaches the user-agent string from the request headers to req.useragent. */ function userAgentMiddleware(req, res, next) { req.useragent = req.headers['user-agent'] || ''; next(); } /** * API Statistics middleware: * When used, it sets a flag on the request (req.enableStats = true) * so that the matched route will record its statistics. */ function statsMiddleware(req, res, next) { req.enableStats = true; next(); } /** * Enhance the response object with helper methods: * - res.send(): sends a string or JSON response. * - res.json(): sends a JSON response. * - res.sendFile(): streams a file to the client. * - res.redirect(): redirects the client to a URL. */ function enhanceResponse(req, res) { res.send = function (body) { if (typeof body === 'object') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(body)); } else { res.setHeader('Content-Type', 'text/html'); res.end(String(body)); } }; res.json = function (data) { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data)); }; res.sendFile = function (filePath) { fs.stat(filePath, (err, stats) => { if (err || !stats.isFile()) { res.statusCode = 404; res.end('File not found'); } else { const stream = fs.createReadStream(filePath); stream.on('error', () => { res.statusCode = 500; res.end('Error reading file'); }); stream.pipe(res); } }); }; res.redirect = function (redirectUrl, statusCode) { statusCode = statusCode || 302; res.statusCode = statusCode; res.setHeader('Location', redirectUrl); res.end(); }; } /** * The main application function. * - Enhances req and res objects. * - Parses the URL to extract query parameters and pathname. * - Checks for multi-domain delegation. * - Processes middleware and routes. * - Uses default error handling if needed. */ function app(req, res) { enhanceResponse(req, res); // Parse URL to populate req.query and req.path. const parsedUrl = url.parse(req.url, true); req.query = parsedUrl.query; req.path = parsedUrl.pathname; // Multi-domain handling: delegate if a sub‑app is registered. let host = req.headers.host; if (host) { host = host.split(':')[0]; if (domains[host]) { return domains[host](req, res); } } let idx = 0; function next(err) { if (idx < middlewares.length) { const middleware = middlewares[idx++]; if (err) { if (middleware.length === 4) { return middleware(err, req, res, next); } return next(err); } else { if (middleware.length < 4) { return middleware(req, res, next); } return next(); } } else { if (err) { res.statusCode = 500; res.end('Internal Server Error: ' + err.message); } else { handleRoutes(); } } } function handleRoutes() { const method = req.method.toLowerCase(); for (const route of routes) { if (route.method === method || route.method === 'all') { if (route.regex) { const match = route.regex.exec(req.path); if (match) { req.params = {}; route.keys.forEach((key, index) => { req.params[key] = match[index + 1]; }); recordAndHandle(route); return; } } else if (route.path === req.path) { req.params = {}; recordAndHandle(route); return; } } } res.statusCode = 404; res.end('Not Found'); } // If API statistics are enabled for this request (req.enableStats), // record the start time and update stats when the response finishes. function recordAndHandle(route) { if (req.enableStats) { const startTime = Date.now(); res.on('finish', () => { const elapsed = Date.now() - startTime; route.stats.hits++; route.stats.totalTime += elapsed; route.stats.averageTime = route.stats.totalTime / route.stats.hits; }); } route.handler(req, res); } next(); } // --- API for Middleware and Routing --- // Register middleware. Use either a path prefix or a function. app.use = function (pathOrMiddleware, maybeMiddleware) { if (typeof pathOrMiddleware === 'string') { const pathPrefix = pathOrMiddleware; const middleware = maybeMiddleware; middlewares.push((req, res, next) => { if (req.path.startsWith(pathPrefix)) { return middleware(req, res, next); } next(); }); } else { middlewares.push(pathOrMiddleware); } }; // Routing methods with dynamic parameter support. ['get', 'post', 'put', 'delete', 'patch', 'all'].forEach(method => { app[method] = function (routePath, handler) { const route = { method, path: routePath, handler, stats: { hits: 0, totalTime: 0, averageTime: 0 } }; if (routePath.includes(':')) { const { regex, keys } = compilePath(routePath); route.regex = regex; route.keys = keys; } routes.push(route); }; }); // Register sub‑apps for specific domains. app.domain = function (domain, subApp) { domains[domain] = subApp; }; // --- Expose built‑in middlewares --- app.cookieParser = cookieParser; app.logger = logger; app.cors = cors; app.bodyParser = bodyParser; app.session = sessionMiddleware; app.static = staticMiddleware; app.useragent = userAgentMiddleware; // API statistics middleware – add with app.use(app.stats) to enable stats tracking. app.stats = statsMiddleware; // --- Built-in API Statistics Endpoint --- // Access at GET /__stats to view stats for each registered route. app.get('/__stats', (req, res) => { const stats = routes.map(route => ({ method: route.method, path: route.path, hits: route.stats.hits, averageResponseTime: route.stats.averageTime })); res.json({ routes: stats }); }); // Start the HTTP server. app.listen = function (port, callback) { const server = http.createServer(app); server.listen(port, callback); return server; }; return app; } module.exports = Nutraj;