cpeak 2.4.0 → 2.4.2

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/README.md CHANGED
@@ -8,8 +8,6 @@ This project is designed to be improved until it's ready for use in complex prod
8
8
 
9
9
  This is an educational project that was started as part of the [Understanding Node.js: Core Concepts](https://www.udemy.com/course/understanding-nodejs-core-concepts/?referralCode=0BC21AC4DD6958AE6A95) course. If you want to learn how to build a framework like this, and get to a point where you can build things like this yourself, check out this course!
10
10
 
11
- <em>This is the current demo, and the development of the project will begin starting from **September 2025.**</em>
12
-
13
11
  ## Why Cpeak?
14
12
 
15
13
  - **Minimalism**: No unnecessary bloat, with zero dependencies. Just the core essentials you need to build fast and reliable applications.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
- import { IncomingMessage, ServerResponse } from 'node:http';
1
+ import http, { IncomingMessage, ServerResponse } from 'node:http';
2
2
 
3
+ type Cpeak$1 = ReturnType<typeof cpeak>;
3
4
  type StringMap = Record<string, string>;
4
- interface CpeakRequest extends IncomingMessage {
5
- params: StringMap;
5
+ interface CpeakRequest<ReqBody = any, ReqParams = any> extends IncomingMessage {
6
+ params: ReqParams;
6
7
  vars?: StringMap;
7
- body?: unknown;
8
+ body?: ReqBody;
8
9
  [key: string]: any;
10
+ query: ReqParams;
9
11
  }
10
12
  interface CpeakResponse extends ServerResponse {
11
13
  sendFile: (path: string, mime: string) => Promise<void>;
@@ -16,15 +18,34 @@ interface CpeakResponse extends ServerResponse {
16
18
  }
17
19
  type Next = (err?: any) => void;
18
20
  type HandleErr = (err: any) => void;
19
- type Middleware = (req: CpeakRequest, res: CpeakResponse, next: Next, handleErr?: HandleErr) => void;
20
- type Handler = (req: CpeakRequest, res: CpeakResponse, handleErr: HandleErr) => void | Promise<void>;
21
-
22
- declare const parseJSON: (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
21
+ type Middleware<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, next: Next) => void;
22
+ type RouteMiddleware<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, next: Next, handleErr: HandleErr) => void | Promise<void>;
23
+ type Handler<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, handleErr: HandleErr) => void | Promise<void>;
24
+ interface Route {
25
+ path: string;
26
+ regex: RegExp;
27
+ middleware: RouteMiddleware[];
28
+ cb: Handler;
29
+ }
30
+ interface RoutesMap {
31
+ [method: string]: Route[];
32
+ }
23
33
 
24
34
  declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
25
35
 
36
+ declare const parseJSON: (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
37
+
26
38
  declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
27
39
 
40
+ declare function frameworkError(message: string, skipFn: Function, code?: string): Error & {
41
+ code?: string;
42
+ };
43
+ declare enum ErrorCode {
44
+ MISSING_MIME = "CPEAK_ERR_MISSING_MIME",
45
+ FILE_NOT_FOUND = "CPEAK_ERR_FILE_NOT_FOUND",
46
+ NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
47
+ SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL"
48
+ }
28
49
  declare class Cpeak {
29
50
  #private;
30
51
  private server;
@@ -32,11 +53,13 @@ declare class Cpeak {
32
53
  private middleware;
33
54
  private _handleErr?;
34
55
  constructor();
35
- route(method: string, path: string, ...args: (Middleware | Handler)[]): void;
56
+ route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]): void;
36
57
  beforeEach(cb: Middleware): void;
37
58
  handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void): void;
38
- listen(port: number, cb?: () => void): void;
59
+ listen(port: number, cb?: () => void): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
39
60
  close(cb?: (err?: Error) => void): void;
40
61
  }
41
62
 
42
- export { Cpeak as default, parseJSON, render, serveStatic };
63
+ declare function cpeak(): Cpeak;
64
+
65
+ export { type Cpeak$1 as Cpeak, type CpeakRequest, type CpeakResponse, ErrorCode, type HandleErr, type Handler, type Middleware, type Next, type RouteMiddleware, type RoutesMap, cpeak as default, frameworkError, parseJSON, render, serveStatic };
package/dist/index.js CHANGED
@@ -1,24 +1,8 @@
1
1
  // lib/index.ts
2
2
  import http from "http";
3
3
  import fs3 from "fs/promises";
