flummpress/src/index.ts
2025-03-24 14:38:12 +01:00

162 lines
4.6 KiB
TypeScript

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<T>(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<T>(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<Record<string, string>> {
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;
}
}