import http, { IncomingMessage, ServerResponse } from "node:http"; import { URL } from "node:url"; import querystring from "node:querystring"; import Router from "./router.js"; import Tpl from "./template.js"; import { Request, Response, Handler } from "./types.js"; export { Router, Tpl }; export default class Flummpress { private server?: http.Server; router: Router; tpl: Tpl; middleware: Handler[]; constructor() { this.router = new Router(); this.tpl = new Tpl(); this.middleware = []; } /** * Adds a plugin to the application, which can be a Router instance, Tpl instance, * or a middleware handler function. The method determines the type of the plugin * and performs the appropriate action. * * - If the plugin is an instance of `Router`, it is added to the application's router. * - If the plugin is an instance of `Tpl`, it sets the application's template engine. * - If the plugin is a middleware function, it is added to the middleware stack. * * @param {Router | Tpl | Handler} plugin - The plugin to add, which can be a `Router` instance, * a `Tpl` instance, or a middleware handler function. * @returns {this} The current instance for method chaining. */ use(plugin: Router | Tpl | Handler): this { if(plugin instanceof Router) this.router.use(plugin); else if(plugin instanceof Tpl) this.tpl = plugin; else if(typeof plugin === "function") this.middleware.push(plugin); return this; } /** * Processes a series of handlers in a pipeline by invoking each handler asynchronously * in sequence with the provided request and response objects. * * Each handler is responsible for calling the `next` function to signal that * the pipeline should continue to the next handler. If `next` is not called * by a handler or the response is ended, the pipeline execution stops. * * @private * @param {Handler[]} handlers - An array of handler functions to process. Each function * should have the signature `(req: Request, res: Response, next: Function) => Promise`. * @param {Request} req - The HTTP request object. * @param {Response} res - The HTTP response object. * @throws {TypeError} If any handler in the array is not a function. * @returns {Promise} Resolves when all handlers have been executed or the pipeline is terminated early. */ private async processPipeline(handlers: Handler[], req: Request, res: Response) { for(const handler of handlers) { if(typeof handler !== "function") throw new TypeError(`Handler is not a function: ${handler}`); let nextCalled = false; await handler(req, res, () => nextCalled = true); if(!nextCalled || res.writableEnded) return; } } /** * 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 = this.parseRequest(request); const res: Response = this.createResponse(response); const start = process.hrtime(); try { await this.processPipeline(this.middleware, req, res); const route = this.router.getRoute(req.url.pathname, req.method!); if(route) { const handler = route.methods[req.method?.toLowerCase()!]; req.params = req.url.pathname.match(new RegExp(route.path))?.groups || {}; req.post = await this.readBody(req); await this.processPipeline(handler, req, res); } else { res.writeHead(404).end("404 - Not Found"); } } catch(err: any) { console.error(err); res.writeHead(500).end("500 - Internal Server Error"); } console.log([ `[${new Date().toISOString()}]`, `${(process.hrtime(start)[1] / 1e6).toFixed(2)}ms`, `${req.method} ${res.statusCode}`, req.url.pathname, ].join(" | ")); }).listen(...args); return this; } /** * Parses an incoming HTTP request and converts it into a custom Request object. * * This method extracts information from the incoming request, such as the URL, * query string parameters, and cookies, and structures them in the returned Request object. * Any malformed or extra trailing slashes in the URL are sanitized. * * @private * @param {IncomingMessage} request - The incoming HTTP request to parse. * @returns {Request} A structured Request object with parsed properties such as `url` and `cookies`. */ private parseRequest(request: IncomingMessage): Request { const url = new URL(request.url!.replace(/(?!^.)(\/+)?$/, ""), "http://localhost"); const req = request as unknown as Request; req.url = { 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); }); } return req; } /** * Reads and parses the body of an incoming HTTP request. * @param {Request} req - The HTTP request object. * @returns {Promise>} - A promise that resolves to the parsed body data. */ private async readBody(req: Request): Promise> { return new Promise((resolve, reject) => { let body: string = ""; req.on("data", (chunk: string) => { 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); }); } /** * Creates a custom Response object with additional utility methods. * @param {ServerResponse} response - The original HTTP response object. * @returns {Response} - A structured Response object with utility methods such as `json` and `html`. */ private createResponse(response: ServerResponse): Response { const res: Response = 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; } }