4
-
5
- // lib/utils/parseJSON.ts
6
- var parseJSON = (req, res, next) => {
7
- function isJSON(contentType = "") {
8
- const [type] = contentType.split(";");
9
- return type.trim().toLowerCase() === "application/json" || /\+json$/i.test(type.trim());
10
- }
11
- if (!isJSON(req.headers["content-type"])) return next();
12
- let body = "";
13
- req.on("data", (chunk) => {
14
- body += chunk.toString("utf-8");
15
- });
16
- req.on("end", () => {
17
- body = JSON.parse(body);
18
- req.body = body;
19
- return next();
20
- });
21
- };
4
+ import { createReadStream } from "fs";
5
+ import { pipeline } from "stream/promises";
22
6
 
23
7
  // lib/utils/serveStatic.ts
24
8
  import fs from "fs";
@@ -81,6 +65,24 @@ var serveStatic = (folderPath, newMimeTypes) => {
81
65
  };
82
66
  };
83
67
 
68
+ // lib/utils/parseJSON.ts
69
+ var parseJSON = (req, res, next) => {
70
+ function isJSON(contentType = "") {
71
+ const [type] = contentType.split(";");
72
+ return type.trim().toLowerCase() === "application/json" || /\+json$/i.test(type.trim());
73
+ }
74
+ if (!isJSON(req.headers["content-type"])) return next();
75
+ let body = "";
76
+ req.on("data", (chunk) => {
77
+ body += chunk.toString("utf-8");
78
+ });
79
+ req.on("end", () => {
80
+ body = JSON.parse(body);
81
+ req.body = body;
82
+ return next();
83
+ });
84
+ };
85
+
84
86
  // lib/utils/render.ts
85
87
  import fs2 from "fs/promises";
