flummpress/src/index.ts

178 lines
4.9 KiB
TypeScript

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<any>} - A promise that resolves to the parsed body data.
*/
private async readBody(req: IncomingMessage): Promise<Record<string, string>> {
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;
}
}