import http, { IncomingMessage, ServerResponse } from "node:http"; import { URL } from "node:url"; import querystring from "node:querystring"; import Router from "./router"; import Tpl from "./template"; import { Request, Response, Middleware } from "./types"; export { Router, Tpl }; export default class Flummpress { private server?: http.Server; private errorHandler: Middleware; router: Router; tpl: Tpl; middleware: Middleware[]; constructor() { this.router = new Router(); this.tpl = new Tpl(); this.middleware = []; this.errorHandler = async (req: Request, res: Response) => { res.writeHead(500).end("500 - Internal Server Error"); }; } /** * 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 | Middleware): this { if(obj instanceof Router) this.router.use(obj); else if(obj instanceof Tpl) this.tpl = obj; else this.middleware.push(obj); return this; } setErrorHandler(handler: Middleware): this { this.errorHandler = handler; 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, response: ServerResponse) => { const req = request as Request; const res = this.createResponse(response); const start = process.hrtime(); try { this.parseRequest(req); for(const mw of this.middleware) { await mw(req, res, () => {}); } const route = this.router.getRoute(req.parsedUrl.pathname, req.method!); if(route) { const [pathPattern, methods] = route; const method = req.method?.toLowerCase(); const handler = methods[method!]; const middleware = methods[`${method}mw`]; for(const mw of middleware) { await mw(req, res, () => {}); } if(handler) { req.params = req.parsedUrl.pathname.match(new RegExp(pathPattern))?.groups || {}; req.post = await this.readBody(req); handler(req, res); } else { res.writeHead(405).end("405 - Method Not Allowed"); } } else { res.writeHead(404).end("404 - Not Found"); } } catch(err: any) { this.errorHandler(req, res, () => {}); } console.log([ `[${new Date().toISOString()}]`, `${(process.hrtime(start)[1] / 1e6).toFixed(2)}ms`, `${req.method} ${res.statusCode}`, req.parsedUrl.pathname, ].join(" | ")); }).listen(...args); return this; } private parseRequest(req: Request): void { 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(url.searchParams.entries()), }; req.cookies = {}; if(req.headers.cookie) { req.headers.cookie.split("; ").forEach(cookie => { const [key, value] = cookie.split("="); req.cookies[key] = decodeURIComponent(value); }); } } /** * 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. */ private async readBody(req: IncomingMessage): Promise> { return new Promise((resolve, reject) => { let body = ""; req.on("data", (chunk) => { body += chunk; }); req.on("end", () => { try { resolve( req.headers["content-type"] === "application/json" ? JSON.parse(body) : querystring.parse(body) ); } catch(err: any) { reject(err); } }); req.on("error", reject); }); } private createResponse(response: ServerResponse): Response { const res = response as Response; res.reply = ({ code = 200, type = "text/html", body }) => { res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` }); res.end(body); }; res.json = (body, code = 200) => { res.reply({ code, type: "application/json", body: JSON.stringify(body) }); }; res.html = (body, code = 200) => { res.reply({ code, type: "text/html", body }); }; res.redirect = (target, code = 302) => { res.writeHead(code, { Location: target }); res.end(); }; return res; } }