import http, { IncomingMessage, ServerResponse } from "node:http"; import { URL } from "node:url"; import { Buffer } from "node:buffer"; import querystring from "node:querystring"; import Router from "./router.js"; import Tpl from "./template.js"; export { Router, Tpl, Request }; type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void; interface Request extends IncomingMessage { parsedUrl: { pathname: string; split: string[]; searchParams: URLSearchParams; qs: Record; }; cookies: Record; params?: Record; post?: Record; }; export default class Flummpress { private server?: http.Server; router: Router; tpl: Tpl; middleware: Set; constructor() { this.router = new Router(); this.tpl = new Tpl(); this.middleware = new Set(); } /** * Adds middleware, routes, or template systems to the server. * @param {any} obj - An instance of Router, Tpl, or a middleware function. * @returns {this} - The current instance for chaining. */ use(obj: Router | Tpl): this { if(obj instanceof Router) this.router.use(obj); else if(obj instanceof Tpl) this.tpl = obj; else { if(!this.middleware.has(obj)) this.middleware.add(obj); } return this; } /** * Starts the HTTP server and begins listening for incoming requests. * @param {...any} args - Arguments passed to `http.Server.listen`. * @returns {this} - The current instance for chaining. */ listen(...args: any[]): this { this.server = http.createServer(async (request: IncomingMessage, res: ServerResponse) => { const req = request as Request; const t_start = process.hrtime(); // URL and query parsing const _url = new URL(req.url!.replace(/(?!^.)(\/+)?$/, ''), "http://localhost"); req.parsedUrl = { pathname: _url.pathname, split: _url.pathname.split("/").slice(1), searchParams: _url.searchParams, qs: Object.fromEntries(Object.entries(querystring.parse(_url.search.substring(1))).filter(([_, v]) => typeof v === 'string')) as Record, }; // extract cookies req['cookies'] = {}; if(req.headers.cookie) { req.headers.cookie.split("; ").forEach((c: string) => { const parts = c.split('='); req.cookies[parts.shift()?.trim() ?? ""] = decodeURI(parts.join('=')); }); } // run middleware await Promise.all([...this.middleware].map(m => m(req, res, () => {}))); // route and method handling const method = req.method === 'HEAD' ? 'get' : req.method?.toLowerCase(); const route = this.router.getRoute(req.parsedUrl.pathname, req.method === 'HEAD' ? 'GET' : req.method!); if(route) { // route found const cb = route[1][method!]; const middleware = route[1][`${method}mw`]; req['params'] = req.parsedUrl.pathname.match(new RegExp(route[0]))?.groups; req['post'] = await this.readBody(req); const result = await this.processMiddleware(middleware, req, this.createResponse(req, res)); if(result) cb(req, res); } else { // route not found res.writeHead(404).end(`404 - ${req.method} ${req.parsedUrl.pathname}`); } console.log([ `[${new Date().toLocaleTimeString()}]`, `${(process.hrtime(t_start)[1] / 1e6).toFixed(2)}ms`, `${req.method} ${res.statusCode}`, req.parsedUrl.pathname ].map(e => e.toString().padEnd(15)).join("")); }).listen(...args); return this; } /** * Reads and parses the body of an incoming HTTP request. * @param {IncomingMessage} req - The HTTP request object. * @returns {Promise} - A promise that resolves to the parsed body data. */ readBody(req: IncomingMessage): Promise> { return new Promise((resolve, reject) => { let data = ""; req.on("data", (chunk: string) => data += chunk); req.on("end", () => { if(req.headers["content-type"] === "application/json") { try { resolve(JSON.parse(data) as Record); } catch(err: any) { reject(err); } } else { const parsedData = querystring.parse(data); const decodedData = Object.fromEntries( Object.entries(parsedData).map(([key, value]) => { try { return [key, decodeURIComponent(value as string)]; } catch(err: any) { return [key, value]; } }) ); resolve(decodedData as Record); } }); req.on("error", (err: Error) => reject(err)); }); } /** * Enhances the HTTP response object with additional methods for convenience. * @param {Request} req - The HTTP request object. * @param {ServerResponse} res - The HTTP response object. * @returns {ServerResponse & { * reply: Function, json: Function, html: Function, redirect: Function * }} - The enhanced response object. */ createResponse(req: Request, res: ServerResponse): ServerResponse & { reply: (options: { code?: number; type?: string; body: string }) => ServerResponse; json: (body: string, code?: number) => ServerResponse; html: (body: string, code?: number) => ServerResponse; redirect: (target: string, code?: number) => void; } { const enhancedRes = Object.assign(res, { reply: ({ code = 200, type = "text/html", body }: { code?: number; type?: string; body: string }) => { res.writeHead(code, { "content-type": `${type}; charset=UTF-8`, "content-length": Buffer.byteLength(body, "utf-8"), }); if(req.method === "HEAD") body = null as unknown as string; res.end(body); return res; }, json: (body: string, code = 200) => { if(typeof body === "object") body = JSON.stringify(body); return enhancedRes.reply({ code, body, type: "application/json" }); }, html: (body: string, code = 200) => enhancedRes.reply({ code, body, type: "text/html" }), redirect: (target: string, code = 307) => { res.writeHead(code, { "Cache-Control": "no-cache, public", "Location": target, }); res.end(); } }); return enhancedRes; } /** * Processes middleware functions for an incoming request. * @param {Middleware} middleware - The middleware function to process. * @param {IncomingMessage} req - The HTTP request object. * @param {ServerResponse} res - The HTTP response object. * @returns {Promise} - Resolves to true if middleware processing is successful. */ private async processMiddleware( middleware: Middleware[], req: IncomingMessage, res: ServerResponse ): Promise { if(!middleware || middleware.length === 0) return true; for(const fn of middleware) { const proceed = await new Promise(resolve => { fn(req, res, () => resolve(true)); }); if(!proceed) return false; } return true; } }