cpeak 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../lib/index.ts","../lib/utils/serveStatic.ts","../lib/utils/parseJSON.ts","../lib/utils/render.ts"],"sourcesContent":["import http from \"node:http\";\nimport fs from \"node:fs/promises\";\nimport { createReadStream } from \"node:fs\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport type {\n StringMap,\n CpeakRequest,\n CpeakResponse,\n Middleware,\n RouteMiddleware,\n Handler,\n RoutesMap,\n} from \"./types\";\n\n// A utility function to create an error with a custom stack trace\nexport function frameworkError(\n message: string,\n skipFn: Function,\n code?: string\n) {\n const err = new Error(message) as Error & { code?: string };\n Error.captureStackTrace(err, skipFn);\n\n if (code) err.code = code;\n\n return err;\n}\n\nexport enum ErrorCode {\n MISSING_MIME = \"CPEAK_ERR_MISSING_MIME\",\n FILE_NOT_FOUND = \"CPEAK_ERR_FILE_NOT_FOUND\",\n NOT_A_FILE = \"CPEAK_ERR_NOT_A_FILE\",\n SEND_FILE_FAIL = \"CPEAK_ERR_SEND_FILE_FAIL\",\n}\n\nclass Cpeak {\n private server: http.Server;\n private routes: RoutesMap;\n private middleware: Middleware[];\n private _handleErr?: (\n err: unknown,\n req: CpeakRequest,\n res: CpeakResponse\n ) => void;\n\n constructor() {\n this.server = http.createServer();\n this.routes = {};\n this.middleware = [];\n\n this.server.on(\"request\", (req: CpeakRequest, res: CpeakResponse) => {\n // Send a file back to the client\n res.sendFile = async (path: string, mime: string) => {\n if (!mime) {\n throw frameworkError(\n 'MIME type is missing. Use res.sendFile(path, \"mime-type\").',\n res.sendFile,\n ErrorCode.MISSING_MIME\n );\n }\n\n try {\n const stat = await fs.stat(path);\n if (!stat.isFile()) {\n throw frameworkError(\n `Not a file: ${path}`,\n res.sendFile,\n ErrorCode.NOT_A_FILE\n );\n }\n\n res.setHeader(\"Content-Type\", mime);\n res.setHeader(\"Content-Length\", String(stat.size));\n\n // Properly propagate stream errors and respect backpressure\n await pipeline(createReadStream(path), res);\n } catch (err: any) {\n if (err?.code === \"ENOENT\") {\n throw frameworkError(\n `File not found: ${path}`,\n res.sendFile,\n ErrorCode.FILE_NOT_FOUND\n );\n }\n\n throw frameworkError(\n `Failed to send file: ${path}`,\n res.sendFile,\n ErrorCode.SEND_FILE_FAIL\n );\n }\n };\n\n // Set the status code of the response\n res.status = (code: number) => {\n res.statusCode = code;\n return res;\n };\n\n // Redirects to a new URL\n res.redirect = (location: string) => {\n res.writeHead(302, { Location: location });\n res.end();\n return res;\n };\n\n // Send a json data back to the client (for small json data, less than the highWaterMark)\n res.json = (data: any) => {\n // This is only good for bodies that their size is less than the highWaterMark value\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(data));\n };\n\n // Get the url without the URL parameters\n const urlWithoutParams = req.url?.split(\"?\")[0];\n\n // Parse the URL parameters (like /users?key1=value1&key2=value2)\n // We put this here to also parse them for all the middleware functions\n const params = new URLSearchParams(req.url?.split(\"?\")[1]);\n\n const paramsObject = Object.fromEntries(params.entries());\n\n req.params = paramsObject;\n req.query = paramsObject; // only for compatibility with frameworks built for express\n\n // Run all the specific middleware functions for that router only and then run the handler\n const runHandler = (\n req: CpeakRequest,\n res: CpeakResponse,\n middleware: RouteMiddleware[],\n cb: Handler,\n index: number\n ) => {\n // Our exit point...\n if (index === middleware.length) {\n // Call the route handler with the modified req and res objects.\n // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.\n try {\n const handlerResult = cb(req, res, (error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n });\n\n if (handlerResult && typeof handlerResult.then === \"function\") {\n handlerResult.catch((error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n });\n }\n\n return handlerResult;\n } catch (error) {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n } else {\n // Handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler middleware in try catch.\n try {\n const middlewareResult = middleware[index](\n req,\n res,\n // The next function\n (error) => {\n // this function only accepts an error argument to be more compatible with NPM modules that are built for express\n if (error) {\n res.setHeader(\"Connection\", \"close\");\n return this._handleErr?.(error, req, res);\n }\n runHandler(req, res, middleware, cb, index + 1);\n },\n // Error handler for a route middleware\n (error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n );\n\n // If the middleware is async, handle the promise rejection\n if (\n middlewareResult &&\n typeof middlewareResult.then === \"function\"\n ) {\n middlewareResult.catch((error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n });\n }\n } catch (error) {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n }\n };\n\n // Run all the middleware functions (beforeEach functions) before we run the corresponding route\n const runMiddleware = (\n req: CpeakRequest,\n res: CpeakResponse,\n middleware: Middleware[],\n index: number\n ) => {\n // Our exit point...\n if (index === middleware.length) {\n const routes = this.routes[req.method?.toLowerCase() || \"\"];\n if (routes && typeof routes[Symbol.iterator] === \"function\")\n for (const route of routes) {\n const match = urlWithoutParams?.match(route.regex);\n\n if (match) {\n // Parse the URL variables from the matched route (like /users/:id)\n const vars = this.#extractVars(route.path, match);\n req.vars = vars;\n\n return runHandler(req, res, route.middleware, route.cb, 0);\n }\n }\n\n // If the requested route dose not exist, return 404\n return res\n .status(404)\n .json({ error: `Cannot ${req.method} ${urlWithoutParams}` });\n } else {\n middleware[index](req, res, () => {\n runMiddleware(req, res, middleware, index + 1);\n });\n }\n };\n\n runMiddleware(req, res, this.middleware, 0);\n });\n }\n\n route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]) {\n if (!this.routes[method]) this.routes[method] = [];\n\n // The last argument should always be our handler\n const cb = args.pop() as Handler;\n\n if (!cb || typeof cb !== \"function\") {\n throw new Error(\"Route definition must include a handler\");\n }\n\n // Rest will be our middleware functions\n const middleware = args.flat() as RouteMiddleware[];\n\n const regex = this.#pathToRegex(path);\n this.routes[method].push({ path, regex, middleware, cb });\n }\n\n beforeEach(cb: Middleware) {\n this.middleware.push(cb);\n }\n\n handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void) {\n this._handleErr = cb;\n }\n\n listen(port: number, cb?: () => void) {\n return this.server.listen(port, cb);\n }\n\n close(cb?: (err?: Error) => void) {\n this.server.close(cb);\n }\n\n // ------------------------------\n // PRIVATE METHODS:\n // ------------------------------\n #pathToRegex(path: string) {\n const varNames: string[] = [];\n const regexString =\n \"^\" +\n path.replace(/:\\w+/g, (match, offset) => {\n varNames.push(match.slice(1));\n return \"([^/]+)\";\n }) +\n \"$\";\n\n const regex = new RegExp(regexString);\n return regex;\n }\n\n #extractVars(path: string, match: RegExpMatchArray) {\n // Extract url variable values from the matched route\n const varNames = (path.match(/:\\w+/g) || []).map((varParam) =>\n varParam.slice(1)\n );\n const vars: StringMap = {};\n varNames.forEach((name, index) => {\n vars[name] = match[index + 1];\n });\n return vars;\n }\n}\n\n// Util functions\nexport { serveStatic } from \"./utils/serveStatic.js\";\nexport { parseJSON } from \"./utils/parseJSON.js\";\nexport { render } from \"./utils/render.js\";\n\nexport type {\n Cpeak,\n CpeakRequest,\n CpeakResponse,\n Next,\n HandleErr,\n Middleware,\n RouteMiddleware,\n Handler,\n RoutesMap,\n} from \"./types\";\n\nexport default function cpeak() {\n return new Cpeak();\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type { StringMap, CpeakRequest, CpeakResponse, Next } from \"../types\";\n\nconst MIME_TYPES: StringMap = {\n html: \"text/html\",\n css: \"text/css\",\n js: \"application/javascript\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n svg: \"image/svg+xml\",\n txt: \"text/plain\",\n eot: \"application/vnd.ms-fontobject\",\n otf: \"font/otf\",\n ttf: \"font/ttf\",\n woff: \"font/woff\",\n woff2: \"font/woff2\",\n};\n\nconst serveStatic = (folderPath: string, newMimeTypes?: StringMap) => {\n // For new user defined mime types\n if (newMimeTypes) {\n Object.assign(MIME_TYPES, newMimeTypes);\n }\n\n function processFolder(folderPath: string, parentFolder: string) {\n const staticFiles: string[] = [];\n\n // Read the contents of the folder\n const files = fs.readdirSync(folderPath);\n\n // Loop through the files and subfolders\n for (const file of files) {\n const fullPath = path.join(folderPath, file);\n\n // Check if it's a directory\n if (fs.statSync(fullPath).isDirectory()) {\n // If it's a directory, recursively process it\n const subfolderFiles = processFolder(fullPath, parentFolder);\n staticFiles.push(...subfolderFiles);\n } else {\n // If it's a file, add it to the array\n const relativePath = path.relative(parentFolder, fullPath);\n const fileExtension = path.extname(file).slice(1);\n if (MIME_TYPES[fileExtension]) staticFiles.push(\"/\" + relativePath);\n }\n }\n\n return staticFiles;\n }\n\n const filesArrayToFilesMap = (filesArray: string[]) => {\n const filesMap: Record<string, { path: string; mime: string }> = {};\n for (const file of filesArray) {\n const fileExtension = path.extname(file).slice(1);\n filesMap[file] = {\n path: folderPath + file,\n mime: MIME_TYPES[fileExtension],\n };\n }\n return filesMap;\n };\n\n // Start processing the folder\n const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));\n\n return function (req: CpeakRequest, res: CpeakResponse, next: Next) {\n const url = req.url;\n if (typeof url !== \"string\") return next();\n\n if (Object.prototype.hasOwnProperty.call(filesMap, url)) {\n const fileRoute = filesMap[url];\n return res.sendFile(fileRoute.path, fileRoute.mime);\n }\n\n next();\n };\n};\n\nexport { serveStatic };\n","import type { CpeakRequest, CpeakResponse, Next } from \"../types\";\n\n// Parsing JSON\nconst parseJSON = (req: CpeakRequest, res: CpeakResponse, next: Next) => {\n // This is only good for bodies that their size is less than the highWaterMark value\n\n function isJSON(contentType: string = \"\") {\n // Remove any params like \"; charset=UTF-8\"\n const [type] = contentType.split(\";\");\n return (\n type.trim().toLowerCase() === \"application/json\" ||\n /\\+json$/i.test(type.trim())\n );\n }\n\n if (!isJSON(req.headers[\"content-type\"] as string)) return next();\n\n let body = \"\";\n req.on(\"data\", (chunk: Buffer) => {\n body += chunk.toString(\"utf-8\");\n });\n\n req.on(\"end\", () => {\n body = JSON.parse(body);\n req.body = body;\n return next();\n });\n};\n\nexport { parseJSON };\n","import fs from \"node:fs/promises\";\nimport { frameworkError } from \"../\";\nimport type { CpeakRequest, CpeakResponse, Next } from \"../types\";\n\nfunction renderTemplate(\n templateStr: string,\n data: Record<string, unknown>\n): string {\n // Initialize variables\n let result: (string | unknown)[] = [];\n\n let currentIndex = 0;\n\n while (currentIndex < templateStr.length) {\n // Find the next opening placeholder\n const startIdx = templateStr.indexOf(\"{{\", currentIndex);\n if (startIdx === -1) {\n // No more placeholders, push the remaining string\n result.push(templateStr.slice(currentIndex));\n break;\n }\n\n // Push the part before the placeholder\n result.push(templateStr.slice(currentIndex, startIdx));\n\n // Find the closing placeholder\n const endIdx = templateStr.indexOf(\"}}\", startIdx);\n if (endIdx === -1) {\n // No closing brace found, treat the rest as plain text\n result.push(templateStr.slice(startIdx));\n break;\n }\n\n // Extract the variable name\n const varName = templateStr.slice(startIdx + 2, endIdx).trim();\n\n // Replace the variable with its value from the data, or use an empty string if not found\n const replacement = data[varName] !== undefined ? data[varName] : \"\";\n\n // Push the replacement to the result array\n result.push(replacement);\n\n // Move the index past the current closing placeholder\n currentIndex = endIdx + 2;\n }\n\n // Join all parts into a final string\n return result.join(\"\");\n}\n\n// Errors to return: recommend to not render files larger than 100KB\n// To Explore: Doing the operation in C++ and return the data as stream back to the client\n// @TODO: remove the file from static map\n// @TODO: escape the string to prevent XSS\n// @TODO: add another {{{ }}} option to not escape the string\nconst render = () => {\n console.log(\"render.ts loaded\");\n return function (req: CpeakRequest, res: CpeakResponse, next: Next): void {\n res.render = async (\n path: string,\n data: Record<string, unknown>,\n mime: string\n ) => {\n // check if mime is specified, if not return an error\n if (!mime) {\n throw frameworkError(\n `MIME type is missing. You called res.render(\"${path}\", ...) but forgot to provide the third \"mime\" argument.`,\n res.render\n );\n }\n\n let fileStr = await fs.readFile(path, \"utf-8\");\n const finalStr = renderTemplate(fileStr, data);\n res.setHeader(\"Content-Type\", mime);\n res.end(finalStr);\n };\n\n next();\n };\n};\n\nexport { render };\n"],"mappings":"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","names":["fs","folderPath","filesMap","fs","path","fs","ErrorCode","path","fs","req","res"]}
1
+ {"version":3,"sources":["../lib/index.ts","../lib/utils/serveStatic.ts","../lib/utils/paseJSON.ts","../lib/utils/render.ts"],"sourcesContent":["import http from \"node:http\";\nimport fs from \"node:fs/promises\";\nimport { createReadStream } from \"node:fs\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport type {\n StringMap,\n CpeakRequest,\n CpeakResponse,\n Middleware,\n RouteMiddleware,\n Handler,\n RoutesMap\n} from \"./types\";\n\n// A utility function to create an error with a custom stack trace\nexport function frameworkError(\n message: string,\n skipFn: Function,\n code?: string,\n status?: number\n) {\n const err = new Error(message) as Error & {\n code?: string;\n cpeak_err?: boolean;\n };\n Error.captureStackTrace(err, skipFn);\n\n err.cpeak_err = true;\n\n if (code) err.code = code;\n if (status) (err as any).status = status;\n\n return err;\n}\n\nexport enum ErrorCode {\n MISSING_MIME = \"CPEAK_ERR_MISSING_MIME\",\n FILE_NOT_FOUND = \"CPEAK_ERR_FILE_NOT_FOUND\",\n NOT_A_FILE = \"CPEAK_ERR_NOT_A_FILE\",\n SEND_FILE_FAIL = \"CPEAK_ERR_SEND_FILE_FAIL\",\n INVALID_JSON = \"CPEAK_ERR_INVALID_JSON\",\n PAYLOAD_TOO_LARGE = \"CPEAK_ERR_PAYLOAD_TOO_LARGE\"\n}\n\nclass CpeakIncomingMessage extends http.IncomingMessage {\n // We define body and params here for better V8 optimization (not changing the shape of the object at runtime)\n public body: any = undefined;\n public params: StringMap = {};\n\n private _query?: StringMap;\n\n // Parse the URL parameters (like /users?key1=value1&key2=value2)\n // We will call this query to be more familiar with other node.js frameworks.\n // This is a getter method (accessed like a property)\n get query(): StringMap {\n // This way if a developer writes req.query multiple times, we don't parse it multiple times\n if (this._query) return this._query;\n\n const url = this.url || \"\";\n const qIndex = url.indexOf(\"?\");\n\n if (qIndex === -1) {\n this._query = {};\n } else {\n const searchParams = new URLSearchParams(url.substring(qIndex + 1));\n this._query = Object.fromEntries(searchParams.entries());\n }\n\n return this._query;\n }\n}\n\nclass CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {\n // Send a file back to the client\n async sendFile(path: string, mime: string) {\n if (!mime) {\n throw frameworkError(\n 'MIME type is missing. Use res.sendFile(path, \"mime-type\").',\n this.sendFile,\n ErrorCode.MISSING_MIME\n );\n }\n\n try {\n const stat = await fs.stat(path);\n if (!stat.isFile()) {\n throw frameworkError(\n `Not a file: ${path}`,\n this.sendFile,\n ErrorCode.NOT_A_FILE\n );\n }\n\n this.setHeader(\"Content-Type\", mime);\n this.setHeader(\"Content-Length\", String(stat.size));\n\n // Properly propagate stream errors and respect backpressure\n await pipeline(createReadStream(path), this);\n } catch (err: any) {\n if (err?.code === \"ENOENT\") {\n throw frameworkError(\n `File not found: ${path}`,\n this.sendFile,\n ErrorCode.FILE_NOT_FOUND\n );\n }\n\n throw frameworkError(\n `Failed to send file: ${path}`,\n this.sendFile,\n ErrorCode.SEND_FILE_FAIL\n );\n }\n }\n\n // Set the status code of the response\n status(code: number) {\n this.statusCode = code;\n return this;\n }\n\n // Redirects to a new URL\n redirect(location: string) {\n this.writeHead(302, { Location: location });\n this.end();\n return this;\n }\n\n // Send a json data back to the client (for small json data, less than the highWaterMark)\n json(data: any) {\n // This is only good for bodies that their size is less than the highWaterMark value\n this.setHeader(\"Content-Type\", \"application/json\");\n this.end(JSON.stringify(data));\n }\n}\n\nclass Cpeak {\n private server: http.Server<\n typeof CpeakIncomingMessage,\n typeof CpeakServerResponse\n >;\n private routes: RoutesMap;\n private middleware: Middleware[];\n private _handleErr?: (\n err: unknown,\n req: CpeakRequest,\n res: CpeakResponse\n ) => void;\n\n constructor() {\n this.server = http.createServer({\n IncomingMessage: CpeakIncomingMessage,\n ServerResponse: CpeakServerResponse\n });\n this.routes = {};\n this.middleware = [];\n\n this.server.on(\"request\", async (req: CpeakRequest, res: CpeakResponse) => {\n // Get the url without the URL parameters (query strings)\n const qIndex = req.url?.indexOf(\"?\");\n const urlWithoutQueries =\n qIndex === -1 ? req.url || \"\" : req.url?.substring(0, qIndex);\n\n // Run all the specific middleware functions for that router only and then run the handler\n const runHandler = async (\n req: CpeakRequest,\n res: CpeakResponse,\n middleware: RouteMiddleware[],\n cb: Handler,\n index: number\n ) => {\n // Our exit point...\n if (index === middleware.length) {\n // Call the route handler with the modified req and res objects.\n // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.\n try {\n await cb(req, res, (error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n });\n } catch (error) {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n } else {\n // Handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler middleware in try catch.\n try {\n await middleware[index](\n req,\n res,\n // The next function\n async (error) => {\n // this function only accepts an error argument to be more compatible with NPM modules that are built for express\n if (error) {\n res.setHeader(\"Connection\", \"close\");\n return this._handleErr?.(error, req, res);\n }\n await runHandler(req, res, middleware, cb, index + 1);\n },\n // Error handler for a route middleware\n (error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n );\n } catch (error) {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n }\n };\n\n // Run all the middleware functions (beforeEach functions) before we run the corresponding route\n const runMiddleware = async (\n req: CpeakRequest,\n res: CpeakResponse,\n middleware: Middleware[],\n index: number\n ) => {\n // Our exit point...\n if (index === middleware.length) {\n const routes = this.routes[req.method?.toLowerCase() || \"\"];\n if (routes && typeof routes[Symbol.iterator] === \"function\")\n for (const route of routes) {\n const match = urlWithoutQueries?.match(route.regex);\n\n if (match) {\n // Parse the URL path variables from the matched route (like /users/:id)\n const pathVariables = this.#extractPathVariables(\n route.path,\n match\n );\n\n // We will call this params to be more familiar with other node.js frameworks.\n req.params = pathVariables;\n\n return await runHandler(\n req,\n res,\n route.middleware,\n route.cb,\n 0\n );\n }\n }\n\n // If the requested route dose not exist, return 404\n return res\n .status(404)\n .json({ error: `Cannot ${req.method} ${urlWithoutQueries}` });\n } else {\n try {\n await middleware[index](req, res, async (err?: unknown) => {\n if (err) {\n res.setHeader(\"Connection\", \"close\");\n return this._handleErr?.(err, req, res);\n }\n await runMiddleware(req, res, middleware, index + 1);\n });\n } catch (error) {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n }\n };\n\n await runMiddleware(req, res, this.middleware, 0);\n });\n }\n\n route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]) {\n if (!this.routes[method]) this.routes[method] = [];\n\n // The last argument should always be our handler\n const cb = args.pop() as Handler;\n\n if (!cb || typeof cb !== \"function\") {\n throw new Error(\"Route definition must include a handler\");\n }\n\n // Rest will be our middleware functions\n const middleware = args.flat() as RouteMiddleware[];\n\n const regex = this.#pathToRegex(path);\n this.routes[method].push({ path, regex, middleware, cb });\n }\n\n beforeEach(cb: Middleware) {\n this.middleware.push(cb);\n }\n\n handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void) {\n this._handleErr = cb;\n }\n\n listen(port: number, cb?: () => void) {\n return this.server.listen(port, cb);\n }\n\n close(cb?: (err?: Error) => void) {\n this.server.close(cb);\n }\n\n // ------------------------------\n // PRIVATE METHODS:\n // ------------------------------\n #pathToRegex(path: string) {\n const paramNames: string[] = [];\n const regexString =\n \"^\" +\n path.replace(/:\\w+/g, (match, offset) => {\n paramNames.push(match.slice(1));\n return \"([^/]+)\";\n }) +\n \"$\";\n\n const regex = new RegExp(regexString);\n return regex;\n }\n\n #extractPathVariables(path: string, match: RegExpMatchArray) {\n // Extract path url variable values from the matched route\n const paramNames = (path.match(/:\\w+/g) || []).map((param) =>\n param.slice(1)\n );\n const params: StringMap = {};\n paramNames.forEach((name, index) => {\n params[name] = match[index + 1];\n });\n return params;\n }\n}\n\n// Util functions\nexport { serveStatic } from \"./utils/serveStatic.js\";\nexport { parseJSON } from \"./utils/paseJSON.js\";\nexport { render } from \"./utils/render.js\";\n\nexport type {\n Cpeak,\n CpeakRequest,\n CpeakResponse,\n Next,\n HandleErr,\n Middleware,\n RouteMiddleware,\n Handler,\n RoutesMap\n} from \"./types\";\n\nexport default function cpeak() {\n return new Cpeak();\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type { StringMap, CpeakRequest, CpeakResponse, Next } from \"../types\";\n\nconst MIME_TYPES: StringMap = {\n html: \"text/html\",\n css: \"text/css\",\n js: \"application/javascript\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n svg: \"image/svg+xml\",\n txt: \"text/plain\",\n eot: \"application/vnd.ms-fontobject\",\n otf: \"font/otf\",\n ttf: \"font/ttf\",\n woff: \"font/woff\",\n woff2: \"font/woff2\",\n};\n\nconst serveStatic = (folderPath: string, newMimeTypes?: StringMap) => {\n // For new user defined mime types\n if (newMimeTypes) {\n Object.assign(MIME_TYPES, newMimeTypes);\n }\n\n function processFolder(folderPath: string, parentFolder: string) {\n const staticFiles: string[] = [];\n\n // Read the contents of the folder\n const files = fs.readdirSync(folderPath);\n\n // Loop through the files and subfolders\n for (const file of files) {\n const fullPath = path.join(folderPath, file);\n\n // Check if it's a directory\n if (fs.statSync(fullPath).isDirectory()) {\n // If it's a directory, recursively process it\n const subfolderFiles = processFolder(fullPath, parentFolder);\n staticFiles.push(...subfolderFiles);\n } else {\n // If it's a file, add it to the array\n const relativePath = path.relative(parentFolder, fullPath);\n const fileExtension = path.extname(file).slice(1);\n if (MIME_TYPES[fileExtension]) staticFiles.push(\"/\" + relativePath);\n }\n }\n\n return staticFiles;\n }\n\n const filesArrayToFilesMap = (filesArray: string[]) => {\n const filesMap: Record<string, { path: string; mime: string }> = {};\n for (const file of filesArray) {\n const fileExtension = path.extname(file).slice(1);\n filesMap[file] = {\n path: folderPath + file,\n mime: MIME_TYPES[fileExtension],\n };\n }\n return filesMap;\n };\n\n // Start processing the folder\n const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));\n\n return function (req: CpeakRequest, res: CpeakResponse, next: Next) {\n const url = req.url;\n if (typeof url !== \"string\") return next();\n\n if (Object.prototype.hasOwnProperty.call(filesMap, url)) {\n const fileRoute = filesMap[url];\n return res.sendFile(fileRoute.path, fileRoute.mime);\n }\n\n next();\n };\n};\n\nexport { serveStatic };\n","import type { CpeakRequest, CpeakResponse, Next } from \"../types\";\nimport { Buffer } from \"node:buffer\";\nimport { frameworkError, ErrorCode } from \"../index\";\n\n// Check if Content-Type is JSON\nfunction isJSON(contentType: string | undefined) {\n if (!contentType) return false;\n if (contentType === \"application/json\") return true;\n return (\n contentType.startsWith(\"application/json\") || contentType.includes(\"+json\")\n );\n}\n\n// Parsing JSON\nconst parseJSON = (options: { limit?: number } = {}) => {\n // Default limit to 1MB\n const limit = options.limit || 1024 * 1024;\n\n return (req: CpeakRequest, res: CpeakResponse, next: Next) => {\n if (!isJSON(req.headers[\"content-type\"])) return next();\n\n const chunks: Buffer[] = [];\n let bytesReceived = 0;\n\n const onData = (chunk: Buffer) => {\n bytesReceived += chunk.length;\n\n // To prevent Denial of Service (DoS) attacks, enforce a maximum body size\n if (bytesReceived > limit) {\n // Stop listening to data\n req.pause();\n\n // Remove listeners so we don't trigger 'end' or more 'data'\n req.removeListener(\"data\", onData);\n req.removeListener(\"end\", onEnd);\n\n next(\n frameworkError(\n \"JSON body too large\",\n onData,\n ErrorCode.PAYLOAD_TOO_LARGE,\n 413 // HTTP 413 Payload Too Large\n )\n );\n\n return;\n }\n\n chunks.push(chunk);\n };\n\n const onEnd = () => {\n try {\n // For better performance, we concat buffers once, then convert to string\n // Optimization: If only one chunk exists, avoid the memory copy of concat\n const rawBody =\n chunks.length === 1\n ? chunks[0].toString(\"utf-8\")\n : Buffer.concat(chunks).toString(\"utf-8\");\n\n // Handle empty body case\n req.body = rawBody ? JSON.parse(rawBody) : {};\n\n next();\n } catch (err) {\n // Handle Invalid JSON without crashing\n next(\n frameworkError(\n \"Invalid JSON format\",\n onEnd,\n ErrorCode.INVALID_JSON,\n 400 // HTTP 400 Bad Request\n )\n );\n }\n };\n\n req.on(\"data\", onData);\n req.on(\"end\", onEnd);\n };\n};\n\nexport { parseJSON };\n","import fs from \"node:fs/promises\";\nimport { frameworkError } from \"../\";\nimport type { CpeakRequest, CpeakResponse, Next } from \"../types\";\n\nfunction renderTemplate(\n templateStr: string,\n data: Record<string, unknown>\n): string {\n // Initialize variables\n let result: (string | unknown)[] = [];\n\n let currentIndex = 0;\n\n while (currentIndex < templateStr.length) {\n // Find the next opening placeholder\n const startIdx = templateStr.indexOf(\"{{\", currentIndex);\n if (startIdx === -1) {\n // No more placeholders, push the remaining string\n result.push(templateStr.slice(currentIndex));\n break;\n }\n\n // Push the part before the placeholder\n result.push(templateStr.slice(currentIndex, startIdx));\n\n // Find the closing placeholder\n const endIdx = templateStr.indexOf(\"}}\", startIdx);\n if (endIdx === -1) {\n // No closing brace found, treat the rest as plain text\n result.push(templateStr.slice(startIdx));\n break;\n }\n\n // Extract the variable name\n const varName = templateStr.slice(startIdx + 2, endIdx).trim();\n\n // Replace the variable with its value from the data, or use an empty string if not found\n const replacement = data[varName] !== undefined ? data[varName] : \"\";\n\n // Push the replacement to the result array\n result.push(replacement);\n\n // Move the index past the current closing placeholder\n currentIndex = endIdx + 2;\n }\n\n // Join all parts into a final string\n return result.join(\"\");\n}\n\n// Errors to return: recommend to not render files larger than 100KB\n// To Explore: Doing the operation in C++ and return the data as stream back to the client\n// @TODO: remove the file from static map\n// @TODO: escape the string to prevent XSS\n// @TODO: add another {{{ }}} option to not escape the string\nconst render = () => {\n return function (req: CpeakRequest, res: CpeakResponse, next: Next): void {\n res.render = async (\n path: string,\n data: Record<string, unknown>,\n mime: string\n ) => {\n // check if mime is specified, if not return an error\n if (!mime) {\n throw frameworkError(\n `MIME type is missing. You called res.render(\"${path}\", ...) but forgot to provide the third \"mime\" argument.`,\n res.render\n );\n }\n\n let fileStr = await fs.readFile(path, \"utf-8\");\n const finalStr = renderTemplate(fileStr, data);\n res.setHeader(\"Content-Type\", mime);\n res.end(finalStr);\n };\n\n next();\n };\n};\n\nexport { render };\n"],"mappings":"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","names":["fs","folderPath","filesMap","fs","path","fs","ErrorCode","path","fs","req","res"]}
package/lib/index.ts CHANGED
@@ -10,19 +10,26 @@ import type {
10
10
  Middleware,
11
11
  RouteMiddleware,
12
12
  Handler,
13
- RoutesMap,
13
+ RoutesMap
14
14
  } from "./types";
