Dateien nach "src" hochladen
This commit is contained in:
		
							
								
								
									
										225
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,225 @@
 | 
				
			|||||||
 | 
					import http, { IncomingMessage, ServerResponse } from "http";
 | 
				
			||||||
 | 
					import { URL } from "url";
 | 
				
			||||||
 | 
					import { Buffer } from "buffer";
 | 
				
			||||||
 | 
					import querystring from "querystring";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Router from "./router.js";
 | 
				
			||||||
 | 
					import Tpl from "./template.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Router, Tpl, Request };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Request extends IncomingMessage {
 | 
				
			||||||
 | 
					  parsedUrl: {
 | 
				
			||||||
 | 
					    pathname: string;
 | 
				
			||||||
 | 
					    split: string[];
 | 
				
			||||||
 | 
					    searchParams: URLSearchParams;
 | 
				
			||||||
 | 
					    qs: Record<string, string>;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  cookies: Record<string, string>;
 | 
				
			||||||
 | 
					  params?: Record<string, string>;
 | 
				
			||||||
 | 
					  post?: Record<string, string>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class Flummpress {
 | 
				
			||||||
 | 
					  private server?: http.Server;
 | 
				
			||||||
 | 
					  router: Router;
 | 
				
			||||||
 | 
					  tpl: Tpl;
 | 
				
			||||||
 | 
					  middleware: Set<Middleware>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    this.router = new Router();
 | 
				
			||||||
 | 
					    this.tpl = new Tpl();
 | 
				
			||||||
 | 
					    this.middleware = new Set();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 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): 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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    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, res: ServerResponse) => {
 | 
				
			||||||
 | 
					      const req = request as Request;
 | 
				
			||||||
 | 
					      const t_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>,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 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('='));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // run middleware
 | 
				
			||||||
 | 
					      await Promise.all([...this.middleware].map(m => m(req, res, () => {})));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 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) {
 | 
				
			||||||
 | 
					        // route found
 | 
				
			||||||
 | 
					        const cb = route[1][method!];
 | 
				
			||||||
 | 
					        const middleware = route[1][`${method}mw`];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        req['params'] = req.parsedUrl.pathname.match(new RegExp(route[0]))?.groups;
 | 
				
			||||||
 | 
					        req['post'] = await this.readBody(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 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>> {
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					      let data = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      req.on("data", (chunk: string) => data += 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];
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          resolve(decodedData as Record<string, string>);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      req.on("error", (err: Error) => reject(err));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 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();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return enhancedRes;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for(const fn of middleware) {
 | 
				
			||||||
 | 
					      const proceed = await new Promise<boolean>(resolve => {
 | 
				
			||||||
 | 
					        fn(req, res, () => resolve(true));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if(!proceed)
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										272
									
								
								src/router.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								src/router.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,272 @@
 | 
				
			|||||||
 | 
					import fs from "fs";
 | 
				
			||||||
 | 
					import path from "path";
 | 
				
			||||||
 | 
					import Tpl from "./template.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Request } from "./index.js";
 | 
				
			||||||
 | 
					import { ServerResponse } from "http";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RouteCallback = (req: Request, res: ServerResponse) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class Router {
 | 
				
			||||||
 | 
					  private routes: Map<RegExp | string, { [key: string]: RouteCallback | RouteCallback[] }>;
 | 
				
			||||||
 | 
					  private tpl?: Tpl;
 | 
				
			||||||
 | 
					  private mimes: Map<string, string>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    this.routes = new Map();
 | 
				
			||||||
 | 
					    this.mimes = new Map();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Dynamically imports routes from a directory and registers them.
 | 
				
			||||||
 | 
					   * @param p - Path to the directory containing route files.
 | 
				
			||||||
 | 
					   * @param tpl - Optional template instance to use with the routes.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async importRoutesFromPath(p: string, tpl: Tpl | false = false): Promise<this> {
 | 
				
			||||||
 | 
					    const dirEntries = await fs.promises.readdir(path.resolve() + '/' + p, { withFileTypes: true });
 | 
				
			||||||
 | 
					    for(const tmp of dirEntries) {
 | 
				
			||||||
 | 
					      if(tmp.isFile() && (tmp.name.endsWith(".mjs") || tmp.name.endsWith(".js"))) {
 | 
				
			||||||
 | 
					        const routeModule = (await import(`${path.resolve()}/${p}/${tmp.name}`)).default;
 | 
				
			||||||
 | 
					        this.use(routeModule(this, tpl));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      else if(tmp.isDirectory()) {
 | 
				
			||||||
 | 
					        await this.importRoutesFromPath(p + '/' + tmp.name);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Groups multiple routes under a shared base path.
 | 
				
			||||||
 | 
					   * @param path - Base path for the route group.
 | 
				
			||||||
 | 
					   * @param cb - Callback to define the grouped routes.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  group(path: string, cb: (methods: any) => void): this {
 | 
				
			||||||
 | 
					    const methods: { [key: string]: (path: string | RegExp, cb: RouteCallback, middleware?: RouteCallback) => Router } = {
 | 
				
			||||||
 | 
					      get: this.get.bind(this),
 | 
				
			||||||
 | 
					      post: this.post.bind(this),
 | 
				
			||||||
 | 
					      head: this.head.bind(this),
 | 
				
			||||||
 | 
					      put: this.put.bind(this),
 | 
				
			||||||
 | 
					      delete: this.delete.bind(this),
 | 
				
			||||||
 | 
					      patch: this.patch.bind(this),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const target = { path: new RegExp(path) };
 | 
				
			||||||
 | 
					    const handler = {
 | 
				
			||||||
 | 
					      get: (opt: any, method: string) => (p: string, ...args: any[]) =>
 | 
				
			||||||
 | 
					        methods[method](
 | 
				
			||||||
 | 
					          new RegExp(
 | 
				
			||||||
 | 
					            [opt.path, new RegExp(p === "/" ? "$" : p)]
 | 
				
			||||||
 | 
					              .map((regex) => regex.source)
 | 
				
			||||||
 | 
					              .join("")
 | 
				
			||||||
 | 
					              .replace(/(\\\/){1,}/g, "/")
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          ...(args as [RouteCallback, RouteCallback?])
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    cb(new Proxy(target, handler));
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Merges routes or assigns a template instance to the Router.
 | 
				
			||||||
 | 
					   * @param obj - An instance of Router or Tpl.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  use(obj: Router | Tpl): void {
 | 
				
			||||||
 | 
					    if(obj instanceof Router) {
 | 
				
			||||||
 | 
					      this.routes = new Map([...this.routes, ...obj.routes]);
 | 
				
			||||||
 | 
					      this.sortRoutes();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if(obj instanceof Tpl) {
 | 
				
			||||||
 | 
					      this.tpl = obj;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Registers a GET route.
 | 
				
			||||||
 | 
					   * @param path - Route path or RegExp.
 | 
				
			||||||
 | 
					   * @param cb - Callback to handle the route.
 | 
				
			||||||
 | 
					   * @param middleware - Optional middleware function.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  get(path: string | RegExp, cb: RouteCallback, middleware?: RouteCallback): this {
 | 
				
			||||||
 | 
					    this.registerRoute(path, cb, "get", middleware);
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Registers a POST route.
 | 
				
			||||||
 | 
					   * @param path - Route path or RegExp.
 | 
				
			||||||
 | 
					   * @param cb - Callback to handle the route.
 | 
				
			||||||
 | 
					   * @param middleware - Optional middleware function.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  post(path: string | RegExp, cb: RouteCallback, middleware?: RouteCallback): this {
 | 
				
			||||||
 | 
					    this.registerRoute(path, cb, "post", middleware);
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Registers a HEAD route.
 | 
				
			||||||
 | 
					   * @param path - Route path or RegExp.
 | 
				
			||||||
 | 
					   * @param cb - Callback to handle the route.
 | 
				
			||||||
 | 
					   * @param middleware - Optional middleware function.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  head(path: string | RegExp, cb: RouteCallback, middleware?: RouteCallback): this {
 | 
				
			||||||
 | 
					    this.registerRoute(path, cb, "head", middleware);
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Registers a PUT route.
 | 
				
			||||||
 | 
					   * @param path - Route path or RegExp.
 | 
				
			||||||
 | 
					   * @param cb - Callback to handle the route.
 | 
				
			||||||
 | 
					   * @param middleware - Optional middleware function.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  put(path: string | RegExp, cb: RouteCallback, middleware?: RouteCallback): this {
 | 
				
			||||||
 | 
					    this.registerRoute(path, cb, "put", middleware);
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Registers a DELETE route.
 | 
				
			||||||
 | 
					   * @param path - Route path or RegExp.
 | 
				
			||||||
 | 
					   * @param cb - Callback to handle the route.
 | 
				
			||||||
 | 
					   * @param middleware - Optional middleware function.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  delete(path: string | RegExp, cb: RouteCallback, middleware?: RouteCallback): this {
 | 
				
			||||||
 | 
					    this.registerRoute(path, cb, "delete", middleware);
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Registers a PATCH route.
 | 
				
			||||||
 | 
					   * @param path - Route path or RegExp.
 | 
				
			||||||
 | 
					   * @param cb - Callback to handle the route.
 | 
				
			||||||
 | 
					   * @param middleware - Optional middleware function.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  patch(path: string | RegExp, cb: RouteCallback, middleware?: RouteCallback): this {
 | 
				
			||||||
 | 
					    this.registerRoute(path, cb, "patch", middleware);
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Registers a new route with an optional middleware.
 | 
				
			||||||
 | 
					   * @param path - The route path or RegExp.
 | 
				
			||||||
 | 
					   * @param cb - Callback to handle the route.
 | 
				
			||||||
 | 
					   * @param method - The HTTP method (e.g., "get", "post").
 | 
				
			||||||
 | 
					   * @param middleware - Optional middleware function.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private registerRoute(
 | 
				
			||||||
 | 
					    path: string | RegExp,
 | 
				
			||||||
 | 
					    cb: RouteCallback,
 | 
				
			||||||
 | 
					    method: string,
 | 
				
			||||||
 | 
					    middleware: RouteCallback | RouteCallback[] = []
 | 
				
			||||||
 | 
					  ): this {
 | 
				
			||||||
 | 
					    const middlewareArray = Array.isArray(middleware) ? middleware : [middleware];
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    if(!this.routes.has(path)) {
 | 
				
			||||||
 | 
					      this.routes.set(path, {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    this.routes.set(path, {
 | 
				
			||||||
 | 
					      ...this.routes.get(path),
 | 
				
			||||||
 | 
					      [method]: cb,
 | 
				
			||||||
 | 
					      [`${method}mw`]: middlewareArray,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    console.log("route set:", method.toUpperCase(), path);
 | 
				
			||||||
 | 
					    this.sortRoutes();
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Finds and returns the route matching the given path and method.
 | 
				
			||||||
 | 
					   * @param path - The requested path.
 | 
				
			||||||
 | 
					   * @param method - The HTTP method (e.g., "GET").
 | 
				
			||||||
 | 
					   * @returns The matching route or undefined.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getRoute(path: string, method: string): any {
 | 
				
			||||||
 | 
					    method = method.toLowerCase();
 | 
				
			||||||
 | 
					    return [...this.routes.entries()].find(r => {
 | 
				
			||||||
 | 
					      return (typeof r[0] === "string" ? r[0] === path : r[0].exec?.(path)) && r[1][method];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Sorts the routes by their keys in reverse order.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private sortRoutes(): this {
 | 
				
			||||||
 | 
					    this.routes = new Map([...this.routes.entries()].sort().reverse());
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Reads MIME types from a file and stores them in a map.
 | 
				
			||||||
 | 
					   * @param file - Path to the MIME types file.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private readMimes(file: string = "/etc/mime.types"): void {
 | 
				
			||||||
 | 
					    fs.readFileSync(file, "utf-8")
 | 
				
			||||||
 | 
					      .split("\n")
 | 
				
			||||||
 | 
					      .filter(line => !line.startsWith("#") && line)
 | 
				
			||||||
 | 
					      .forEach(line => {
 | 
				
			||||||
 | 
					        const [mimeType, extensions] = line.split(/\s+/);
 | 
				
			||||||
 | 
					        extensions?.split(" ").forEach(ext => this.mimes.set(ext, mimeType));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Serves static files from a specified directory.
 | 
				
			||||||
 | 
					   * @param options - Options for serving static files.
 | 
				
			||||||
 | 
					   * @param options.dir - Directory containing the static files.
 | 
				
			||||||
 | 
					   * @param options.route - Regular expression to match the route for static files.
 | 
				
			||||||
 | 
					   * @returns The Router instance for chaining.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static({ dir = path.resolve() + "/public", route = /^\/public/ }: { dir?: string; route?: RegExp }): this {
 | 
				
			||||||
 | 
					    if(!this.mimes.size)
 | 
				
			||||||
 | 
					      this.readMimes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.get(route, (req: any, res: any) => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const filename = req.url.pathname.replace(route, "") || "index.html";
 | 
				
			||||||
 | 
					        const mime = this.mimes.get(filename.split(".").pop() || "");
 | 
				
			||||||
 | 
					        const file = path.join(dir, filename);
 | 
				
			||||||
 | 
					        const stat = fs.statSync(file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if(req.headers.range) {
 | 
				
			||||||
 | 
					          const [startStr, endStr] = req.headers.range.replace(/bytes=/, "").split("-");
 | 
				
			||||||
 | 
					          const start = parseInt(startStr, 10);
 | 
				
			||||||
 | 
					          const end = endStr ? parseInt(endStr, 10) : stat.size - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          res.writeHead(206, {
 | 
				
			||||||
 | 
					            "Content-Range": `bytes ${start}-${end}/${stat.size}`,
 | 
				
			||||||
 | 
					            "Accept-Ranges": "bytes",
 | 
				
			||||||
 | 
					            "Content-Length": end - start + 1,
 | 
				
			||||||
 | 
					            "Content-Type": mime,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          fs.createReadStream(file, { start, end }).pipe(res);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					          res.writeHead(200, {
 | 
				
			||||||
 | 
					            "Content-Length": stat.size,
 | 
				
			||||||
 | 
					            "Content-Type": mime,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          fs.createReadStream(file).pipe(res);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      catch(err: any) {
 | 
				
			||||||
 | 
					        console.error(err);
 | 
				
			||||||
 | 
					        res.reply({ code: 404, body: "404 - File not found" });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										189
									
								
								src/template.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/template.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
				
			|||||||
 | 
					import fs from "fs";
 | 
				
			||||||
 | 
					import path from "path";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class Template {
 | 
				
			||||||
 | 
					  private views: string;
 | 
				
			||||||
 | 
					  private globals: Record<string, any>;
 | 
				
			||||||
 | 
					  private cache: boolean;
 | 
				
			||||||
 | 
					  private templates: Map<string, { code: string; cached: Date }>;
 | 
				
			||||||
 | 
					  private debug: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    this.views = "./views";
 | 
				
			||||||
 | 
					    this.globals = {};
 | 
				
			||||||
 | 
					    this.debug = false;
 | 
				
			||||||
 | 
					    this.cache = true;
 | 
				
			||||||
 | 
					    this.templates = new Map();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Enables or disables debug mode.
 | 
				
			||||||
 | 
					   * @param debug - If true, enables debug mode.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  setDebug(debug: boolean): void {
 | 
				
			||||||
 | 
					    this.debug = debug;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Sets the directory for template files and preloads all templates.
 | 
				
			||||||
 | 
					   * @param views - The directory path for template files.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  setViews(views: string): void {
 | 
				
			||||||
 | 
					    this.views = views;
 | 
				
			||||||
 | 
					    this.readdir(views);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Sets global variables to be used in all templates.
 | 
				
			||||||
 | 
					   * @param globals - An object containing global variables.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  setGlobals(globals: Record<string, any>): void {
 | 
				
			||||||
 | 
					    this.globals = globals;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Enables or disables the template caching mechanism.
 | 
				
			||||||
 | 
					   * @param cache - If true, enables caching.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  setCache(cache: boolean): void {
 | 
				
			||||||
 | 
					    this.cache = cache;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Recursively reads the specified directory and loads all templates into memory.
 | 
				
			||||||
 | 
					   * @param dir - The directory to read.
 | 
				
			||||||
 | 
					   * @param root - The root directory for relative paths.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private readdir(dir: string, root: string = dir): void {
 | 
				
			||||||
 | 
					    for(const d of fs.readdirSync(`${path.resolve()}/${dir}`)) {
 | 
				
			||||||
 | 
					      if(d.endsWith(".html")) {
 | 
				
			||||||
 | 
					        const file = path.parse(`${dir.replace(this.views, "")}/${d}`);
 | 
				
			||||||
 | 
					        const t_dir = file.dir.substring(1);
 | 
				
			||||||
 | 
					        const t_file = file.name;
 | 
				
			||||||
 | 
					        this.getTemplate(!t_dir ? t_file : `${t_dir}/${t_file}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      else {
 | 
				
			||||||
 | 
					        this.readdir(`${dir}/${d}`, root);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Retrieves a template from the cache or loads it from the file system if not cached.
 | 
				
			||||||
 | 
					   * @param tpl - The name of the template.
 | 
				
			||||||
 | 
					   * @returns The template code as a string.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private getTemplate(tpl: string): string {
 | 
				
			||||||
 | 
					    let template: { code: string; cached: Date };
 | 
				
			||||||
 | 
					    let cache = false;
 | 
				
			||||||
 | 
					    if(this.cache && this.templates.has(tpl)) {
 | 
				
			||||||
 | 
					      template = this.templates.get(tpl)!;
 | 
				
			||||||
 | 
					      cache = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					      template = {
 | 
				
			||||||
 | 
					        code: fs.readFileSync(`${this.views}/${tpl}.html`, "utf-8"),
 | 
				
			||||||
 | 
					        cached: new Date(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      this.templates.set(tpl, template);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      template.code,
 | 
				
			||||||
 | 
					      this.debug ? `<!-- ${tpl}.html ${cache ? `cached ${template.cached}` : "not cached"} -->` : "",
 | 
				
			||||||
 | 
					    ].join("");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Renders a template with the provided data and local variables.
 | 
				
			||||||
 | 
					   * @param file - The name of the template file (without extension).
 | 
				
			||||||
 | 
					   * @param data - Data object to inject into the template.
 | 
				
			||||||
 | 
					   * @param locals - Local variables to be used within the template.
 | 
				
			||||||
 | 
					   * @returns The rendered HTML string.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  render(file: string, data: Record<string, any> = {}, locals: Record<string, any> = {}): string {
 | 
				
			||||||
 | 
					    data = { ...data, ...locals, ...this.globals };
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const code =
 | 
				
			||||||
 | 
					        'with(_data){const __html = [];' +
 | 
				
			||||||
 | 
					        '__html.push(`' +
 | 
				
			||||||
 | 
					        this.getTemplate(file)
 | 
				
			||||||
 | 
					          .replace(/[\t]/g, " ")
 | 
				
			||||||
 | 
					          .split("`")
 | 
				
			||||||
 | 
					          .join("\\`")
 | 
				
			||||||
 | 
					          .replace(/{{--(.*?)--}}/g, "")
 | 
				
			||||||
 | 
					          .replace(/{{(.+?)}}/g, "`, $1, `")
 | 
				
			||||||
 | 
					          .replace(/{!!(.+?)!!}/g, "`, this.escape($1), `")
 | 
				
			||||||
 | 
					          .replace(/@js/g, "`);")
 | 
				
			||||||
 | 
					          .replace(/@endjs/g, ";__html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@mtime\((.*?)\)/g, "`);__html.push(this.getMtime('$1'));__html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@include\((.*?)\)/g, (_, inc) => this.render(inc, data))
 | 
				
			||||||
 | 
					          .replace(/@for\((.*?)\)$/gm, "`); for($1) { __html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@endfor/g, "`); } __html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@each\((.*?) as (.*?)\)/g, "`); this.forEach($1, ($2, key) => { __html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@endeach/g, "`); }); __html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@elseif\((.*?)\)(\)?)/g, "`); } else if ($1$2) { __html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@if\((.*?)\)(\)?)/g, "`); if ($1$2) { __html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@else/g, "`); } else { __html.push(`")
 | 
				
			||||||
 | 
					          .replace(/@endif/g, "`); } __html.push(`") +
 | 
				
			||||||
 | 
					        "`); return __html.join('').replace(/\\n\\s*\\n/g, '\\n'); }";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return new Function("_data", code).bind({
 | 
				
			||||||
 | 
					        escape: this.escape,
 | 
				
			||||||
 | 
					        forEach: this.forEach,
 | 
				
			||||||
 | 
					        getMtime: this.getMtime,
 | 
				
			||||||
 | 
					      })(data);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    catch(err: any) {
 | 
				
			||||||
 | 
					      console.log(file, (err as Error).message);
 | 
				
			||||||
 | 
					      return this.debug ? `${(err as Error).message} in ${file}` : "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Escapes a string for safe usage in HTML.
 | 
				
			||||||
 | 
					   * @param str - The string to escape.
 | 
				
			||||||
 | 
					   * @returns The escaped string.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  escape(str: string): string {
 | 
				
			||||||
 | 
					    return (str + "")
 | 
				
			||||||
 | 
					      .replace(/&/g, "&")
 | 
				
			||||||
 | 
					      .replace(/</g, "<")
 | 
				
			||||||
 | 
					      .replace(/>/g, ">")
 | 
				
			||||||
 | 
					      .replace(/"/g, """)
 | 
				
			||||||
 | 
					      .replace(/'/g, "'")
 | 
				
			||||||
 | 
					      .replace(/{/g, "{")
 | 
				
			||||||
 | 
					      .replace(/}/g, "}");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Iterates over an object or array and applies a callback function.
 | 
				
			||||||
 | 
					   * @param o - The object or array to iterate over.
 | 
				
			||||||
 | 
					   * @param f - The callback function.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  forEach(o: any, f: (value: any, key: string | number) => void): void {
 | 
				
			||||||
 | 
					    if(Array.isArray(o)) {
 | 
				
			||||||
 | 
					      o.forEach(f);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else if(typeof o === "object") {
 | 
				
			||||||
 | 
					      Object.keys(o).forEach((k) => f.call(null, o[k], k));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					      throw new Error(`${o} is not an iterable object`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Retrieves the last modification time of a file.
 | 
				
			||||||
 | 
					   * @param file - The file path to check.
 | 
				
			||||||
 | 
					   * @returns The last modification time in milliseconds.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getMtime(file: string): number {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      return +`${fs.statSync(path.normalize(process.cwd() + file)).mtimeMs}`.split(".")[0];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    catch(err: any) {
 | 
				
			||||||
 | 
					      console.log(err);
 | 
				
			||||||
 | 
					      return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user