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