15
15
 
16
16
  // A utility function to create an error with a custom stack trace
17
17
  export function frameworkError(
18
18
  message: string,
19
19
  skipFn: Function,
20
- code?: string
20
+ code?: string,
21
+ status?: number
21
22
  ) {
22
- const err = new Error(message) as Error & { code?: string };
23
+ const err = new Error(message) as Error & {
24
+ code?: string;
25
+ cpeak_err?: boolean;
26
+ };
23
27
  Error.captureStackTrace(err, skipFn);
24
28
 
29
+ err.cpeak_err = true;
30
+
25
31
  if (code) err.code = code;
32
+ if (status) (err as any).status = status;
26
33
 
27
34
  return err;
28
35
  }
@@ -32,10 +39,107 @@ export enum ErrorCode {
32
39
  FILE_NOT_FOUND = "CPEAK_ERR_FILE_NOT_FOUND",
33
40
  NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
34
41
  SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
42
+ INVALID_JSON = "CPEAK_ERR_INVALID_JSON",
43
+ PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE"
44
+ }
45
+
46
+ class CpeakIncomingMessage extends http.IncomingMessage {
47
+ // We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
48
+ public body: any = undefined;
49
+ public params: StringMap = {};
50
+
51
+ private _query?: StringMap;
52
+
53
+ // Parse the URL parameters (like /users?key1=value1&key2=value2)
54
+ // We will call this query to be more familiar with other node.js frameworks.
55
+ // This is a getter method (accessed like a property)
56
+ get query(): StringMap {
57
+ // This way if a developer writes req.query multiple times, we don't parse it multiple times
58
+ if (this._query) return this._query;
59
+
60
+ const url = this.url || "";
61
+ const qIndex = url.indexOf("?");
62
+
63
+ if (qIndex === -1) {
64
+ this._query = {};
65
+ } else {
66
+ const searchParams = new URLSearchParams(url.substring(qIndex + 1));
67
+ this._query = Object.fromEntries(searchParams.entries());
68
+ }
69
+
70
+ return this._query;
71
+ }
72
+ }
73
+
74
+ class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
75
+ // Send a file back to the client
76
+ async sendFile(path: string, mime: string) {
77
+ if (!mime) {
78
+ throw frameworkError(
79
+ 'MIME type is missing. Use res.sendFile(path, "mime-type").',
80
+ this.sendFile,
81
+ ErrorCode.MISSING_MIME
82
+ );
83
+ }
84
+
85
+ try {
86
+ const stat = await fs.stat(path);
87
+ if (!stat.isFile()) {
88
+ throw frameworkError(
89
+ `Not a file: ${path}`,
90
+ this.sendFile,
91
+ ErrorCode.NOT_A_FILE
92
+ );
93
+ }
94
+
95
+ this.setHeader("Content-Type", mime);
96
+ this.setHeader("Content-Length", String(stat.size));
97
+
98
+ // Properly propagate stream errors and respect backpressure
99
+ await pipeline(createReadStream(path), this);
100
+ } catch (err: any) {
101
+ if (err?.code === "ENOENT") {
102
+ throw frameworkError(
103
+ `File not found: ${path}`,
104
+ this.sendFile,
105
+ ErrorCode.FILE_NOT_FOUND
106
+ );
107
+ }
108
+
109
+ throw frameworkError(
110
+ `Failed to send file: ${path}`,
111
+ this.sendFile,
112
+ ErrorCode.SEND_FILE_FAIL
113
+ );
114
+ }
115
+ }
116
+
117
+ // Set the status code of the response
118
+ status(code: number) {
119
+ this.statusCode = code;
120
+ return this;
121
+ }
122
+
123
+ // Redirects to a new URL
124
+ redirect(location: string) {
125
+ this.writeHead(302, { Location: location });
126
+ this.end();
127
+ return this;
128
+ }
129
+
130
+ // Send a json data back to the client (for small json data, less than the highWaterMark)
131
+ json(data: any) {
132
+ // This is only good for bodies that their size is less than the highWaterMark value
133
+ this.setHeader("Content-Type", "application/json");
134
+ this.end(JSON.stringify(data));
135
+ }
35
136
  }