86
88
  function renderTemplate(templateStr, data) {
@@ -106,8 +108,15 @@ function renderTemplate(templateStr, data) {
106
108
  return result.join("");
107
109
  }
108
110
  var render = () => {
111
+ console.log("render.ts loaded");
109
112
  return function(req, res, next) {
110
113
  res.render = async (path2, data, mime) => {
114
+ if (!mime) {
115
+ throw frameworkError(
116
+ `MIME type is missing. You called res.render("${path2}", ...) but forgot to provide the third "mime" argument.`,
117
+ res.render
118
+ );
119
+ }
111
120
  let fileStr = await fs2.readFile(path2, "utf-8");
112
121
  const finalStr = renderTemplate(fileStr, data);
113
122
  res.setHeader("Content-Type", mime);
@@ -118,6 +127,19 @@ var render = () => {
118
127
  };
119
128
 
120
129
  // lib/index.ts
130
+ function frameworkError(message, skipFn, code) {
131
+ const err = new Error(message);
132
+ Error.captureStackTrace(err, skipFn);
133
+ if (code) err.code = code;
134
+ return err;
135
+ }
136
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
137
+ ErrorCode2["MISSING_MIME"] = "CPEAK_ERR_MISSING_MIME";
138
+ ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
139
+ ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
140
+ ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
141
+ return ErrorCode2;
142
+ })(ErrorCode || {});
121
143
  var Cpeak = class {
122
144
  server;
123
145
  routes;
@@ -129,10 +151,39 @@ var Cpeak = class {
129
151
  this.middleware = [];
130
152
  this.server.on("request", (req, res) => {
131
153
  res.sendFile = async (path2, mime) => {
132
- const fileHandle = await fs3.open(path2, "r");
133
- const fileStream = fileHandle.createReadStream();
134
- res.setHeader("Content-Type", mime);
135
- fileStream.pipe(res);
154
+ if (!mime) {
155
+ throw frameworkError(
156
+ 'MIME type is missing. Use res.sendFile(path, "mime-type").',
157
+ res.sendFile,
158
+ "CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
159
+ );
160
+ }
161
+ try {
162
+ const stat = await fs3.stat(path2);
163
+ if (!stat.isFile()) {
164
+ throw frameworkError(
165
+ `Not a file: ${path2}`,
166
+ res.sendFile,
167
+ "CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
168
+ );
169
+ }
170
+ res.setHeader("Content-Type", mime);
171
+ res.setHeader("Content-Length", String(stat.size));
172
+ await pipeline(createReadStream(path2), res);
173
+ } catch (err) {
174
+ if (err?.code === "ENOENT") {
175
+ throw frameworkError(
176
+ `File not found: ${path2}`,
177
+ res.sendFile,
178
+ "CPEAK_ERR_FILE_NOT_FOUND" /* FILE_NOT_FOUND */
179
+ );
180
+ }
181
+ throw frameworkError(
182
+ `Failed to send file: ${path2}`,
183
+ res.sendFile,
184
+ "CPEAK_ERR_SEND_FILE_FAIL" /* SEND_FILE_FAIL */
185
+ );
186
+ }
136
187
  };
137
188
  res.status = (code) => {
138
189
  res.statusCode = code;
@@ -149,7 +200,9 @@ var Cpeak = class {
149
200
  };
150
201
  const urlWithoutParams = req.url?.split("?")[0];
151
202
  const params = new URLSearchParams(req.url?.split("?")[1]);
152
- req.params = Object.fromEntries(params.entries());
203
+ const paramsObject = Object.fromEntries(params.entries());
204
+ req.params = paramsObject;
205
+ req.query = paramsObject;
153
206
  const runHandler = (req2, res2, middleware, cb, index) => {
154
207
  if (index === middleware.length) {
155
208
  try {
@@ -169,19 +222,34 @@ var Cpeak = class {
169
222
  this._handleErr?.(error, req2, res2);
170
223
  }
171
224
  } else {
172
- middleware[index](
173
- req2,
174
- res2,
175
- // The next function
176
- () => {
177
- runHandler(req2, res2, middleware, cb, index + 1);
178
- },
179
- // Error handler for a route middleware
180
- (error) => {
181
- res2.setHeader("Connection", "close");
182
- this._handleErr?.(error, req2, res2);
225
+ try {
226
+ const middlewareResult = middleware[index](
227
+ req2,
228
+ res2,
229
+ // The next function
230
+ (error) => {
231
+ if (error) {
232
+ res2.setHeader("Connection", "close");
233
+ return this._handleErr?.(error, req2, res2);
234
+ }
235
+ runHandler(req2, res2, middleware, cb, index + 1);
236
+ },
237
+ // Error handler for a route middleware
238
+ (error) => {
239
+ res2.setHeader("Connection", "close");
240
+ this._handleErr?.(error, req2, res2);
241
+ }
242
+ );
243
+ if (middlewareResult && typeof middlewareResult.then === "function") {
244
+ middlewareResult.catch((error) => {
245
+ res2.setHeader("Connection", "close");
246
+ this._handleErr?.(error, req2, res2);
247
+ });
183
248
  }
184
- );
249
+ } catch (error) {
250
+ res2.setHeader("Connection", "close");
251
+ this._handleErr?.(error, req2, res2);
252
+ }
185
253
  }
186
254
  };
187
255
  const runMiddleware = (req2, res2, middleware, index) => {
@@ -223,7 +291,7 @@ var Cpeak = class {
223
291
  this._handleErr = cb;
224
292
  }
225
293
  listen(port, cb) {
226
- this.server.listen(port, cb);
294
+ return this.server.listen(port, cb);
227
295
  }
228
296
  close(cb) {
229
297
  this.server.close(cb);
@@ -251,9 +319,13 @@ var Cpeak = class {
251
319
  return vars;
252
320
  }
253
321
  };
254
- var index_default = Cpeak;
322
+ function cpeak() {
323
+ return new Cpeak();
324
+ }
255
325
  export {
256
- index_default as default,
326
+ ErrorCode,
327
+ cpeak as default,
328
+ frameworkError,
257
329
  parseJSON,
258
330
  render,
259
331
  serveStatic
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../lib/index.ts","../lib/utils/parseJSON.ts","../lib/utils/serveStatic.ts","../lib/utils/render.ts"],"sourcesContent":["import http, { IncomingMessage, ServerResponse } from \"node:http\";\nimport fs from \"node:fs/promises\";\n\nimport { serveStatic, parseJSON, render } from \"./utils\";\n\nimport type {\n StringMap,\n CpeakRequest,\n CpeakResponse,\n Middleware,\n Handler,\n RoutesMap,\n} from \"./types\";\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 const fileHandle = await fs.open(path, \"r\");\n const fileStream = fileHandle.createReadStream();\n\n res.setHeader(\"Content-Type\", mime);\n\n fileStream.pipe(res);\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 req.params = Object.fromEntries(params.entries());\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: Middleware[],\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 middleware[index](\n req,\n res,\n // The next function\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 };\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: (Middleware | Handler)[]) {\n if (!this.routes[method]) this.routes[method] = [];\n\n // The last argument should always be our handler\n const cb = args.pop();\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 Middleware[];\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 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\nexport { serveStatic, parseJSON, render };\n\nexport default Cpeak;\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\";\nimport path from \"node:path\";\n\nimport type { StringMap, CpeakRequest, CpeakResponse, Next } from \"../types.js\";\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 fs from \"node:fs/promises\";\nimport type { CpeakRequest, CpeakResponse, Next } from \"../types.js\";\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 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":";AAAA,OAAO,UAA+C;AACtD,OAAOA,SAAQ;;;ACEf,IAAM,YAAY,CAAC,KAAmB,KAAoB,SAAe;AAGvE,WAAS,OAAO,cAAsB,IAAI;AAExC,UAAM,CAAC,IAAI,IAAI,YAAY,MAAM,GAAG;AACpC,WACE,KAAK,KAAK,EAAE,YAAY,MAAM,sBAC9B,WAAW,KAAK,KAAK,KAAK,CAAC;AAAA,EAE/B;AAEA,MAAI,CAAC,OAAO,IAAI,QAAQ,cAAc,CAAW,EAAG,QAAO,KAAK;AAEhE,MAAI,OAAO;AACX,MAAI,GAAG,QAAQ,CAAC,UAAkB;AAChC,YAAQ,MAAM,SAAS,OAAO;AAAA,EAChC,CAAC;AAED,MAAI,GAAG,OAAO,MAAM;AAClB,WAAO,KAAK,MAAM,IAAI;AACtB,QAAI,OAAO;AACX,WAAO,KAAK;AAAA,EACd,CAAC;AACH;;;AC3BA,OAAO,QAAQ;AACf,OAAO,UAAU;AAIjB,IAAM,aAAwB;AAAA,EAC5B,MAAM;AAAA,EACN,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AACT;AAEA,IAAM,cAAc,CAAC,YAAoB,iBAA6B;AAEpE,MAAI,cAAc;AAChB,WAAO,OAAO,YAAY,YAAY;AAAA,EACxC;AAEA,WAAS,cAAcC,aAAoB,cAAsB;AAC/D,UAAM,cAAwB,CAAC;AAG/B,UAAM,QAAQ,GAAG,YAAYA,WAAU;AAGvC,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,KAAK,KAAKA,aAAY,IAAI;AAG3C,UAAI,GAAG,SAAS,QAAQ,EAAE,YAAY,GAAG;AAEvC,cAAM,iBAAiB,cAAc,UAAU,YAAY;AAC3D,oBAAY,KAAK,GAAG,cAAc;AAAA,MACpC,OAAO;AAEL,cAAM,eAAe,KAAK,SAAS,cAAc,QAAQ;AACzD,cAAM,gBAAgB,KAAK,QAAQ,IAAI,EAAE,MAAM,CAAC;AAChD,YAAI,WAAW,aAAa,EAAG,aAAY,KAAK,MAAM,YAAY;AAAA,MACpE;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,uBAAuB,CAAC,eAAyB;AACrD,UAAMC,YAA2D,CAAC;AAClE,eAAW,QAAQ,YAAY;AAC7B,YAAM,gBAAgB,KAAK,QAAQ,IAAI,EAAE,MAAM,CAAC;AAChD,MAAAA,UAAS,IAAI,IAAI;AAAA,QACf,MAAM,aAAa;AAAA,QACnB,MAAM,WAAW,aAAa;AAAA,MAChC;AAAA,IACF;AACA,WAAOA;AAAA,EACT;AAGA,QAAM,WAAW,qBAAqB,cAAc,YAAY,UAAU,CAAC;AAE3E,SAAO,SAAU,KAAmB,KAAoB,MAAY;AAClE,UAAM,MAAM,IAAI;AAChB,QAAI,OAAO,QAAQ,SAAU,QAAO,KAAK;AAEzC,QAAI,OAAO,UAAU,eAAe,KAAK,UAAU,GAAG,GAAG;AACvD,YAAM,YAAY,SAAS,GAAG;AAC9B,aAAO,IAAI,SAAS,UAAU,MAAM,UAAU,IAAI;AAAA,IACpD;AAEA,SAAK;AAAA,EACP;AACF;;;AC/EA,OAAOC,SAAQ;AAGf,SAAS,eACP,aACA,MACQ;AAER,MAAI,SAA+B,CAAC;AAEpC,MAAI,eAAe;AAEnB,SAAO,eAAe,YAAY,QAAQ;AAExC,UAAM,WAAW,YAAY,QAAQ,MAAM,YAAY;AACvD,QAAI,aAAa,IAAI;AAEnB,aAAO,KAAK,YAAY,MAAM,YAAY,CAAC;AAC3C;AAAA,IACF;AAGA,WAAO,KAAK,YAAY,MAAM,cAAc,QAAQ,CAAC;AAGrD,UAAM,SAAS,YAAY,QAAQ,MAAM,QAAQ;AACjD,QAAI,WAAW,IAAI;AAEjB,aAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC;AAAA,IACF;AAGA,UAAM,UAAU,YAAY,MAAM,WAAW,GAAG,MAAM,EAAE,KAAK;AAG7D,UAAM,cAAc,KAAK,OAAO,MAAM,SAAY,KAAK,OAAO,IAAI;AAGlE,WAAO,KAAK,WAAW;AAGvB,mBAAe,SAAS;AAAA,EAC1B;AAGA,SAAO,OAAO,KAAK,EAAE;AACvB;AAOA,IAAM,SAAS,MAAM;AACnB,SAAO,SAAU,KAAmB,KAAoB,MAAkB;AACxE,QAAI,SAAS,OACXC,OACA,MACA,SACG;AACH,UAAI,UAAU,MAAMD,IAAG,SAASC,OAAM,OAAO;AAC7C,YAAM,WAAW,eAAe,SAAS,IAAI;AAC7C,UAAI,UAAU,gBAAgB,IAAI;AAClC,UAAI,IAAI,QAAQ;AAAA,IAClB;AAEA,SAAK;AAAA,EACP;AACF;;;AHvDA,IAAM,QAAN,MAAY;AAAA,EACF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAMR,cAAc;AACZ,SAAK,SAAS,KAAK,aAAa;AAChC,SAAK,SAAS,CAAC;AACf,SAAK,aAAa,CAAC;AAEnB,SAAK,OAAO,GAAG,WAAW,CAAC,KAAmB,QAAuB;AAEnE,UAAI,WAAW,OAAOC,OAAc,SAAiB;AACnD,cAAM,aAAa,MAAMC,IAAG,KAAKD,OAAM,GAAG;AAC1C,cAAM,aAAa,WAAW,iBAAiB;AAE/C,YAAI,UAAU,gBAAgB,IAAI;AAElC,mBAAW,KAAK,GAAG;AAAA,MACrB;AAGA,UAAI,SAAS,CAAC,SAAiB;AAC7B,YAAI,aAAa;AACjB,eAAO;AAAA,MACT;AAGA,UAAI,WAAW,CAAC,aAAqB;AACnC,YAAI,UAAU,KAAK,EAAE,UAAU,SAAS,CAAC;AACzC,YAAI,IAAI;AACR,eAAO;AAAA,MACT;AAGA,UAAI,OAAO,CAAC,SAAc;AAExB,YAAI,UAAU,gBAAgB,kBAAkB;AAChD,YAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,MAC9B;AAGA,YAAM,mBAAmB,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC;AAI9C,YAAM,SAAS,IAAI,gBAAgB,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC;AACzD,UAAI,SAAS,OAAO,YAAY,OAAO,QAAQ,CAAC;AAGhD,YAAM,aAAa,CACjBE,MACAC,MACA,YACA,IACA,UACG;AAEH,YAAI,UAAU,WAAW,QAAQ;AAG/B,cAAI;AACF,kBAAM,gBAAgB,GAAGD,MAAKC,MAAK,CAAC,UAAU;AAC5C,cAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,mBAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,YACnC,CAAC;AAED,gBAAI,iBAAiB,OAAO,cAAc,SAAS,YAAY;AAC7D,4BAAc,MAAM,CAAC,UAAU;AAC7B,gBAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,qBAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,cACnC,CAAC;AAAA,YACH;AAEA,mBAAO;AAAA,UACT,SAAS,OAAO;AACd,YAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,iBAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,UACnC;AAAA,QACF,OAAO;AACL,qBAAW,KAAK;AAAA,YACdD;AAAA,YACAC;AAAA;AAAA,YAEA,MAAM;AACJ,yBAAWD,MAAKC,MAAK,YAAY,IAAI,QAAQ,CAAC;AAAA,YAChD;AAAA;AAAA,YAEA,CAAC,UAAU;AACT,cAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,mBAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,YACnC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAGA,YAAM,gBAAgB,CACpBD,MACAC,MACA,YACA,UACG;AAEH,YAAI,UAAU,WAAW,QAAQ;AAC/B,gBAAM,SAAS,KAAK,OAAOD,KAAI,QAAQ,YAAY,KAAK,EAAE;AAC1D,cAAI,UAAU,OAAO,OAAO,OAAO,QAAQ,MAAM;AAC/C,uBAAW,SAAS,QAAQ;AAC1B,oBAAM,QAAQ,kBAAkB,MAAM,MAAM,KAAK;AAEjD,kBAAI,OAAO;AAET,sBAAM,OAAO,KAAK,aAAa,MAAM,MAAM,KAAK;AAChD,gBAAAA,KAAI,OAAO;AAEX,uBAAO,WAAWA,MAAKC,MAAK,MAAM,YAAY,MAAM,IAAI,CAAC;AAAA,cAC3D;AAAA,YACF;AAGF,iBAAOA,KACJ,OAAO,GAAG,EACV,KAAK,EAAE,OAAO,UAAUD,KAAI,MAAM,IAAI,gBAAgB,GAAG,CAAC;AAAA,QAC/D,OAAO;AACL,qBAAW,KAAK,EAAEA,MAAKC,MAAK,MAAM;AAChC,0BAAcD,MAAKC,MAAK,YAAY,QAAQ,CAAC;AAAA,UAC/C,CAAC;AAAA,QACH;AAAA,MACF;AAEA,oBAAc,KAAK,KAAK,KAAK,YAAY,CAAC;AAAA,IAC5C,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAgBH,UAAiB,MAAgC;AACrE,QAAI,CAAC,KAAK,OAAO,MAAM,EAAG,MAAK,OAAO,MAAM,IAAI,CAAC;AAGjD,UAAM,KAAK,KAAK,IAAI;AAEpB,QAAI,CAAC,MAAM,OAAO,OAAO,YAAY;AACnC,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAGA,UAAM,aAAa,KAAK,KAAK;AAE7B,UAAM,QAAQ,KAAK,aAAaA,KAAI;AACpC,SAAK,OAAO,MAAM,EAAE,KAAK,EAAE,MAAAA,OAAM,OAAO,YAAY,GAAG,CAAC;AAAA,EAC1D;AAAA,EAEA,WAAW,IAAgB;AACzB,SAAK,WAAW,KAAK,EAAE;AAAA,EACzB;AAAA,EAEA,UAAU,IAAmE;AAC3E,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,OAAO,MAAc,IAAiB;AACpC,SAAK,OAAO,OAAO,MAAM,EAAE;AAAA,EAC7B;AAAA,EAEA,MAAM,IAA4B;AAChC,SAAK,OAAO,MAAM,EAAE;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,aAAaA,OAAc;AACzB,UAAM,WAAqB,CAAC;AAC5B,UAAM,cACJ,MACAA,MAAK,QAAQ,SAAS,CAAC,OAAO,WAAW;AACvC,eAAS,KAAK,MAAM,MAAM,CAAC,CAAC;AAC5B,aAAO;AAAA,IACT,CAAC,IACD;AAEF,UAAM,QAAQ,IAAI,OAAO,WAAW;AACpC,WAAO;AAAA,EACT;AAAA,EAEA,aAAaA,OAAc,OAAyB;AAElD,UAAM,YAAYA,MAAK,MAAM,OAAO,KAAK,CAAC,GAAG;AAAA,MAAI,CAAC,aAChD,SAAS,MAAM,CAAC;AAAA,IAClB;AACA,UAAM,OAAkB,CAAC;AACzB,aAAS,QAAQ,CAAC,MAAM,UAAU;AAChC,WAAK,IAAI,IAAI,MAAM,QAAQ,CAAC;AAAA,IAC9B,CAAC;AACD,WAAO;AAAA,EACT;AACF;AAIA,IAAO,gBAAQ;","names":["fs","folderPath","filesMap","fs","path","path","fs","req","res"]}
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"]}
package/lib/index.ts CHANGED
@@ -1,17 +1,39 @@
1
- import http, { IncomingMessage, ServerResponse } from "node:http";
1
+ import http from "node:http";
2
2
  import fs from "node:fs/promises";
3
-
4
- import { serveStatic, parseJSON, render } from "./utils";
3
+ import { createReadStream } from "node:fs";
4
+ import { pipeline } from "node:stream/promises";
5
5
 
6
6
  import type {
7
7
  StringMap,
8
8
  CpeakRequest,
9
9
  CpeakResponse,
10
10
  Middleware,
11
+ RouteMiddleware,
11
12
  Handler,
12
13
  RoutesMap,
13
14
  } from "./types";
14
15
 
16
+ // A utility function to create an error with a custom stack trace
17
+ export function frameworkError(
18
+ message: string,
19
+ skipFn: Function,
20
+ code?: string
21
+ ) {
22
+ const err = new Error(message) as Error & { code?: string };
23
+ Error.captureStackTrace(err, skipFn);
24
+
25
+ if (code) err.code = code;
26
+
27
+ return err;
28
+ }
29
+
30
+ export enum ErrorCode {
31
+ MISSING_MIME = "CPEAK_ERR_MISSING_MIME",
32
+ FILE_NOT_FOUND = "CPEAK_ERR_FILE_NOT_FOUND",
33
+ NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
34
+ SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
35
+ }
36
+
15
37
  class Cpeak {
16
38
  private server: http.Server;
17
39
  private routes: RoutesMap;
@@ -30,12 +52,44 @@ class Cpeak {
30
52
  this.server.on("request", (req: CpeakRequest, res: CpeakResponse) => {
31
53
  // Send a file back to the client
32
54
  res.sendFile = async (path: string, mime: string) => {
33
- const fileHandle = await fs.open(path, "r");
34
- const fileStream = fileHandle.createReadStream();
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
+ }
35
72
 
36
- res.setHeader("Content-Type", mime);
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
+ }
37
86
 
38
- fileStream.pipe(res);
87
+ throw frameworkError(
88
+ `Failed to send file: ${path}`,
89
+ res.sendFile,
90
+ ErrorCode.SEND_FILE_FAIL
91
+ );
92
+ }
39
93
  };
40
94
 
41
95
  // Set the status code of the response
@@ -64,13 +118,17 @@ class Cpeak {
64
118
  // Parse the URL parameters (like /users?key1=value1&key2=value2)
65
119
  // We put this here to also parse them for all the middleware functions
66
120
  const params = new URLSearchParams(req.url?.split("?")[1]);
67
- req.params = Object.fromEntries(params.entries());
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
68
126
 
69
127
  // Run all the specific middleware functions for that router only and then run the handler
70
128
  const runHandler = (
71
129
  req: CpeakRequest,
72
130
  res: CpeakResponse,
73
- middleware: Middleware[],
131
+ middleware: RouteMiddleware[],
74
132
  cb: Handler,
75
133
  index: number
76
134
  ) => {
@@ -97,19 +155,41 @@ class Cpeak {
97
155
  this._handleErr?.(error, req, res);
98
156
  }
99
157
  } else {
100
- middleware[index](
101
- req,
102
- res,
103
- // The next function
104
- () => {
105
- runHandler(req, res, middleware, cb, index + 1);
106
- },
107
- // Error handler for a route middleware
108
- (error) => {
109
- res.setHeader("Connection", "close");
110
- this._handleErr?.(error, req, res);
158
+ // 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
+ try {
160
+ const middlewareResult = middleware[index](
161
+ req,
162
+ res,
163
+ // The next function
164
+ (error) => {
165
+ // this function only accepts an error argument to be more compatible with NPM modules that are built for express
166
+ if (error) {
167
+ res.setHeader("Connection", "close");
168
+ return this._handleErr?.(error, req, res);
169
+ }
170
+ runHandler(req, res, middleware, cb, index + 1);
171
+ },
172
+ // Error handler for a route middleware
173
+ (error) => {
174
+ res.setHeader("Connection", "close");
175
+ this._handleErr?.(error, req, res);
176
+ }
177
+ );
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
+ });
111
188
  }
112
- );
189
+ } catch (error) {
190
+ res.setHeader("Connection", "close");
191
+ this._handleErr?.(error, req, res);
192
+ }
113
193
  }
114
194
  };
115
195
 
@@ -151,18 +231,18 @@ class Cpeak {
151
231
  });
152
232
  }
153
233
 
154
- route(method: string, path: string, ...args: (Middleware | Handler)[]) {
234
+ route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]) {
155
235
  if (!this.routes[method]) this.routes[method] = [];
156
236
 
157
237
  // The last argument should always be our handler
158
- const cb = args.pop();
238
+ const cb = args.pop() as Handler;
159
239
 
160
240
  if (!cb || typeof cb !== "function") {
161
241
  throw new Error("Route definition must include a handler");
162
242
  }
163
243
 
164
244
  // Rest will be our middleware functions
165
- const middleware = args.flat() as Middleware[];
245
+ const middleware = args.flat() as RouteMiddleware[];
166
246
 
167
247
  const regex = this.#pathToRegex(path);
168
248
  this.routes[method].push({ path, regex, middleware, cb });
@@ -177,7 +257,7 @@ class Cpeak {
177
257
  }
178
258
 
179
259
  listen(port: number, cb?: () => void) {
180
- this.server.listen(port, cb);
260
+ return this.server.listen(port, cb);
181
261
  }
182
262
 
183
263
  close(cb?: (err?: Error) => void) {
@@ -214,6 +294,23 @@ class Cpeak {
214
294
  }
215
295
  }
216
296
 
217
- export { serveStatic, parseJSON, render };
297
+ // Util functions
298
+ export { serveStatic } from "./utils/serveStatic.js";
299
+ export { parseJSON } from "./utils/parseJSON.js";
300
+ export { render } from "./utils/render.js";
218
301
 
219
- export default Cpeak;
302
+ export type {
303
+ Cpeak,
304
+ CpeakRequest,
305
+ CpeakResponse,
306
+ Next,
307
+ HandleErr,
308
+ Middleware,
309
+ RouteMiddleware,
310
+ Handler,
311
+ RoutesMap,
312
+ } from "./types";
313
+
314
+ export default function cpeak() {
315
+ return new Cpeak();
316
+ }
package/lib/types.ts CHANGED
@@ -1,13 +1,20 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
+ import cpeak from "./index";
3
+
4
+ export type Cpeak = ReturnType<typeof cpeak>;
2
5
 
3
6
  // Extending Node.js's Request and Response objects to add our custom properties
4
7
  export type StringMap = Record<string, string>;
5
8
 
6
- export interface CpeakRequest extends IncomingMessage {
7
- params: StringMap;
9
+ export interface CpeakRequest<ReqBody = any, ReqParams = any>
10
+ extends IncomingMessage {
11
+ params: ReqParams;
8
12
  vars?: StringMap;
9
- body?: unknown;
13
+ body?: ReqBody;
10
14
  [key: string]: any; // allow developers to add their onw extensions (e.g. req.test)
15
+
16
+ // For express frameworks compatibility:
17
+ query: ReqParams;
11
18
  }
12
19
 
13
20
  export interface CpeakResponse extends ServerResponse {
@@ -22,17 +29,23 @@ export type Next = (err?: any) => void;
22
29
  export type HandleErr = (err: any) => void;
23
30
 
24
31
  // beforeEach middleware: (req, res, next)
32
+ export type Middleware<ReqBody = any, ReqParams = any> = (
33
+ req: CpeakRequest<ReqBody, ReqParams>,
34
+ res: CpeakResponse,
35
+ next: Next
36
+ ) => void;
37
+
25
38
  // Route middleware: (req, res, next, handleErr)
26
- export type Middleware = (
27
- req: CpeakRequest,
39
+ export type RouteMiddleware<ReqBody = any, ReqParams = any> = (
40
+ req: CpeakRequest<ReqBody, ReqParams>,
28
41
  res: CpeakResponse,
29
42
  next: Next,
30
- handleErr?: HandleErr
31
- ) => void;
43
+ handleErr: HandleErr
44
+ ) => void | Promise<void>;
32
45
 
33
46
  // Route handlers: (req, res, handleErr)
34
- export type Handler = (
35
- req: CpeakRequest,
47
+ export type Handler<ReqBody = any, ReqParams = any> = (
48
+ req: CpeakRequest<ReqBody, ReqParams>,
36
49
  res: CpeakResponse,
37
50
  handleErr: HandleErr
38
51
  ) => void | Promise<void>;
@@ -41,7 +54,7 @@ export type Handler = (
41
54
  export interface Route {
42
55
  path: string;
43
56
  regex: RegExp;
44
- middleware: Middleware[];
57
+ middleware: RouteMiddleware[];
45
58
  cb: Handler;
46
59
  }
47
60
 
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
- import type { CpeakRequest, CpeakResponse, Next } from "../types.js";
2
+ import { frameworkError } from "../";
3
+ import type { CpeakRequest, CpeakResponse, Next } from "../types";
3
4
 
4
5
  function renderTemplate(
5
6
  templateStr: string,
@@ -59,6 +60,14 @@ const render = () => {
59
60
  data: Record<string, unknown>,
60
61
  mime: string
61
62
  ) => {
63
+ // check if mime is specified, if not return an error
64
+ if (!mime) {
65
+ throw frameworkError(
66
+ `MIME type is missing. You called res.render("${path}", ...) but forgot to provide the third "mime" argument.`,
67
+ res.render
68
+ );
69
+ }
70
+
62
71
  let fileStr = await fs.readFile(path, "utf-8");
63
72
  const finalStr = renderTemplate(fileStr, data);
64
73
  res.setHeader("Content-Type", mime);
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- import type { StringMap, CpeakRequest, CpeakResponse, Next } from "../types.js";
4
+ import type { StringMap, CpeakRequest, CpeakResponse, Next } from "../types";
5
5
 
6
6
  const MIME_TYPES: StringMap = {
7
7
  html: "text/html",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cpeak",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "A minimal and fast Node.js HTTP framework.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -19,6 +19,12 @@
19
19
  "main": "./dist/index.js",
20
20
  "module": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.js",
25
+ "types": "./dist/index.d.ts"
26
+ }
27
+ },
22
28
  "files": [
23
29
  "dist",
24
30
  "lib",