UNPKG

@ah_naf/noderoute

Version:

A lightweight, flexible HTTP server framework for Node.js.

277 lines (238 loc) 8.06 kB
const fs = require("fs"); const path = require("path"); const { serve404, mimeTypes, serveStaticFile } = require("./utils"); class Route { constructor(path, options = {}) { if (typeof path !== "string" || !path) { throw new Error("Provide a valid route path"); } this.path = path; this.handlers = { GET: null, POST: null, PUT: null, DELETE: null, }; this.staticRoutes = new Map(); this.options = options; this.globalMiddlewares = []; } validateHandler(handler) { if (typeof handler !== "function") { throw new Error("Handler must be a function"); } } registerHandler(method, ...handlers) { if (this.handlers[method]) { throw new Error( `${method} handler for path "${this.path}" is already defined` ); } handlers.forEach(this.validateHandler); this.handlers[method] = handlers; } get(...handlers) { this.registerHandler("GET", ...handlers); return this; } post(...handlers) { this.registerHandler("POST", ...handlers); return this; } put(...handlers) { this.registerHandler("PUT", ...handlers); return this; } delete(...handlers) { this.registerHandler("DELETE", ...handlers); return this; } use(...middlewares) { middlewares.forEach(this.validateHandler); this.globalMiddlewares.push(...middlewares); return this; } async addStaticRoutes(rootPath, prefix = "") { try { const stat = await fs.promises.lstat(rootPath); if (stat.isFile()) { const urlPath = path .join(prefix, path.relative(this.staticDir, rootPath)) .replace(/\\/g, "/"); this.staticRoutes.set( (this.path === "/" ? "/" : this.path + "/") + urlPath, path.join(this.staticDir, urlPath) ); } else if (stat.isDirectory()) { const files = await fs.promises.readdir(rootPath); for (const file of files) { await this.addStaticRoutes(path.join(rootPath, file), prefix); } } } catch (error) { console.error("Error setting up static routes:", error); } } extractParams(routePattern, actualUrl) { const paramNames = []; const exists = new Set(); const routeSegments = routePattern.split("/"); const urlSegments = actualUrl.split("/"); if (routeSegments.length !== urlSegments.length) { return null; } const params = {}; for (let i = 0; i < routeSegments.length; i++) { if (routeSegments[i] && exists.has(routeSegments[i])) { throw new Error( "Found duplicate parameter in URL. Parameter name should be unique" ); } const routeSegment = routeSegments[i]; const urlSegment = urlSegments[i]; exists.add(routeSegment); if (routeSegment.startsWith(":")) { const paramName = routeSegment.slice(1); paramNames.push(paramName); params[paramName] = urlSegment; } else if (routeSegment !== urlSegment) { return null; // If a static segment doesn't match, return null } } return params; } async handleRequest(req, res) { const urlPath = req.url === "/" ? "/index.html" : req.url; if (this.staticRoutes.has(urlPath) && req.method === "GET") { const filePath = this.staticRoutes.get(urlPath); await serveStaticFile(filePath, res); } else { const method = req.method.toUpperCase(); const handlers = this.handlers[method]; if (handlers) { const parsedUrl = new URL(req.url, `http://${req.headers.host}`); req.params = this.extractParams(this.path, parsedUrl.pathname); req.query = Object.fromEntries(parsedUrl.searchParams.entries()); const contentType = req.headers["content-type"]; if (contentType === "application/json") { this.handleJsonRequest(req, res, handlers); } else if (contentType && contentType.includes("multipart/form-data")) { // this.handleMultipart(req, res, handlers); throw new Error( "Multipart/form-data is not supported at this moment" ); } else { this.handleOtherRequests(req, res, handlers); } } else { serve404(res, this.options.custom404Path); } } } handleJsonRequest(req, res, handlers) { let body = Buffer.alloc(0); let bodySize = 0; req.on("data", (data) => { bodySize += data.length; if (bodySize > this.options.bodySizeLimit) { res.writeHead(413, { "Content-Type": "text/plain" }); res.end("Payload Too Large"); req.destroy(); return; } body = Buffer.concat([body, data]); }); req.on("end", () => { try { req.body = body ? JSON.parse(body.toString()) : {}; this.executeHandlers(req, res, handlers); } catch (error) { console.error("Error parsing JSON:", error); res.writeHead(400, { "Content-Type": "text/plain" }); res.end("Bad Request"); } }); req.on("error", (error) => { console.error(error); res.writeHead(500, { "Content-Type": "text/plain" }); res.end("Internal Server Error"); }); } // handleMultipart(req, res, handlers) { // const boundary = req.headers["content-type"].split("boundary=")[1]; // const boundaryBuffer = Buffer.from(`--${boundary}`); // let fileStream = null; // let buffer = Buffer.alloc(0); // req.on("data", (data) => { // buffer = Buffer.concat([buffer, data]); // let boundaryIndex; // while ((boundaryIndex = buffer.indexOf(boundaryBuffer)) !== -1) { // const part = buffer.slice(0, boundaryIndex); // buffer = buffer.slice(boundaryIndex + boundaryBuffer.length); // if (fileStream) { // fileStream.end(part); // fileStream = null; // } else { // const headers = part.toString().split("\r\n"); // const dispositionHeader = headers.find(header => header.startsWith("Content-Disposition")); // if (dispositionHeader) { // const match = dispositionHeader.match(/filename="([^"]+)"/); // if (match) { // const filename = match[1]; // const filePath = path.join("./uploads", filename); // fileStream = fs.createWriteStream(filePath); // fileStream.write(part.slice(part.indexOf("\r\n\r\n") + 4)); // req.file = { // filename, // path: filePath, // }; // } // } // } // } // }); // req.on("end", () => { // if (fileStream) { // fileStream.end(buffer); // } // this.executeHandlers(req, res, handlers); // }); // req.on("error", (error) => { // console.error(error); // res.writeHead(500, { "Content-Type": "text/plain" }); // res.end("Internal Server Error"); // }); // } handleOtherRequests(req, res, handlers) { const contentType = req.headers["content-type"]; if (contentType) { req.file = req; } this.executeHandlers(req, res, handlers); } executeHandlers(req, res, handlers) { if (this.options.defaultHeaders) { for (const [key, value] of Object.entries(this.options.defaultHeaders)) { res.setHeader(key, value); } } const allHandlers = [...this.globalMiddlewares, ...handlers]; const execute = (index) => { if (index >= allHandlers.length) return; allHandlers[index](req, res, () => execute(index + 1)); }; execute(0); } sendStatic(staticDir, options = {}) { if (typeof staticDir !== "string" || !staticDir) { throw new Error("Provide a valid static directory path"); } this.staticDir = staticDir; this.options = options; this.addStaticRoutes(staticDir).catch((error) => { console.error("Error adding static routes:", error); }); return this; } } module.exports = Route;