import http, { IncomingMessage, ServerResponse } from "node:http"; import { URL } from "node:url"; import querystring from "node:querystring"; import Router, { Request, Response, Handler } from "./router.js"; import Container from "./container.js"; import Tpl from "./template.js"; export { Router, Tpl, Request, Response, Handler }; export default class Flummpress { private server?: http.Server; private container: Container; private middleware: Handler[]; public router: Router; constructor() { this.container = new Container(); this.router = new Router(); this.middleware = []; } public use(plugin: string | Router | Handler, factory?: () => T): this { if(typeof plugin === "string" && factory) this.container.register(plugin, factory); else if(plugin instanceof Router) this.router.use(plugin); else if(typeof plugin === "function") this.middleware.push(plugin); else throw new TypeError("Invalid arguments provided to use()"); return this; } public resolve(name: string): T { return this.container.resolve(name); } 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; } } public 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(" | ")); }) this.server.listen(...args); return this; } 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; } 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); }); } 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.status = (code = 200) => { return res.writeHead(code); }; res.json = (body: any, 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: encodeURI(target) }); res.end(); }; return res; } }