36
137
 
37
138
  class Cpeak {
38
- private server: http.Server;
139
+ private server: http.Server<
140
+ typeof CpeakIncomingMessage,
141
+ typeof CpeakServerResponse
142
+ >;
39
143
  private routes: RoutesMap;
40
144
  private middleware: Middleware[];
41
145
  private _handleErr?: (
@@ -45,87 +149,21 @@ class Cpeak {
45
149
  ) => void;
46
150
 
47
151
  constructor() {
48
- this.server = http.createServer();
152
+ this.server = http.createServer({
153
+ IncomingMessage: CpeakIncomingMessage,
154
+ ServerResponse: CpeakServerResponse
155
+ });
49
156
  this.routes = {};
50
157
  this.middleware = [];
51
158
 
52
- this.server.on("request", (req: CpeakRequest, res: CpeakResponse) => {
53
- // Send a file back to the client
54
- res.sendFile = async (path: string, mime: string) => {
55
- if (!mime) {
56
- throw frameworkError(
57
- 'MIME type is missing. Use res.sendFile(path, "mime-type").',
58
- res.sendFile,
59
- ErrorCode.MISSING_MIME
60
- );
61
- }
62
-
63
- try {
64
- const stat = await fs.stat(path);
65
- if (!stat.isFile()) {
66
- throw frameworkError(
67
- `Not a file: ${path}`,
68
- res.sendFile,
69
- ErrorCode.NOT_A_FILE
70
- );
71
- }
72
-
73
- res.setHeader("Content-Type", mime);
74
- res.setHeader("Content-Length", String(stat.size));
75
-
76
- // Properly propagate stream errors and respect backpressure
77
- await pipeline(createReadStream(path), res);
78
- } catch (err: any) {
79
- if (err?.code === "ENOENT") {
80
- throw frameworkError(
81
- `File not found: ${path}`,
82
- res.sendFile,
83
- ErrorCode.FILE_NOT_FOUND
84
- );
85
- }
86
-
87
- throw frameworkError(
88
- `Failed to send file: ${path}`,
89
- res.sendFile,
90
- ErrorCode.SEND_FILE_FAIL
91
- );
92
- }
93
- };
94
-
95
- // Set the status code of the response
96
- res.status = (code: number) => {
97
- res.statusCode = code;
98
- return res;
99
- };
100
-
101
- // Redirects to a new URL
102
- res.redirect = (location: string) => {
103
- res.writeHead(302, { Location: location });
104
- res.end();
105
- return res;
106
- };
107
-
108
- // Send a json data back to the client (for small json data, less than the highWaterMark)
109
- res.json = (data: any) => {
110
- // This is only good for bodies that their size is less than the highWaterMark value
111
- res.setHeader("Content-Type", "application/json");
112
- res.end(JSON.stringify(data));
113
- };
114
-
115
- // Get the url without the URL parameters
116
- const urlWithoutParams = req.url?.split("?")[0];
117
-
118
- // Parse the URL parameters (like /users?key1=value1&key2=value2)
119
- // We put this here to also parse them for all the middleware functions
120
- const params = new URLSearchParams(req.url?.split("?")[1]);
121
-
122
- const paramsObject = Object.fromEntries(params.entries());
123
-
124
- req.params = paramsObject;
125
- req.query = paramsObject; // only for compatibility with frameworks built for express
159
+ this.server.on("request", async (req: CpeakRequest, res: CpeakResponse) => {
160
+ // Get the url without the URL parameters (query strings)
161
+ const qIndex = req.url?.indexOf("?");
162
+ const urlWithoutQueries =
163
+ qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
126
164
 
127
165
  // Run all the specific middleware functions for that router only and then run the handler
128
- const runHandler = (
166
+ const runHandler = async (
129
167
  req: CpeakRequest,
130
168
  res: CpeakResponse,
131
169
  middleware: RouteMiddleware[],
@@ -137,19 +175,10 @@ class Cpeak {
137
175
  // Call the route handler with the modified req and res objects.
138
176
  // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.
139
177
  try {
140
- const handlerResult = cb(req, res, (error) => {
178
+ await cb(req, res, (error) => {
141
179
  res.setHeader("Connection", "close");
142
180
  this._handleErr?.(error, req, res);
143
181
  });
144
-
145
- if (handlerResult && typeof handlerResult.then === "function") {
146
- handlerResult.catch((error) => {
147
- res.setHeader("Connection", "close");
148
- this._handleErr?.(error, req, res);
149
- });
150
- }
151
-
152
- return handlerResult;
153
182
  } catch (error) {
154
183
  res.setHeader("Connection", "close");
155
184
  this._handleErr?.(error, req, res);
@@ -157,17 +186,17 @@ class Cpeak {
157
186
  } else {
158
187
  // Handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler middleware in try catch.
159
188
  try {
160
- const middlewareResult = middleware[index](
189
+ await middleware[index](
161
190
  req,
162
191
  res,
163
192
  // The next function
164
- (error) => {
193
+ async (error) => {
165
194
  // this function only accepts an error argument to be more compatible with NPM modules that are built for express
166
195
  if (error) {
167
196
  res.setHeader("Connection", "close");
168
197
  return this._handleErr?.(error, req, res);
169
198
  }
170
- runHandler(req, res, middleware, cb, index + 1);
199
+ await runHandler(req, res, middleware, cb, index + 1);
171
200
  },
172
201
  // Error handler for a route middleware
173
202
  (error) => {
@@ -175,17 +204,6 @@ class Cpeak {
175
204
  this._handleErr?.(error, req, res);
176
205
  }
177
206
  );
178
-
179
- // If the middleware is async, handle the promise rejection
180
- if (
181
- middlewareResult &&
182
- typeof middlewareResult.then === "function"
183
- ) {
184
- middlewareResult.catch((error) => {
185
- res.setHeader("Connection", "close");
186
- this._handleErr?.(error, req, res);
187
- });
188
- }
189
207
  } catch (error) {
190
208
  res.setHeader("Connection", "close");
191
209
  this._handleErr?.(error, req, res);
@@ -194,7 +212,7 @@ class Cpeak {
194
212
  };
195
213
 
196
214
  // Run all the middleware functions (beforeEach functions) before we run the corresponding route
197
- const runMiddleware = (
215
+ const runMiddleware = async (
198
216
  req: CpeakRequest,
199
217
  res: CpeakResponse,
200
218
  middleware: Middleware[],
@@ -205,29 +223,49 @@ class Cpeak {
205
223
  const routes = this.routes[req.method?.toLowerCase() || ""];
206
224
  if (routes && typeof routes[Symbol.iterator] === "function")
207
225
  for (const route of routes) {
208
- const match = urlWithoutParams?.match(route.regex);
226
+ const match = urlWithoutQueries?.match(route.regex);
209
227
 
210
228
  if (match) {
211
- // Parse the URL variables from the matched route (like /users/:id)
212
- const vars = this.#extractVars(route.path, match);
213
- req.vars = vars;
214
-
215
- return runHandler(req, res, route.middleware, route.cb, 0);
229
+ // Parse the URL path variables from the matched route (like /users/:id)
230
+ const pathVariables = this.#extractPathVariables(
231
+ route.path,
232
+ match
233
+ );
234
+
235
+ // We will call this params to be more familiar with other node.js frameworks.
236
+ req.params = pathVariables;
237
+
238
+ return await runHandler(
239
+ req,
240
+ res,
241
+ route.middleware,
242
+ route.cb,
243
+ 0
244
+ );
216
245
  }
217
246
  }
218
247
 
219
248
  // If the requested route dose not exist, return 404
220
249
  return res
221
250
  .status(404)
222
- .json({ error: `Cannot ${req.method} ${urlWithoutParams}` });
251
+ .json({ error: `Cannot ${req.method} ${urlWithoutQueries}` });
223
252
  } else {
224
- middleware[index](req, res, () => {
225
- runMiddleware(req, res, middleware, index + 1);
226
- });
253
+ try {
254
+ await middleware[index](req, res, async (err?: unknown) => {
255
+ if (err) {
256
+ res.setHeader("Connection", "close");
257
+ return this._handleErr?.(err, req, res);
258
+ }
259
+ await runMiddleware(req, res, middleware, index + 1);
260
+ });
261
+ } catch (error) {
262
+ res.setHeader("Connection", "close");
263
+ this._handleErr?.(error, req, res);
264
+ }
227
265
  }
228
266
  };
229
267
 
230
- runMiddleware(req, res, this.middleware, 0);
268
+ await runMiddleware(req, res, this.middleware, 0);
231
269
  });
232
270
  }
233
271
 
@@ -268,11 +306,11 @@ class Cpeak {
268
306
  // PRIVATE METHODS:
269
307
  // ------------------------------
270
308
  #pathToRegex(path: string) {
271
- const varNames: string[] = [];
309
+ const paramNames: string[] = [];
272
310
  const regexString =
273
311
  "^" +
274
312
  path.replace(/:\w+/g, (match, offset) => {
275
- varNames.push(match.slice(1));
313
+ paramNames.push(match.slice(1));
276
314
  return "([^/]+)";
277
315
  }) +
278
316
  "$";
@@ -281,22 +319,22 @@ class Cpeak {
281
319
  return regex;
282
320
  }
283
321
 
284
- #extractVars(path: string, match: RegExpMatchArray) {
285
- // Extract url variable values from the matched route
286
- const varNames = (path.match(/:\w+/g) || []).map((varParam) =>
287
- varParam.slice(1)
322
+ #extractPathVariables(path: string, match: RegExpMatchArray) {
323
+ // Extract path url variable values from the matched route
324
+ const paramNames = (path.match(/:\w+/g) || []).map((param) =>
325
+ param.slice(1)
288
326
  );
289
- const vars: StringMap = {};
290
- varNames.forEach((name, index) => {
291
- vars[name] = match[index + 1];
327
+ const params: StringMap = {};
328
+ paramNames.forEach((name, index) => {
329
+ params[name] = match[index + 1];
292
330
  });
293
- return vars;
331
+ return params;
294
332
  }
295
333
  }
296
334
 
297
335
  // Util functions
298
336
  export { serveStatic } from "./utils/serveStatic.js";
299
- export { parseJSON } from "./utils/parseJSON.js";
337
+ export { parseJSON } from "./utils/paseJSON.js";
300
338
  export { render } from "./utils/render.js";
301
339
 
302
340
  export type {
@@ -308,7 +346,7 @@ export type {
308
346
  Middleware,
309
347
  RouteMiddleware,
310
348
  Handler,
311
- RoutesMap,
349
+ RoutesMap
312
350
  } from "./types";
313
351
 
314
352
  export default function cpeak() {
package/lib/types.ts CHANGED
@@ -6,15 +6,15 @@ export type Cpeak = ReturnType<typeof cpeak>;
6
6
  // Extending Node.js's Request and Response objects to add our custom properties
7
7
  export type StringMap = Record<string, string>;
8
8
 
9
- export interface CpeakRequest<ReqBody = any, ReqParams = any>
10
- extends IncomingMessage {
11
- params: ReqParams;
12
- vars?: StringMap;
9
+ export interface CpeakRequest<
10
+ ReqBody = any,
11
+ ReqQueries = any
12
+ > extends IncomingMessage {
13
+ params: StringMap;
14
+ query: ReqQueries;
15
+ // vars?: StringMap;
13
16
  body?: ReqBody;
14
17
  [key: string]: any; // allow developers to add their onw extensions (e.g. req.test)
15
-
16
- // For express frameworks compatibility:
17
- query: ReqParams;
18
18
  }
19
19
 
20
20
  export interface CpeakResponse extends ServerResponse {
@@ -0,0 +1,83 @@
1
+ import type { CpeakRequest, CpeakResponse, Next } from "../types";
2
+ import { Buffer } from "node:buffer";
3
+ import { frameworkError, ErrorCode } from "../index";
4
+
5
+ // Check if Content-Type is JSON
6
+ function isJSON(contentType: string | undefined) {
7
+ if (!contentType) return false;
8
+ if (contentType === "application/json") return true;
9
+ return (
10
+ contentType.startsWith("application/json") || contentType.includes("+json")
11
+ );
12
+ }
13
+
14
+ // Parsing JSON
15
+ const parseJSON = (options: { limit?: number } = {}) => {
16
+ // Default limit to 1MB
17
+ const limit = options.limit || 1024 * 1024;
18
+
19
+ return (req: CpeakRequest, res: CpeakResponse, next: Next) => {
20
+ if (!isJSON(req.headers["content-type"])) return next();
21
+
22
+ const chunks: Buffer[] = [];
23
+ let bytesReceived = 0;
24
+
25
+ const onData = (chunk: Buffer) => {
26
+ bytesReceived += chunk.length;
27
+
28
+ // To prevent Denial of Service (DoS) attacks, enforce a maximum body size
29
+ if (bytesReceived > limit) {
30
+ // Stop listening to data
31
+ req.pause();
32
+
33
+ // Remove listeners so we don't trigger 'end' or more 'data'
34
+ req.removeListener("data", onData);
35
+ req.removeListener("end", onEnd);
36
+
37
+ next(
38
+ frameworkError(
39
+ "JSON body too large",
40
+ onData,
41
+ ErrorCode.PAYLOAD_TOO_LARGE,
42
+ 413 // HTTP 413 Payload Too Large
43
+ )
44
+ );
45
+
46
+ return;
47
+ }
48
+
49
+ chunks.push(chunk);
50
+ };
51
+
52
+ const onEnd = () => {
53
+ try {
54
+ // For better performance, we concat buffers once, then convert to string
55
+ // Optimization: If only one chunk exists, avoid the memory copy of concat
56
+ const rawBody =
57
+ chunks.length === 1
58
+ ? chunks[0].toString("utf-8")
59
+ : Buffer.concat(chunks).toString("utf-8");
60
+
61
+ // Handle empty body case
62
+ req.body = rawBody ? JSON.parse(rawBody) : {};
63
+
64
+ next();
65
+ } catch (err) {
66
+ // Handle Invalid JSON without crashing
67
+ next(
68
+ frameworkError(
69
+ "Invalid JSON format",
70
+ onEnd,
71
+ ErrorCode.INVALID_JSON,
72
+ 400 // HTTP 400 Bad Request
73
+ )
74
+ );
75
+ }
76
+ };
77
+
78
+ req.on("data", onData);
79
+ req.on("end", onEnd);
80
+ };
81
+ };
82
+
83
+ export { parseJSON };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cpeak",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "description": "A minimal and fast Node.js HTTP framework.",
5
5
  "type": "module",
6
6
  "scripts": {