cpeak 2.4.3 → 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/README.md CHANGED
@@ -131,7 +131,7 @@ const requireAuth = (req, res, next, handleErr) => {
131
131
  return handleErr({ status: 401, message: "Unauthorized" });
132
132
  };
133
133
 
134
- server.route("get", "/profile", requireAuth, (req, res, handleErr) => {
134
+ server.route("get", "/profile", requireAuth, (req, res) => {
135
135
  console.log(req.test); // this is a test value
136
136
  });
137
137
  ```
@@ -145,7 +145,7 @@ server.route(
145
145
  requireAuth,
146
146
  anotherFunction,
147
147
  oneMore,
148
- (req, res, handleErr) => {
148
+ (req, res) => {
149
149
  // your logic
150
150
  }
151
151
  );
@@ -165,16 +165,15 @@ First add the HTTP method name you want to handle, then the path, and finally, t
165
165
 
166
166
  ### URL Variables & Parameters
167
167
 
168
- Since in HTTP these are called URL parameters: `/path?key1=value1&key2=value2&foo=900`, in Cpeak, we also call them `params` (short for HTTP URL parameters).
169
- We can also do custom path management, and we call them `vars` (short for URL variables).
168
+ To be more consistent with the broader Node.js community and frameworks, we call the HTTP URL parameters (query strings) '**query**', and the path variables (route parameters) '**params**'.
170
169
 
171
170
  Here’s how we can read both:
172
171
 
173
172
  ```javascript
174
173
  // Imagine request URL is example.com/test/my-title/more-text?filter=newest
175
174
  server.route("patch", "/test/:title/more-text", (req, res) => {
176
- const title = req.vars.title;
177
- const filter = req.params.filter;
175
+ const title = req.params.title;
176
+ const filter = req.query.filter;
178
177
 
179
178
  console.log(title); // my-title
180
179
  console.log(filter); // newest
@@ -206,11 +205,23 @@ res.redirect("https://whatever.com");
206
205
 
207
206
  ### Error Handling
208
207
 
209
- If anywhere in your route functions or route middleware functions you want to return an error, it's cleaner to pass it to the `handleErr` function like this:
208
+ If anywhere in your route functions or route middleware functions you want to return an error, you can just throw the error and let the automatic error handler catch it:
209
+
210
+ ```javascript
211
+ server.route("get", "/api/document/:title", (req, res) => {
212
+ const title = req.params.title;
213
+
214
+ if (title.length > 500) throw { status: 400, message: "Title too long." };
215
+
216
+ // The rest of your logic...
217
+ });
218
+ ```
219
+
220
+ You can also make use of the `handleErr` callback function like this:
210
221
 
211
222
  ```javascript
212
223
  server.route("get", "/api/document/:title", (req, res, handleErr) => {
213
- const title = req.vars.title;
224
+ const title = req.params.title;
214
225
 
215
226
  if (title.length > 500)
216
227
  return handleErr({ status: 400, message: "Title too long." });
@@ -219,7 +230,7 @@ server.route("get", "/api/document/:title", (req, res, handleErr) => {
219
230
  });
220
231
  ```
221
232
 
222
- And then handle all the errors like this in the `handleErr` callback:
233
+ **Make sure** to call the `server.handleErr` and pass a function like this to have the automatic error handler work properly:
223
234
 
224
235
  ```javascript
225
236
  server.handleErr((error, req, res) => {
@@ -229,13 +240,13 @@ server.handleErr((error, req, res) => {
229
240
  // Log the unexpected errors somewhere so you can keep track of them...
230
241
  console.error(error);
231
242
  res.status(500).json({
232
- error: "Sorry, something unexpected happened on our side.",
243
+ error: "Sorry, something unexpected happened on our side."
233
244
  });
234
245
  }
235
246
  });
236
247
  ```
237
248
 
238
- The error object is the object that you passed to the `handleErr` function earlier in your routes.
249
+ _The error object is the object that you threw or passed to the `handleErr` function earlier in your routes._
239
250
 
240
251
  ### Listening
241
252
 
@@ -270,7 +281,7 @@ With this middleware function, you can automatically set a folder in your projec
270
281
  ```javascript
271
282
  server.beforeEach(
272
283
  serveStatic("./public", {
273
- mp3: "audio/mpeg",
284
+ mp3: "audio/mpeg"
274
285
  })
275
286
  );
276
287
  ```
@@ -298,7 +309,9 @@ If you have file types in your public folder that are not one of the following,
298
309
  With this middleware function, you can easily read and send JSON in HTTP message bodies in route and middleware functions. Fire it up like this:
299
310
 
300
311
  ```javascript
301
- server.beforeEach(parseJSON);
312
+ // You can pass an optional limit option to indicate the maximum
313
+ // JSON body size that your server will accept.
314
+ server.beforeEach(parseJSON({ limit: 1024 * 1024 })); // default value is 1024 * 1024 (1MB)
302
315
  ```
303
316
 
304
317
  Read and send JSON from HTTP messages like this:
@@ -333,7 +346,7 @@ server.route("get", "/", (req, res, next) => {
333
346
  "./public/index.html",
334
347
  {
335
348
  title: "Page title",
336
- name: "Allan",
349
+ name: "Allan"
337
350
  },
338
351
  "text/html"
339
352
  );
@@ -364,14 +377,14 @@ const server = cpeak();
364
377
 
365
378
  server.beforeEach(
366
379
  serveStatic("./public", {
367
- mp3: "audio/mpeg",
380
+ mp3: "audio/mpeg"
368
381
  })
369
382
  );
370
383
 
371
384
  server.beforeEach(render());
372
385
 
373
386
  // For parsing JSON bodies
374
- server.beforeEach(parseJSON);
387
+ server.beforeEach(parseJSON());
375
388
 
376
389
  // Adding custom middleware functions
377
390
  server.beforeEach((req, res, next) => {
@@ -383,7 +396,7 @@ server.beforeEach((req, res, next) => {
383
396
  const testRouteMiddleware = (req, res, next, handleErr) => {
384
397
  req.whatever = "some calculated value maybe";
385
398
 
386
- if (req.vars.test !== "something special") {
399
+ if (req.params.test !== "something special") {
387
400
  return handleErr({ status: 400, message: "an error message" });
388
401
  }
389
402
 
@@ -396,7 +409,7 @@ server.route("get", "/", (req, res, next) => {
396
409
  "<path-to-file-relative-to-cwd>",
397
410
  {
398
411
  test: "some testing value",
399
- number: "2343242",
412
+ number: "2343242"
400
413
  },
401
414
  "<mime-type>"
402
415
  );
@@ -406,28 +419,23 @@ server.route("get", "/old-url", testRouteMiddleware, (req, res, next) => {
406
419
  return res.redirect("/new-url");
407
420
  });
408
421
 
409
- server.route(
410
- "get",
411
- "/api/document/:title",
412
- testRouteMiddleware,
413
- (req, res, handleErr) => {
414
- // Reading URL variables
415
- const title = req.vars.title;
422
+ server.route("get", "/api/document/:title", testRouteMiddleware, (req, res) => {
423
+ // Reading URL variables (route parameters)
424
+ const title = req.params.title;
416
425
 
417
- // Reading URL parameters (like /users?filter=active)
418
- const filter = req.params.filter;
426
+ // Reading URL parameters (query strings) (like /users?filter=active)
427
+ const filter = req.query.filter;
419
428
 
420
- // Reading JSON request body
421
- const anything = req.body.anything;
429
+ // Reading JSON request body
430
+ const anything = req.body.anything;
422
431
 
423
- // Handling errors
424
- if (anything === "not-expected-thing")
425
- return handleErr({ status: 400, message: "Invalid property." });
432
+ // Handling errors
433
+ if (anything === "not-expected-thing")
434
+ throw { status: 400, message: "Invalid property." };
426
435
 
427
- // Sending a JSON response
428
- res.status(200).json({ message: "This is a test response" });
429
- }
430
- );
436
+ // Sending a JSON response
437
+ res.status(200).json({ message: "This is a test response" });
438
+ });
431
439
 
432
440
  // Sending a file response
433
441
  server.route("get", "/file", (req, res) => {
@@ -442,7 +450,7 @@ server.handleErr((error, req, res) => {
442
450
  } else {
443
451
  console.error(error);
444
452
  res.status(500).json({
445
- error: "Sorry, something unexpected happened from our side.",
453
+ error: "Sorry, something unexpected happened from our side."
446
454
  });
447
455
  }
448
456
  });
package/dist/index.d.ts CHANGED
@@ -2,12 +2,11 @@ import http, { IncomingMessage, ServerResponse } from 'node:http';
2
2
 
3
3
  type Cpeak$1 = ReturnType<typeof cpeak>;
4
4
  type StringMap = Record<string, string>;
5
- interface CpeakRequest<ReqBody = any, ReqParams = any> extends IncomingMessage {
6
- params: ReqParams;
7
- vars?: StringMap;
5
+ interface CpeakRequest<ReqBody = any, ReqQueries = any> extends IncomingMessage {
6
+ params: StringMap;
7
+ query: ReqQueries;
8
8
  body?: ReqBody;
9
9
  [key: string]: any;
10
- query: ReqParams;
11
10
  }
12
11
  interface CpeakResponse extends ServerResponse {
13
12
  sendFile: (path: string, mime: string) => Promise<void>;
@@ -33,18 +32,35 @@ interface RoutesMap {
33
32
 
34
33
  declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
35
34
 
36
- declare const parseJSON: (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
35
+ declare const parseJSON: (options?: {
36
+ limit?: number;
37
+ }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
37
38
 
38
39
  declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
39
40
 
40
- declare function frameworkError(message: string, skipFn: Function, code?: string): Error & {
41
+ declare function frameworkError(message: string, skipFn: Function, code?: string, status?: number): Error & {
41
42
  code?: string;
43
+ cpeak_err?: boolean;
42
44
  };
43
45
  declare enum ErrorCode {
44
46
  MISSING_MIME = "CPEAK_ERR_MISSING_MIME",
45
47
  FILE_NOT_FOUND = "CPEAK_ERR_FILE_NOT_FOUND",
46
48
  NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
47
- SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL"
49
+ SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
50
+ INVALID_JSON = "CPEAK_ERR_INVALID_JSON",
51
+ PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE"
52
+ }
53
+ declare class CpeakIncomingMessage extends http.IncomingMessage {
54
+ body: any;
55
+ params: StringMap;
56
+ private _query?;
57
+ get query(): StringMap;
58
+ }
59
+ declare class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
60
+ sendFile(path: string, mime: string): Promise<void>;
61
+ status(code: number): this;
62
+ redirect(location: string): this;
63
+ json(data: any): void;
48
64
  }
49
65
  declare class Cpeak {
50
66
  #private;
@@ -56,7 +72,7 @@ declare class Cpeak {
56
72
  route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]): void;
57
73
  beforeEach(cb: Middleware): void;
58
74
  handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void): void;
59
- listen(port: number, cb?: () => void): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
75
+ listen(port: number, cb?: () => void): http.Server<typeof CpeakIncomingMessage, typeof CpeakServerResponse>;
60
76
  close(cb?: (err?: Error) => void): void;
61
77
  }
62
78
 
package/dist/index.js CHANGED
@@ -65,22 +65,58 @@ var serveStatic = (folderPath, newMimeTypes) => {
65
65
  };
66
66
  };
67
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
- });
68
+ // lib/utils/paseJSON.ts
69
+ import { Buffer } from "buffer";
70
+ function isJSON(contentType) {
71
+ if (!contentType) return false;
72
+ if (contentType === "application/json") return true;
73
+ return contentType.startsWith("application/json") || contentType.includes("+json");
74
+ }
75
+ var parseJSON = (options = {}) => {
76
+ const limit = options.limit || 1024 * 1024;
77
+ return (req, res, next) => {
78
+ if (!isJSON(req.headers["content-type"])) return next();
79
+ const chunks = [];
80
+ let bytesReceived = 0;
81
+ const onData = (chunk) => {
82
+ bytesReceived += chunk.length;
83
+ if (bytesReceived > limit) {
84
+ req.pause();
85
+ req.removeListener("data", onData);
86
+ req.removeListener("end", onEnd);
87
+ next(
88
+ frameworkError(
89
+ "JSON body too large",
90
+ onData,
91
+ "CPEAK_ERR_PAYLOAD_TOO_LARGE" /* PAYLOAD_TOO_LARGE */,
92
+ 413
93
+ // HTTP 413 Payload Too Large
94
+ )
95
+ );
96
+ return;
97
+ }
98
+ chunks.push(chunk);
99
+ };
100
+ const onEnd = () => {
101
+ try {
102
+ const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer.concat(chunks).toString("utf-8");
103
+ req.body = rawBody ? JSON.parse(rawBody) : {};
104
+ next();
105
+ } catch (err) {
106
+ next(
107
+ frameworkError(
108
+ "Invalid JSON format",
109
+ onEnd,
110
+ "CPEAK_ERR_INVALID_JSON" /* INVALID_JSON */,
111
+ 400
112
+ // HTTP 400 Bad Request
113
+ )
114
+ );
115
+ }
116
+ };
117
+ req.on("data", onData);
118
+ req.on("end", onEnd);
119
+ };
84
120
  };
85
121
 
86
122
  // lib/utils/render.ts
@@ -108,7 +144,6 @@ function renderTemplate(templateStr, data) {
108
144
  return result.join("");
109
145
  }
110
146
  var render = () => {
111
- console.log("render.ts loaded");
112
147
  return function(req, res, next) {
113
148
  res.render = async (path2, data, mime) => {
114
149
  if (!mime) {
@@ -127,10 +162,12 @@ var render = () => {
127
162
  };
128
163
 
129
164
  // lib/index.ts
130
- function frameworkError(message, skipFn, code) {
165
+ function frameworkError(message, skipFn, code, status) {
131
166
  const err = new Error(message);
132
167
  Error.captureStackTrace(err, skipFn);
168
+ err.cpeak_err = true;
133
169
  if (code) err.code = code;
170
+ if (status) err.status = status;
134
171
  return err;
135
172
  }
136
173
  var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
@@ -138,101 +175,123 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
138
175
  ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
139
176
  ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
140
177
  ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
178
+ ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
179
+ ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
141
180
  return ErrorCode2;
142
181
  })(ErrorCode || {});
182
+ var CpeakIncomingMessage = class extends http.IncomingMessage {
183
+ // We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
184
+ body = void 0;
185
+ params = {};
186
+ _query;
187
+ // Parse the URL parameters (like /users?key1=value1&key2=value2)
188
+ // We will call this query to be more familiar with other node.js frameworks.
189
+ // This is a getter method (accessed like a property)
190
+ get query() {
191
+ if (this._query) return this._query;
192
+ const url = this.url || "";
193
+ const qIndex = url.indexOf("?");
194
+ if (qIndex === -1) {
195
+ this._query = {};
196
+ } else {
197
+ const searchParams = new URLSearchParams(url.substring(qIndex + 1));
198
+ this._query = Object.fromEntries(searchParams.entries());
199
+ }
200
+ return this._query;
201
+ }
202
+ };
203
+ var CpeakServerResponse = class extends http.ServerResponse {
204
+ // Send a file back to the client
205
+ async sendFile(path2, mime) {
206
+ if (!mime) {
207
+ throw frameworkError(
208
+ 'MIME type is missing. Use res.sendFile(path, "mime-type").',
209
+ this.sendFile,
210
+ "CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
211
+ );
212
+ }
213
+ try {
214
+ const stat = await fs3.stat(path2);
215
+ if (!stat.isFile()) {
216
+ throw frameworkError(
217
+ `Not a file: ${path2}`,
218
+ this.sendFile,
219
+ "CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
220
+ );
221
+ }
222
+ this.setHeader("Content-Type", mime);
223
+ this.setHeader("Content-Length", String(stat.size));
224
+ await pipeline(createReadStream(path2), this);
225
+ } catch (err) {
226
+ if (err?.code === "ENOENT") {
227
+ throw frameworkError(
228
+ `File not found: ${path2}`,
229
+ this.sendFile,
230
+ "CPEAK_ERR_FILE_NOT_FOUND" /* FILE_NOT_FOUND */
231
+ );
232
+ }
233
+ throw frameworkError(
234
+ `Failed to send file: ${path2}`,
235
+ this.sendFile,
236
+ "CPEAK_ERR_SEND_FILE_FAIL" /* SEND_FILE_FAIL */
237
+ );
238
+ }
239
+ }
240
+ // Set the status code of the response
241
+ status(code) {
242
+ this.statusCode = code;
243
+ return this;
244
+ }
245
+ // Redirects to a new URL
246
+ redirect(location) {
247
+ this.writeHead(302, { Location: location });
248
+ this.end();
249
+ return this;
250
+ }
251
+ // Send a json data back to the client (for small json data, less than the highWaterMark)
252
+ json(data) {
253
+ this.setHeader("Content-Type", "application/json");
254
+ this.end(JSON.stringify(data));
255
+ }
256
+ };
143
257
  var Cpeak = class {
144
258
  server;
145
259
  routes;
146
260
  middleware;
147
261
  _handleErr;
148
262
  constructor() {
149
- this.server = http.createServer();
263
+ this.server = http.createServer({
264
+ IncomingMessage: CpeakIncomingMessage,
265
+ ServerResponse: CpeakServerResponse
266
+ });
150
267
  this.routes = {};
151
268
  this.middleware = [];
152
- this.server.on("request", (req, res) => {
153
- res.sendFile = async (path2, mime) => {
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
- }
187
- };
188
- res.status = (code) => {
189
- res.statusCode = code;
190
- return res;
191
- };
192
- res.redirect = (location) => {
193
- res.writeHead(302, { Location: location });
194
- res.end();
195
- return res;
196
- };
197
- res.json = (data) => {
198
- res.setHeader("Content-Type", "application/json");
199
- res.end(JSON.stringify(data));
200
- };
201
- const urlWithoutParams = req.url?.split("?")[0];
202
- const params = new URLSearchParams(req.url?.split("?")[1]);
203
- const paramsObject = Object.fromEntries(params.entries());
204
- req.params = paramsObject;
205
- req.query = paramsObject;
206
- const runHandler = (req2, res2, middleware, cb, index) => {
269
+ this.server.on("request", async (req, res) => {
270
+ const qIndex = req.url?.indexOf("?");
271
+ const urlWithoutQueries = qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
272
+ const runHandler = async (req2, res2, middleware, cb, index) => {
207
273
  if (index === middleware.length) {
208
274
  try {
209
- const handlerResult = cb(req2, res2, (error) => {
275
+ await cb(req2, res2, (error) => {
210
276
  res2.setHeader("Connection", "close");
211
277
  this._handleErr?.(error, req2, res2);
212
278
  });
213
- if (handlerResult && typeof handlerResult.then === "function") {
214
- handlerResult.catch((error) => {
215
- res2.setHeader("Connection", "close");
216
- this._handleErr?.(error, req2, res2);
217
- });
218
- }
219
- return handlerResult;
220
279
  } catch (error) {
221
280
  res2.setHeader("Connection", "close");
222
281
  this._handleErr?.(error, req2, res2);
223
282
  }
224
283
  } else {
225
284
  try {
226
- const middlewareResult = middleware[index](
285
+ await middleware[index](
227
286
  req2,
228
287
  res2,
229
288
  // The next function
230
- (error) => {
289
+ async (error) => {
231
290
  if (error) {
232
291
  res2.setHeader("Connection", "close");
233
292
  return this._handleErr?.(error, req2, res2);
234
293
  }
235
- runHandler(req2, res2, middleware, cb, index + 1);
294
+ await runHandler(req2, res2, middleware, cb, index + 1);
236
295
  },
237
296
  // Error handler for a route middleware
238
297
  (error) => {
@@ -240,38 +299,50 @@ var Cpeak = class {
240
299
  this._handleErr?.(error, req2, res2);
241
300
  }
242
301
  );
243
- if (middlewareResult && typeof middlewareResult.then === "function") {
244
- middlewareResult.catch((error) => {
245
- res2.setHeader("Connection", "close");
246
- this._handleErr?.(error, req2, res2);
247
- });
248
- }
249
302
  } catch (error) {
250
303
  res2.setHeader("Connection", "close");
251
304
  this._handleErr?.(error, req2, res2);
252
305
  }
253
306
  }
254
307
  };
255
- const runMiddleware = (req2, res2, middleware, index) => {
308
+ const runMiddleware = async (req2, res2, middleware, index) => {
256
309
  if (index === middleware.length) {
257
310
  const routes = this.routes[req2.method?.toLowerCase() || ""];
258
311
  if (routes && typeof routes[Symbol.iterator] === "function")
259
312
  for (const route of routes) {
260
- const match = urlWithoutParams?.match(route.regex);
313
+ const match = urlWithoutQueries?.match(route.regex);
261
314
  if (match) {
262
- const vars = this.#extractVars(route.path, match);
263
- req2.vars = vars;
264
- return runHandler(req2, res2, route.middleware, route.cb, 0);
315
+ const pathVariables = this.#extractPathVariables(
316
+ route.path,
317
+ match
318
+ );
319
+ req2.params = pathVariables;
320
+ return await runHandler(
321
+ req2,
322
+ res2,
323
+ route.middleware,
324
+ route.cb,
325
+ 0
326
+ );
265
327
  }
266
328
  }
267
- return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutParams}` });
329
+ return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
268
330
  } else {
269
- middleware[index](req2, res2, () => {
270
- runMiddleware(req2, res2, middleware, index + 1);
271
- });
331
+ try {
332
+ await middleware[index](req2, res2, async (err) => {
333
+ if (err) {
334
+ res2.setHeader("Connection", "close");
335
+ return this._handleErr?.(err, req2, res2);
336
+ }
337
+ await runMiddleware(req2, res2, middleware, index + 1);
338
+ });
339
+ } catch (error) {
340
+ res2.setHeader("Connection", "close");
341
+ this._handleErr?.(error, req2, res2);
342
+ }
272
343
  }
273
344
  };
274
- runMiddleware(req, res, this.middleware, 0);
345
+ await runMiddleware(req, res, this.middleware, 0);
275
346
  });
276
347
  }
277
348
  route(method, path2, ...args) {
@@ -300,23 +371,23 @@ var Cpeak = class {
300
371
  // PRIVATE METHODS:
301
372
  // ------------------------------
302
373
  #pathToRegex(path2) {
303
- const varNames = [];
374
+ const paramNames = [];
304
375
  const regexString = "^" + path2.replace(/:\w+/g, (match, offset) => {
305
- varNames.push(match.slice(1));
376
+ paramNames.push(match.slice(1));
306
377
  return "([^/]+)";
307
378
  }) + "$";
308
379
  const regex = new RegExp(regexString);
309
380
  return regex;
310
381
  }
311
- #extractVars(path2, match) {
312
- const varNames = (path2.match(/:\w+/g) || []).map(
313
- (varParam) => varParam.slice(1)
382
+ #extractPathVariables(path2, match) {
383
+ const paramNames = (path2.match(/:\w+/g) || []).map(
384
+ (param) => param.slice(1)
314
385
  );
315
- const vars = {};
316
- varNames.forEach((name, index) => {
317
- vars[name] = match[index + 1];
386
+ const params = {};
387
+ paramNames.forEach((name, index) => {
388
+ params[name] = match[index + 1];
318
389
  });
319
- return vars;
390
+ return params;
320
391
  }
321
392
  };
322
393
  function cpeak() {
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.3",
3
+ "version": "2.5.0",
4
4
  "description": "A minimal and fast Node.js HTTP framework.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,30 +0,0 @@
1
- import type { CpeakRequest, CpeakResponse, Next } from "../types";
2
-
3
- // Parsing JSON
4
- const parseJSON = (req: CpeakRequest, res: CpeakResponse, next: Next) => {
5
- // This is only good for bodies that their size is less than the highWaterMark value
6
-
7
- function isJSON(contentType: string = "") {
8
- // Remove any params like "; charset=UTF-8"
9
- const [type] = contentType.split(";");
10
- return (
11
- type.trim().toLowerCase() === "application/json" ||
12
- /\+json$/i.test(type.trim())
13
- );
14
- }
15
-
16
- if (!isJSON(req.headers["content-type"] as string)) return next();
17
-
18
- let body = "";
19
- req.on("data", (chunk: Buffer) => {
20
- body += chunk.toString("utf-8");
21
- });
22
-
23
- req.on("end", () => {
24
- body = JSON.parse(body);
25
- req.body = body;
26
- return next();
27
- });
28
- };
29
-
30
- export { parseJSON };