Erweitere die Flummpress-Klasse um Response- und Middleware-Typen, verbessere die Fehlerbehandlung und optimiere die Anfrageverarbeitung
This commit is contained in:
		
							
								
								
									
										261
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										261
									
								
								src/index.ts
									
									
									
									
									
								
							@@ -6,9 +6,9 @@ import querystring from "node:querystring";
 | 
			
		||||
import Router from "./router.js";
 | 
			
		||||
import Tpl from "./template.js";
 | 
			
		||||
 | 
			
		||||
export { Router, Tpl, Request };
 | 
			
		||||
export { Router, Tpl, Request, Response, Middleware };
 | 
			
		||||
 | 
			
		||||
type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
 | 
			
		||||
type Middleware = (req: Request, res: Response, next: () => void) => void;
 | 
			
		||||
 | 
			
		||||
interface Request extends IncomingMessage {
 | 
			
		||||
  parsedUrl: {
 | 
			
		||||
@@ -22,16 +22,27 @@ interface Request extends IncomingMessage {
 | 
			
		||||
  post?: Record<string, string>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface Response extends ServerResponse {
 | 
			
		||||
  reply: (options: { code?: number; type?: string; body: string }) => void;
 | 
			
		||||
  json: (body: string, code?: number) => void;
 | 
			
		||||
  html: (body: string, code?: number) => void;
 | 
			
		||||
  redirect: (target: string, code?: number) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class Flummpress {
 | 
			
		||||
  private server?: http.Server;
 | 
			
		||||
  private errorHandler: Middleware;
 | 
			
		||||
  router: Router;
 | 
			
		||||
  tpl: Tpl;
 | 
			
		||||
  middleware: Set<Middleware>;
 | 
			
		||||
  middleware: Middleware[];
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.router = new Router();
 | 
			
		||||
    this.tpl = new Tpl();
 | 
			
		||||
    this.middleware = new Set();
 | 
			
		||||
    this.middleware = [];
 | 
			
		||||
    this.errorHandler = async (req: Request, res: Response) => {
 | 
			
		||||
      res.writeHead(500).end("500 - Internal Server Error");
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -39,15 +50,18 @@ export default class Flummpress {
 | 
			
		||||
   * @param {any} obj - An instance of Router, Tpl, or a middleware function.
 | 
			
		||||
   * @returns {this} - The current instance for chaining.
 | 
			
		||||
   */
 | 
			
		||||
  use(obj: Router | Tpl): this {
 | 
			
		||||
  use(obj: Router | Tpl | Middleware): this {
 | 
			
		||||
    if(obj instanceof Router)
 | 
			
		||||
      this.router.use(obj);
 | 
			
		||||
    else if(obj instanceof Tpl)
 | 
			
		||||
      this.tpl = obj;
 | 
			
		||||
    else {
 | 
			
		||||
      if(!this.middleware.has(obj))
 | 
			
		||||
        this.middleware.add(obj);
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
      this.middleware.push(obj);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setErrorHandler(handler: Middleware): this {
 | 
			
		||||
    this.errorHandler = handler;
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -57,169 +71,132 @@ export default class Flummpress {
 | 
			
		||||
   * @returns {this} - The current instance for chaining.
 | 
			
		||||
   */
 | 
			
		||||
  listen(...args: any[]): this {
 | 
			
		||||
    this.server = http.createServer(async (request: IncomingMessage, res: ServerResponse) => {
 | 
			
		||||
      const req = request as Request;
 | 
			
		||||
      const t_start = process.hrtime();
 | 
			
		||||
    this.server = http
 | 
			
		||||
      .createServer(async (request: IncomingMessage, response: ServerResponse) => {
 | 
			
		||||
        const req = request as Request;
 | 
			
		||||
        const res = this.createResponse(req, response);
 | 
			
		||||
        const start = process.hrtime();
 | 
			
		||||
 | 
			
		||||
      // URL and query parsing
 | 
			
		||||
      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(Object.entries(querystring.parse(_url.search.substring(1))).filter(([_, v]) => typeof v === 'string')) as Record<string, string>,
 | 
			
		||||
      };
 | 
			
		||||
        try {
 | 
			
		||||
          this.parseRequest(req);
 | 
			
		||||
 | 
			
		||||
      // extract cookies
 | 
			
		||||
      req['cookies'] = {};
 | 
			
		||||
      if(req.headers.cookie) {
 | 
			
		||||
        req.headers.cookie.split("; ").forEach((c: string) => {
 | 
			
		||||
          const parts = c.split('=');
 | 
			
		||||
          req.cookies[parts.shift()?.trim() ?? ""] = decodeURI(parts.join('='));
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
          for(const mw of this.middleware) {
 | 
			
		||||
            await mw(req, res, () => {});
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
      // run middleware
 | 
			
		||||
      await Promise.all([...this.middleware].map(m => m(req, res, () => {})));
 | 
			
		||||
          const route = this.router.getRoute(req.parsedUrl.pathname, req.method!);
 | 
			
		||||
 | 
			
		||||
      // route and method handling
 | 
			
		||||
      const method = req.method === 'HEAD' ? 'get' : req.method?.toLowerCase();
 | 
			
		||||
      const route = this.router.getRoute(req.parsedUrl.pathname, req.method === 'HEAD' ? 'GET' : req.method!);
 | 
			
		||||
          if(route) {
 | 
			
		||||
            const [pathPattern, methods] = route;
 | 
			
		||||
            const method = req.method?.toLowerCase();
 | 
			
		||||
            const handler = methods[method!];
 | 
			
		||||
            const middleware = methods[`${method}mw`];
 | 
			
		||||
 | 
			
		||||
      if(route) {
 | 
			
		||||
        // route found
 | 
			
		||||
        const cb = route[1][method!];
 | 
			
		||||
        const middleware = route[1][`${method}mw`];
 | 
			
		||||
            if(middleware) {
 | 
			
		||||
              for(const mw of middleware) {
 | 
			
		||||
                await mw(req, res, () => {});
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        req['params'] = req.parsedUrl.pathname.match(new RegExp(route[0]))?.groups;
 | 
			
		||||
        req['post'] = await this.readBody(req);
 | 
			
		||||
            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, () => {});
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const result = await this.processMiddleware(middleware, req, this.createResponse(req, res));
 | 
			
		||||
        if(result)
 | 
			
		||||
          cb(req, res);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        // route not found
 | 
			
		||||
        res.writeHead(404).end(`404 - ${req.method} ${req.parsedUrl.pathname}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log([
 | 
			
		||||
        `[${new Date().toLocaleTimeString()}]`,
 | 
			
		||||
        `${(process.hrtime(t_start)[1] / 1e6).toFixed(2)}ms`,
 | 
			
		||||
        `${req.method} ${res.statusCode}`,
 | 
			
		||||
        req.parsedUrl.pathname
 | 
			
		||||
      ].map(e => e.toString().padEnd(15)).join(""));
 | 
			
		||||
    }).listen(...args);
 | 
			
		||||
        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.
 | 
			
		||||
   */
 | 
			
		||||
  readBody(req: IncomingMessage): Promise<Record<string, string>> {
 | 
			
		||||
  private async readBody(req: IncomingMessage): Promise<Record<string, string>> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      let data = "";
 | 
			
		||||
      let body = "";
 | 
			
		||||
 | 
			
		||||
      req.on("data", (chunk: string) => data += chunk);
 | 
			
		||||
      req.on("data", (chunk) => {
 | 
			
		||||
        body += chunk;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      req.on("end", () => {
 | 
			
		||||
        if(req.headers["content-type"] === "application/json") {
 | 
			
		||||
          try {
 | 
			
		||||
            resolve(JSON.parse(data) as Record<string, string>);
 | 
			
		||||
          }
 | 
			
		||||
          catch(err: any) {
 | 
			
		||||
            reject(err);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          const parsedData = querystring.parse(data);
 | 
			
		||||
          const decodedData = Object.fromEntries(
 | 
			
		||||
            Object.entries(parsedData).map(([key, value]) => {
 | 
			
		||||
              try {
 | 
			
		||||
                return [key, decodeURIComponent(value as string)];
 | 
			
		||||
              }
 | 
			
		||||
              catch(err: any) {
 | 
			
		||||
                return [key, value];
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
        try {
 | 
			
		||||
          resolve(
 | 
			
		||||
            req.headers["content-type"] === "application/json"
 | 
			
		||||
              ? JSON.parse(body)
 | 
			
		||||
              : querystring.parse(body)
 | 
			
		||||
          );
 | 
			
		||||
          resolve(decodedData as Record<string, string>);
 | 
			
		||||
        }
 | 
			
		||||
        catch(err: any) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      req.on("error", (err: Error) => reject(err));
 | 
			
		||||
      req.on("error", reject);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Enhances the HTTP response object with additional methods for convenience.
 | 
			
		||||
   * @param {Request} req - The HTTP request object.
 | 
			
		||||
   * @param {ServerResponse} res - The HTTP response object.
 | 
			
		||||
   * @returns {ServerResponse & {
 | 
			
		||||
  *  reply: Function, json: Function, html: Function, redirect: Function
 | 
			
		||||
  * }} - The enhanced response object.
 | 
			
		||||
  */
 | 
			
		||||
  createResponse(req: Request, res: ServerResponse): ServerResponse & {
 | 
			
		||||
    reply: (options: { code?: number; type?: string; body: string }) => ServerResponse;
 | 
			
		||||
    json: (body: string, code?: number) => ServerResponse;
 | 
			
		||||
    html: (body: string, code?: number) => ServerResponse;
 | 
			
		||||
    redirect: (target: string, code?: number) => void;
 | 
			
		||||
  } {
 | 
			
		||||
    const enhancedRes = Object.assign(res, {
 | 
			
		||||
      reply: ({ code = 200, type = "text/html", body }: { code?: number; type?: string; body: string }) => {
 | 
			
		||||
        res.writeHead(code, {
 | 
			
		||||
          "content-type": `${type}; charset=UTF-8`,
 | 
			
		||||
          "content-length": Buffer.byteLength(body, "utf-8"),
 | 
			
		||||
        });
 | 
			
		||||
        if(req.method === "HEAD")
 | 
			
		||||
          body = null as unknown as string;
 | 
			
		||||
        res.end(body);
 | 
			
		||||
        return res;
 | 
			
		||||
      },
 | 
			
		||||
      json: (body: string, code = 200) => {
 | 
			
		||||
        if(typeof body === "object")
 | 
			
		||||
          body = JSON.stringify(body);
 | 
			
		||||
        return enhancedRes.reply({ code, body, type: "application/json" });
 | 
			
		||||
      },
 | 
			
		||||
      html: (body: string, code = 200) => enhancedRes.reply({ code, body, type: "text/html" }),
 | 
			
		||||
      redirect: (target: string, code = 307) => {
 | 
			
		||||
        res.writeHead(code, {
 | 
			
		||||
          "Cache-Control": "no-cache, public",
 | 
			
		||||
          "Location": target,
 | 
			
		||||
        });
 | 
			
		||||
        res.end();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  private createResponse(
 | 
			
		||||
    req: Request,
 | 
			
		||||
    response: ServerResponse
 | 
			
		||||
  ): Response {
 | 
			
		||||
    const res = response as Response;
 | 
			
		||||
 | 
			
		||||
    return enhancedRes;
 | 
			
		||||
  }
 | 
			
		||||
    res.reply = ({ code = 200, type = "text/html", body }) => {
 | 
			
		||||
      res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` });
 | 
			
		||||
      res.end(body);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Processes middleware functions for an incoming request.
 | 
			
		||||
   * @param {Middleware} middleware - The middleware function to process.
 | 
			
		||||
   * @param {IncomingMessage} req - The HTTP request object.
 | 
			
		||||
   * @param {ServerResponse} res - The HTTP response object.
 | 
			
		||||
   * @returns {Promise<boolean>} - Resolves to true if middleware processing is successful.
 | 
			
		||||
   */
 | 
			
		||||
  private async processMiddleware(
 | 
			
		||||
    middleware: Middleware[],
 | 
			
		||||
    req: IncomingMessage,
 | 
			
		||||
    res: ServerResponse
 | 
			
		||||
  ): Promise<boolean> {
 | 
			
		||||
    if(!middleware || middleware.length === 0)
 | 
			
		||||
      return true;
 | 
			
		||||
    res.json = (body, code = 200) => {
 | 
			
		||||
      res.reply({ code, type: "application/json", body: JSON.stringify(body) });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    for(const fn of middleware) {
 | 
			
		||||
      const proceed = await new Promise<boolean>(resolve => {
 | 
			
		||||
        fn(req, res, () => resolve(true));
 | 
			
		||||
      });
 | 
			
		||||
    res.html = (body, code = 200) => {
 | 
			
		||||
      res.reply({ code, type: "text/html", body });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
      if(!proceed)
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
    res.redirect = (target, code = 302) => {
 | 
			
		||||
      res.writeHead(code, { Location: target });
 | 
			
		||||
      res.end();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return res;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user