UNPKG

aroma.js

Version:

A lightweight, feature-rich, and developer-friendly web framework to build modern applications with ease.

328 lines (267 loc) 9.65 kB
const http = require('http'); const url = require('url'); const EventEmitter = require('events'); const fastQuerystring = require('fast-querystring'); const { processMiddlewares, matchRoute, processErrorHandlers } = require('./lib/middleware'); const { serveStatic } = require('./lib/static'); const { enableTemplateEngine, render } = require('./lib/template'); const { parseCookies, manageCookies } = require('./lib/cookies'); const { manageSessions, sessions } = require('./lib/session'); const { parseJson, parseUrlEncoded } = require('./lib/bodyParser'); const rateLimiter = require('./lib/rateLimiter'); const logger = require('./lib/logger'); const { version } = require('./package.json'); class Router { constructor() { this.routes = []; this.middlewares = []; } use(path, middleware) { if (typeof path === 'string' && middleware instanceof Router) { this.middlewares.push(async (req, res, next) => { const originalPath = req.path; if (req.path.startsWith(path)) { req.path = req.path.slice(path.length) || '/'; await processMiddlewares(middleware.middlewares, req, res, middleware.routes); const route = matchRoute(middleware.routes, req); if (route) { await route.handler(req, res); } else { next(); } } else { next(); } req.path = originalPath; }); } else if (typeof path === 'function') { this.middlewares.push(path); } else { throw new Error('Invalid middleware or path'); } } route(method, path, ...handlers) { const paramRegex = /:([^/]+)/g; const paramNames = []; const regexPath = path.replace(paramRegex, (_, paramName) => { paramNames.push(paramName); return '([^/]+)'; }); const isWildcard = path.endsWith('*'); const finalRegexPath = isWildcard ? regexPath.slice(0, -1) + '.*' : `^${regexPath}$`; this.routes.push({ method, path: new RegExp(isWildcard ? `^${finalRegexPath}` : finalRegexPath), handler: async (req, res) => { let index = 0; const next = async () => { const handler = handlers[index++]; if (handler) await handler(req, res, next); }; await next(); }, paramNames }); } rateLimiter(options) { rateLimiter(this, options); } logger() { logger(this); } get(path, ...handlers) { this.route('GET', path, ...handlers); } post(path, ...handlers) { this.route('POST', path, ...handlers); } put(path, ...handlers) { this.route('PUT', path, ...handlers); } delete(path, ...handlers) { this.route('DELETE', path, ...handlers); } all(path, ...handlers) { this.route('*', path, ...handlers); } } class Aroma extends EventEmitter { constructor() { super(); this.routes = []; this.middlewares = []; this.errorHandlers = []; this.settings = {}; } set(setting, value) { this.settings[setting] = value; } get(setting) { return this.settings[setting]; } use(path, middleware) { if (!this.middlewares.includes(middleware)) { if (typeof path === 'string' && (middleware instanceof Router || middleware instanceof Aroma)) { this.middlewares.push(async (req, res, next) => { const originalPath = req.path; if (req.path.startsWith(path)) { req.path = req.path.slice(path.length) || '/'; await processMiddlewares(middleware.middlewares, req, res, middleware.routes, true); const route = matchRoute(middleware.routes, req); if (route) { await route.handler(req, res); } else { next(); } } else { next(); } req.path = originalPath; }); } else if (typeof path === 'function') { this.middlewares.push(path); } else { throw new Error('Invalid middleware or path'); } } } route(method, path, ...handlers) { const paramRegex = /:([^/]+)/g; const paramNames = []; const regexPath = path.replace(paramRegex, (_, paramName) => { paramNames.push(paramName); return '([^/]+)'; }); const isWildcard = path.endsWith('*'); const finalRegexPath = isWildcard ? regexPath.slice(0, -1) + '.*' : `^${regexPath}$`; this.routes.push({ method, path: new RegExp(isWildcard ? `^${finalRegexPath}` : finalRegexPath), handler: async (req, res) => { let index = 0; const next = async () => { const handler = handlers[index++]; if (handler) await handler(req, res, next); }; await next(); }, paramNames }); } get(path, ...handlers) { this.route('GET', path, ...handlers); } post(path, ...handlers) { this.route('POST', path, ...handlers); } put(path, ...handlers) { this.route('PUT', path, ...handlers); } delete(path, ...handlers) { this.route('DELETE', path, ...handlers); } all(path, ...handlers) { this.route('*', path, ...handlers); } metrics(path = '/metrics') { let requestCount = 0; this.use((req, res, next) => { requestCount++; next(); }); this.get(path, (req, res) => { res.json({ status: 'ok', uptime: process.uptime(), memory: process.memoryUsage(), requests: requestCount, timestamp: new Date() }); }); } serveStatic(staticPath) { serveStatic(this, staticPath); } enableTemplateEngine(engine) { enableTemplateEngine(this, engine); } useSessions() { parseCookies(this); manageSessions(this); } useCookies() { parseCookies(this); } manageCookies(res, name, value, options = {}) { manageCookies(res, name, value, options); } parseJson() { parseJson(this); } parseUrlEncoded() { parseUrlEncoded(this); } rateLimiter(options) { rateLimiter(this, options); } logger() { logger(this); } render(res, view, data) { render(this, res, view, data); } handleErrors(handler) { this.errorHandlers.push(handler); } listen(port, callback) { const server = http.createServer(async (req, res) => { const parsedUrl = new URL(req.url, `http://${req.headers.host}`); req.query = fastQuerystring.parse(parsedUrl.search.slice(1)); req.path = parsedUrl.pathname; res.statusCode = 200; res.setHeader('X-Powered-By', `Aroma.js/${version}`); res.status = function (statusCode) { this.statusCode = statusCode; return this; }; res.json = function (data) { if (this.writableEnded) return; this.writeHead(this.statusCode || 200, { 'Content-Type': 'application/json' }); this.end(JSON.stringify(data, null, 0)); }; res.send = function (data) { if (this.writableEnded) return; if (!this.statusCode) this.statusCode = 200; if (typeof data === 'object') { this.setHeader('Content-Type', 'application/json'); this.end(JSON.stringify(data)); } else { this.end(data); } }; try { await processMiddlewares(this.middlewares, req, res, this.routes); const route = matchRoute(this.routes, req); if (route) { if (!res.writableEnded) { await route.handler(req, res); } } else { if (!res.writableEnded) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('404 Not Found'); } } } catch (err) { await processErrorHandlers(this.errorHandlers, err, req, res); } }); server.listen(port, callback); } } module.exports = Aroma; module.exports.Router = Router;