cpeak 2.2.5 → 2.4.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
@@ -8,6 +8,8 @@ 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
+
11
13
  ## Why Cpeak?
12
14
 
13
15
  - **Minimalism**: No unnecessary bloat, with zero dependencies. Just the core essentials you need to build fast and reliable applications.
@@ -24,13 +26,16 @@ This is an educational project that was started as part of the [Understanding No
24
26
  - [Initializing](#initializing)
25
27
  - [Middleware](#middleware)
26
28
  - [Route Handling](#route-handling)
29
+ - [Route Middleware](#route-middleware)
27
30
  - [URL Variables & Parameters](#url-variables--parameters)
28
31
  - [Sending Files](#sending-files)
32
+ - [Redirecting](#redirecting)
29
33
  - [Error Handling](#error-handling)
30
34
  - [Listening](#listening)
31
35
  - [Util Functions](#util-functions)
32
36
  - [serveStatic](#servestatic)
33
37
  - [parseJSON](#parsejson)
38
+ - [render](#render)
34
39
  - [Complete Example](#complete-example)
35
40
  - [Versioning Notice](#versioning-notice)
36
41
 
@@ -114,6 +119,40 @@ server.beforeEach((req, res, next) => {
114
119
  });
115
120
  ```
116
121
 
122
+ ### Route Middleware
123
+
124
+ You can also add middleware functions for a particular route handler like this:
125
+
126
+ ```javascript
127
+ const requireAuth = (req, res, next, handleErr) => {
128
+ // Check if user is logged in, if so then:
129
+ req.test = "this is a test value";
130
+ next();
131
+
132
+ // If user is not logged in:
133
+ return handleErr({ status: 401, message: "Unauthorized" });
134
+ };
135
+
136
+ server.route("get", "/profile", requireAuth, (req, res, handleErr) => {
137
+ console.log(req.test); // this is a test value
138
+ });
139
+ ```
140
+
141
+ You can add as many middleware functions as you want for a route:
142
+
143
+ ```javascript
144
+ server.route(
145
+ "get",
146
+ "/profile",
147
+ requireAuth,
148
+ anotherFunction,
149
+ oneMore,
150
+ (req, res, handleErr) => {
151
+ // your logic
152
+ }
153
+ );
154
+ ```
155
+
117
156
  ### Route Handling
118
157
 
119
158
  You can add new routes like this:
@@ -159,9 +198,17 @@ server.route("get", "/testing", (req, res) => {
159
198
 
160
199
  The file’s binary content will be in the HTTP response body content. Make sure you specify a correct path relative to your CWD (use the `path` module for better compatibility) and also the correct HTTP MIME type for that file.
161
200
 
201
+ ### Redirecting
202
+
203
+ If you want to redirect to a new URL, you can simply do:
204
+
205
+ ```javascript
206
+ res.redirect("https://whatever.com");
207
+ ```
208
+
162
209
  ### Error Handling
163
210
 
164
- If anywhere in your route functions you want to return an error, it's cleaner to pass it to the `handleErr` function like this:
211
+ 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:
165
212
 
166
213
  ```javascript
167
214
  server.route("get", "/api/document/:title", (req, res, handleErr) => {
@@ -210,6 +257,7 @@ The list of utility functions as of now:
210
257
 
211
258
  - serveStatic
212
259
  - parseJSON
260
+ - render
213
261
 
214
262
  Including any one of them is done like this:
215
263
 
@@ -269,12 +317,50 @@ server.route("put", "/api/user", (req, res) => {
269
317
  });
270
318
  ```
271
319
 
320
+ #### render
321
+
322
+ With this function you can do server side rendering before sending a file to a client. This can be useful for dynamic customization and search engine optimization.
323
+
324
+ First fire it up like this:
325
+
326
+ ```javascript
327
+ server.beforeEach(render());
328
+ ```
329
+
330
+ And then for rendering:
331
+
332
+ ```javascript
333
+ server.route("get", "/", (req, res, next) => {
334
+ return res.render(
335
+ "./public/index.html",
336
+ {
337
+ title: "Page title",
338
+ name: "Allan",
339
+ },
340
+ "text/html"
341
+ );
342
+ });
343
+ ```
344
+
345
+ You can then inject the variables into your file in {{ variable_name }} like this:
346
+
347
+ ```HTML
348
+ <html>
349
+ <head>
350
+ <title>{{ title }}</title>
351
+ </head>
352
+ <body>
353
+ <h1>{{ name }}</h1>
354
+ </body>
355
+ </html>
356
+ ```
357
+
272
358
  ## Complete Example
273
359
 
274
360
  Here you can see all the features that Cpeak offers, in one small piece of code:
275
361
 
276
362
  ```javascript
277
- import cpeak, { serveStatic, parseJSON } from "cpeak";
363
+ import cpeak, { serveStatic, parseJSON, render } from "cpeak";
278
364
 
279
365
  const server = new cpeak();
280
366
 
@@ -284,6 +370,8 @@ server.beforeEach(
284
370
  })
285
371
  );
286
372
 
373
+ server.beforeEach(render());
374
+
287
375
  // For parsing JSON bodies
288
376
  server.beforeEach(parseJSON);
289
377
 
@@ -293,25 +381,56 @@ server.beforeEach((req, res, next) => {
293
381
  next();
294
382
  });
295
383
 
296
- // Adding route handlers
297
- server.route("get", "/api/document/:title", (req, res, handleErr) => {
298
- // Reading URL variables
299
- const title = req.vars.title;
384
+ // A middleware function that can be specified to run before some particular routes
385
+ const testRouteMiddleware = (req, res, next, handleErr) => {
386
+ req.whatever = "some calculated value maybe";
300
387
 
301
- // Reading URL parameters (like /users?filter=active)
302
- const filter = req.params.filter;
388
+ if (req.vars.test !== "something special") {
389
+ return handleErr({ status: 400, message: "an error message" });
390
+ }
303
391
 
304
- // Reading JSON request body
305
- const anything = req.body.anything;
392
+ next();
393
+ };
306
394
 
307
- // Handling errors
308
- if (anything === "not-expected-thing")
309
- return handleErr({ status: 400, message: "Invalid property." });
395
+ // Adding route handlers
396
+ server.route("get", "/", (req, res, next) => {
397
+ return res.render(
398
+ "<path-to-file-relative-to-cwd>",
399
+ {
400
+ test: "some testing value",
401
+ number: "2343242",
402
+ },
403
+ "<mime-type>"
404
+ );
405
+ });
310
406
 
311
- // Sending a JSON response
312
- res.status(200).json({ message: "This is a test response" });
407
+ server.route("get", "/old-url", testRouteMiddleware, (req, res, next) => {
408
+ return res.redirect("/new-url");
313
409
  });
314
410
 
411
+ server.route(
412
+ "get",
413
+ "/api/document/:title",
414
+ testRouteMiddleware,
415
+ (req, res, handleErr) => {
416
+ // Reading URL variables
417
+ const title = req.vars.title;
418
+
419
+ // Reading URL parameters (like /users?filter=active)
420
+ const filter = req.params.filter;
421
+
422
+ // Reading JSON request body
423
+ const anything = req.body.anything;
424
+
425
+ // Handling errors
426
+ if (anything === "not-expected-thing")
427
+ return handleErr({ status: 400, message: "Invalid property." });
428
+
429
+ // Sending a JSON response
430
+ res.status(200).json({ message: "This is a test response" });
431
+ }
432
+ );
433
+
315
434
  // Sending a file response
316
435
  server.route("get", "/file", (req, res) => {
317
436
  // Make sure to specify a correct path and MIME type...
@@ -0,0 +1,42 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+
3
+ type StringMap = Record<string, string>;
4
+ interface CpeakRequest extends IncomingMessage {
5
+ params: StringMap;
6
+ vars?: StringMap;
7
+ body?: unknown;
8
+ [key: string]: any;
9
+ }
10
+ interface CpeakResponse extends ServerResponse {
11
+ sendFile: (path: string, mime: string) => Promise<void>;
12
+ status: (code: number) => CpeakResponse;
13
+ redirect: (location: string) => CpeakResponse;
14
+ json: (data: any) => void;
15
+ [key: string]: any;
16
+ }
17
+ type Next = (err?: any) => void;
18
+ 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;
23
+
24
+ declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
25
+
26
+ declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
27
+
28
+ declare class Cpeak {
29
+ #private;
30
+ private server;
31
+ private routes;
32
+ private middleware;
33
+ private _handleErr?;
34
+ constructor();
35
+ route(method: string, path: string, ...args: (Middleware | Handler)[]): void;
36
+ beforeEach(cb: Middleware): void;
37
+ handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void): void;
38
+ listen(port: number, cb?: () => void): void;
39
+ close(cb?: (err?: Error) => void): void;
40
+ }
41
+
42
+ export { Cpeak as default, parseJSON, render, serveStatic };
package/dist/index.js ADDED
@@ -0,0 +1,261 @@
1
+ // lib/index.ts
2
+ import http from "http";
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
+ };
22
+
23
+ // lib/utils/serveStatic.ts
24
+ import fs from "fs";
25
+ import path from "path";
26
+ var MIME_TYPES = {
27
+ html: "text/html",
28
+ css: "text/css",
29
+ js: "application/javascript",
30
+ jpg: "image/jpeg",
31
+ jpeg: "image/jpeg",
32
+ png: "image/png",
33
+ svg: "image/svg+xml",
34
+ txt: "text/plain",
35
+ eot: "application/vnd.ms-fontobject",
36
+ otf: "font/otf",
37
+ ttf: "font/ttf",
38
+ woff: "font/woff",
39
+ woff2: "font/woff2"
40
+ };
41
+ var serveStatic = (folderPath, newMimeTypes) => {
42
+ if (newMimeTypes) {
43
+ Object.assign(MIME_TYPES, newMimeTypes);
44
+ }
45
+ function processFolder(folderPath2, parentFolder) {
46
+ const staticFiles = [];
47
+ const files = fs.readdirSync(folderPath2);
48
+ for (const file of files) {
49
+ const fullPath = path.join(folderPath2, file);
50
+ if (fs.statSync(fullPath).isDirectory()) {
51
+ const subfolderFiles = processFolder(fullPath, parentFolder);
52
+ staticFiles.push(...subfolderFiles);
53
+ } else {
54
+ const relativePath = path.relative(parentFolder, fullPath);
55
+ const fileExtension = path.extname(file).slice(1);
56
+ if (MIME_TYPES[fileExtension]) staticFiles.push("/" + relativePath);
57
+ }
58
+ }
59
+ return staticFiles;
60
+ }
61
+ const filesArrayToFilesMap = (filesArray) => {
62
+ const filesMap2 = {};
63
+ for (const file of filesArray) {
64
+ const fileExtension = path.extname(file).slice(1);
65
+ filesMap2[file] = {
66
+ path: folderPath + file,
67
+ mime: MIME_TYPES[fileExtension]
68
+ };
69
+ }
70
+ return filesMap2;
71
+ };
72
+ const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));
73
+ return function(req, res, next) {
74
+ const url = req.url;
75
+ if (typeof url !== "string") return next();
76
+ if (Object.prototype.hasOwnProperty.call(filesMap, url)) {
77
+ const fileRoute = filesMap[url];
78
+ return res.sendFile(fileRoute.path, fileRoute.mime);
79
+ }
80
+ next();
81
+ };
82
+ };
83
+
84
+ // lib/utils/render.ts
85
+ import fs2 from "fs/promises";
86
+ function renderTemplate(templateStr, data) {
87
+ let result = [];
88
+ let currentIndex = 0;
89
+ while (currentIndex < templateStr.length) {
90
+ const startIdx = templateStr.indexOf("{{", currentIndex);
91
+ if (startIdx === -1) {
92
+ result.push(templateStr.slice(currentIndex));
93
+ break;
94
+ }
95
+ result.push(templateStr.slice(currentIndex, startIdx));
96
+ const endIdx = templateStr.indexOf("}}", startIdx);
97
+ if (endIdx === -1) {
98
+ result.push(templateStr.slice(startIdx));
99
+ break;
100
+ }
101
+ const varName = templateStr.slice(startIdx + 2, endIdx).trim();
102
+ const replacement = data[varName] !== void 0 ? data[varName] : "";
103
+ result.push(replacement);
104
+ currentIndex = endIdx + 2;
105
+ }
106
+ return result.join("");
107
+ }
108
+ var render = () => {
109
+ return function(req, res, next) {
110
+ res.render = async (path2, data, mime) => {
111
+ let fileStr = await fs2.readFile(path2, "utf-8");
112
+ const finalStr = renderTemplate(fileStr, data);
113
+ res.setHeader("Content-Type", mime);
114
+ res.end(finalStr);
115
+ };
116
+ next();
117
+ };
118
+ };
119
+
120
+ // lib/index.ts
121
+ var Cpeak = class {
122
+ server;
123
+ routes;
124
+ middleware;
125
+ _handleErr;
126
+ constructor() {
127
+ this.server = http.createServer();
128
+ this.routes = {};
129
+ this.middleware = [];
130
+ this.server.on("request", (req, res) => {
131
+ 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);
136
+ };
137
+ res.status = (code) => {
138
+ res.statusCode = code;
139
+ return res;
140
+ };
141
+ res.redirect = (location) => {
142
+ res.writeHead(302, { Location: location });
143
+ res.end();
144
+ return res;
145
+ };
146
+ res.json = (data) => {
147
+ res.setHeader("Content-Type", "application/json");
148
+ res.end(JSON.stringify(data));
149
+ };
150
+ const urlWithoutParams = req.url?.split("?")[0];
151
+ const params = new URLSearchParams(req.url?.split("?")[1]);
152
+ req.params = Object.fromEntries(params.entries());
153
+ const runHandler = (req2, res2, middleware, cb, index) => {
154
+ if (index === middleware.length) {
155
+ try {
156
+ const handlerResult = cb(req2, res2, (error) => {
157
+ res2.setHeader("Connection", "close");
158
+ this._handleErr?.(error, req2, res2);
159
+ });
160
+ if (handlerResult && typeof handlerResult.then === "function") {
161
+ handlerResult.catch((error) => {
162
+ res2.setHeader("Connection", "close");
163
+ this._handleErr?.(error, req2, res2);
164
+ });
165
+ }
166
+ return handlerResult;
167
+ } catch (error) {
168
+ res2.setHeader("Connection", "close");
169
+ this._handleErr?.(error, req2, res2);
170
+ }
171
+ } 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);
183
+ }
184
+ );
185
+ }
186
+ };
187
+ const runMiddleware = (req2, res2, middleware, index) => {
188
+ if (index === middleware.length) {
189
+ const routes = this.routes[req2.method?.toLowerCase() || ""];
190
+ if (routes && typeof routes[Symbol.iterator] === "function")
191
+ for (const route of routes) {
192
+ const match = urlWithoutParams?.match(route.regex);
193
+ if (match) {
194
+ const vars = this.#extractVars(route.path, match);
195
+ req2.vars = vars;
196
+ return runHandler(req2, res2, route.middleware, route.cb, 0);
197
+ }
198
+ }
199
+ return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutParams}` });
200
+ } else {
201
+ middleware[index](req2, res2, () => {
202
+ runMiddleware(req2, res2, middleware, index + 1);
203
+ });
204
+ }
205
+ };
206
+ runMiddleware(req, res, this.middleware, 0);
207
+ });
208
+ }
209
+ route(method, path2, ...args) {
210
+ if (!this.routes[method]) this.routes[method] = [];
211
+ const cb = args.pop();
212
+ if (!cb || typeof cb !== "function") {
213
+ throw new Error("Route definition must include a handler");
214
+ }
215
+ const middleware = args.flat();
216
+ const regex = this.#pathToRegex(path2);
217
+ this.routes[method].push({ path: path2, regex, middleware, cb });
218
+ }
219
+ beforeEach(cb) {
220
+ this.middleware.push(cb);
221
+ }
222
+ handleErr(cb) {
223
+ this._handleErr = cb;
224
+ }
225
+ listen(port, cb) {
226
+ this.server.listen(port, cb);
227
+ }
228
+ close(cb) {
229
+ this.server.close(cb);
230
+ }
231
+ // ------------------------------
232
+ // PRIVATE METHODS:
233
+ // ------------------------------
234
+ #pathToRegex(path2) {
235
+ const varNames = [];
236
+ const regexString = "^" + path2.replace(/:\w+/g, (match, offset) => {
237
+ varNames.push(match.slice(1));
238
+ return "([^/]+)";
239
+ }) + "$";
240
+ const regex = new RegExp(regexString);
241
+ return regex;
242
+ }
243
+ #extractVars(path2, match) {
244
+ const varNames = (path2.match(/:\w+/g) || []).map(
245
+ (varParam) => varParam.slice(1)
246
+ );
247
+ const vars = {};
248
+ varNames.forEach((name, index) => {
249
+ vars[name] = match[index + 1];
250
+ });
251
+ return vars;
252
+ }
253
+ };
254
+ var index_default = Cpeak;
255
+ export {
256
+ index_default as default,
257
+ parseJSON,
258
+ render,
259
+ serveStatic
260
+ };
261
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":"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","names":["fs","folderPath","filesMap","fs","path","path","fs","req","res"]}
package/lib/index.ts ADDED
@@ -0,0 +1,219 @@
1
+ import http, { IncomingMessage, ServerResponse } from "node:http";
2
+ import fs from "node:fs/promises";
3
+
4
+ import { serveStatic, parseJSON, render } from "./utils";
5
+
6
+ import type {
7
+ StringMap,
8
+ CpeakRequest,
9
+ CpeakResponse,
10
+ Middleware,
11
+ Handler,
12
+ RoutesMap,
13
+ } from "./types";
14
+
15
+ class Cpeak {
16
+ private server: http.Server;
17
+ private routes: RoutesMap;
18
+ private middleware: Middleware[];
19
+ private _handleErr?: (
20
+ err: unknown,
21
+ req: CpeakRequest,
22
+ res: CpeakResponse
23
+ ) => void;
24
+
25
+ constructor() {
26
+ this.server = http.createServer();
27
+ this.routes = {};
28
+ this.middleware = [];
29
+
30
+ this.server.on("request", (req: CpeakRequest, res: CpeakResponse) => {
31
+ // Send a file back to the client
32
+ res.sendFile = async (path: string, mime: string) => {
33
+ const fileHandle = await fs.open(path, "r");
34
+ const fileStream = fileHandle.createReadStream();
35
+
36
+ res.setHeader("Content-Type", mime);
37
+
38
+ fileStream.pipe(res);
39
+ };
40
+
41
+ // Set the status code of the response
42
+ res.status = (code: number) => {
43
+ res.statusCode = code;
44
+ return res;
45
+ };
46
+
47
+ // Redirects to a new URL
48
+ res.redirect = (location: string) => {
49
+ res.writeHead(302, { Location: location });
50
+ res.end();
51
+ return res;
52
+ };
53
+
54
+ // Send a json data back to the client (for small json data, less than the highWaterMark)
55
+ res.json = (data: any) => {
56
+ // This is only good for bodies that their size is less than the highWaterMark value
57
+ res.setHeader("Content-Type", "application/json");
58
+ res.end(JSON.stringify(data));
59
+ };
60
+
61
+ // Get the url without the URL parameters
62
+ const urlWithoutParams = req.url?.split("?")[0];
63
+
64
+ // Parse the URL parameters (like /users?key1=value1&key2=value2)
65
+ // We put this here to also parse them for all the middleware functions
66
+ const params = new URLSearchParams(req.url?.split("?")[1]);
67
+ req.params = Object.fromEntries(params.entries());
68
+
69
+ // Run all the specific middleware functions for that router only and then run the handler
70
+ const runHandler = (
71
+ req: CpeakRequest,
72
+ res: CpeakResponse,
73
+ middleware: Middleware[],
74
+ cb: Handler,
75
+ index: number
76
+ ) => {
77
+ // Our exit point...
78
+ if (index === middleware.length) {
79
+ // Call the route handler with the modified req and res objects.
80
+ // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.
81
+ try {
82
+ const handlerResult = cb(req, res, (error) => {
83
+ res.setHeader("Connection", "close");
84
+ this._handleErr?.(error, req, res);
85
+ });
86
+
87
+ if (handlerResult && typeof handlerResult.then === "function") {
88
+ handlerResult.catch((error) => {
89
+ res.setHeader("Connection", "close");
90
+ this._handleErr?.(error, req, res);
91
+ });
92
+ }
93
+
94
+ return handlerResult;
95
+ } catch (error) {
96
+ res.setHeader("Connection", "close");
97
+ this._handleErr?.(error, req, res);
98
+ }
99
+ } 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);
111
+ }
112
+ );
113
+ }
114
+ };
115
+
116
+ // Run all the middleware functions (beforeEach functions) before we run the corresponding route
117
+ const runMiddleware = (
118
+ req: CpeakRequest,
119
+ res: CpeakResponse,
120
+ middleware: Middleware[],
121
+ index: number
122
+ ) => {
123
+ // Our exit point...
124
+ if (index === middleware.length) {
125
+ const routes = this.routes[req.method?.toLowerCase() || ""];
126
+ if (routes && typeof routes[Symbol.iterator] === "function")
127
+ for (const route of routes) {
128
+ const match = urlWithoutParams?.match(route.regex);
129
+
130
+ if (match) {
131
+ // Parse the URL variables from the matched route (like /users/:id)
132
+ const vars = this.#extractVars(route.path, match);
133
+ req.vars = vars;
134
+
135
+ return runHandler(req, res, route.middleware, route.cb, 0);
136
+ }
137
+ }
138
+
139
+ // If the requested route dose not exist, return 404
140
+ return res
141
+ .status(404)
142
+ .json({ error: `Cannot ${req.method} ${urlWithoutParams}` });
143
+ } else {
144
+ middleware[index](req, res, () => {
145
+ runMiddleware(req, res, middleware, index + 1);
146
+ });
147
+ }
148
+ };
149
+
150
+ runMiddleware(req, res, this.middleware, 0);
151
+ });
152
+ }
153
+
154
+ route(method: string, path: string, ...args: (Middleware | Handler)[]) {
155
+ if (!this.routes[method]) this.routes[method] = [];
156
+
157
+ // The last argument should always be our handler
158
+ const cb = args.pop();
159
+
160
+ if (!cb || typeof cb !== "function") {
161
+ throw new Error("Route definition must include a handler");
162
+ }
163
+
164
+ // Rest will be our middleware functions
165
+ const middleware = args.flat() as Middleware[];
166
+
167
+ const regex = this.#pathToRegex(path);
168
+ this.routes[method].push({ path, regex, middleware, cb });
169
+ }
170
+
171
+ beforeEach(cb: Middleware) {
172
+ this.middleware.push(cb);
173
+ }
174
+
175
+ handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void) {
176
+ this._handleErr = cb;
177
+ }
178
+
179
+ listen(port: number, cb?: () => void) {
180
+ this.server.listen(port, cb);
181
+ }
182
+
183
+ close(cb?: (err?: Error) => void) {
184
+ this.server.close(cb);
185
+ }
186
+
187
+ // ------------------------------
188
+ // PRIVATE METHODS:
189
+ // ------------------------------
190
+ #pathToRegex(path: string) {
191
+ const varNames: string[] = [];
192
+ const regexString =
193
+ "^" +
194
+ path.replace(/:\w+/g, (match, offset) => {
195
+ varNames.push(match.slice(1));
196
+ return "([^/]+)";
197
+ }) +
198
+ "$";
199
+
200
+ const regex = new RegExp(regexString);
201
+ return regex;
202
+ }
203
+
204
+ #extractVars(path: string, match: RegExpMatchArray) {
205
+ // Extract url variable values from the matched route
206
+ const varNames = (path.match(/:\w+/g) || []).map((varParam) =>
207
+ varParam.slice(1)
208
+ );
209
+ const vars: StringMap = {};
210
+ varNames.forEach((name, index) => {
211
+ vars[name] = match[index + 1];
212
+ });
213
+ return vars;
214
+ }
215
+ }
216
+
217
+ export { serveStatic, parseJSON, render };
218
+
219
+ export default Cpeak;
package/lib/types.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ // Extending Node.js's Request and Response objects to add our custom properties
4
+ export type StringMap = Record<string, string>;
5
+
6
+ export interface CpeakRequest extends IncomingMessage {
7
+ params: StringMap;
8
+ vars?: StringMap;
9
+ body?: unknown;
10
+ [key: string]: any; // allow developers to add their onw extensions (e.g. req.test)
11
+ }
12
+
13
+ export interface CpeakResponse extends ServerResponse {
14
+ sendFile: (path: string, mime: string) => Promise<void>;
15
+ status: (code: number) => CpeakResponse;
16
+ redirect: (location: string) => CpeakResponse;
17
+ json: (data: any) => void;
18
+ [key: string]: any; // allow developers to add their onw extensions (e.g. res.test)
19
+ }
20
+
21
+ export type Next = (err?: any) => void;
22
+ export type HandleErr = (err: any) => void;
23
+
24
+ // beforeEach middleware: (req, res, next)
25
+ // Route middleware: (req, res, next, handleErr)
26
+ export type Middleware = (
27
+ req: CpeakRequest,
28
+ res: CpeakResponse,
29
+ next: Next,
30
+ handleErr?: HandleErr
31
+ ) => void;
32
+
33
+ // Route handlers: (req, res, handleErr)
34
+ export type Handler = (
35
+ req: CpeakRequest,
36
+ res: CpeakResponse,
37
+ handleErr: HandleErr
38
+ ) => void | Promise<void>;
39
+
40
+ // For a route object value in Cpeak.routes. The key is the method name.
41
+ export interface Route {
42
+ path: string;
43
+ regex: RegExp;
44
+ middleware: Middleware[];
45
+ cb: Handler;
46
+ }
47
+
48
+ // For Cpeak.routes:
49
+ export interface RoutesMap {
50
+ [method: string]: Route[];
51
+ }
@@ -0,0 +1,5 @@
1
+ import { parseJSON } from "./parseJSON";
2
+ import { serveStatic } from "./serveStatic";
3
+ import { render } from "./render";
4
+
5
+ export { serveStatic, parseJSON, render };
@@ -0,0 +1,30 @@
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 };
@@ -0,0 +1,72 @@
1
+ import fs from "node:fs/promises";
2
+ import type { CpeakRequest, CpeakResponse, Next } from "../types.js";
3
+
4
+ function renderTemplate(
5
+ templateStr: string,
6
+ data: Record<string, unknown>
7
+ ): string {
8
+ // Initialize variables
9
+ let result: (string | unknown)[] = [];
10
+
11
+ let currentIndex = 0;
12
+
13
+ while (currentIndex < templateStr.length) {
14
+ // Find the next opening placeholder
15
+ const startIdx = templateStr.indexOf("{{", currentIndex);
16
+ if (startIdx === -1) {
17
+ // No more placeholders, push the remaining string
18
+ result.push(templateStr.slice(currentIndex));
19
+ break;
20
+ }
21
+
22
+ // Push the part before the placeholder
23
+ result.push(templateStr.slice(currentIndex, startIdx));
24
+
25
+ // Find the closing placeholder
26
+ const endIdx = templateStr.indexOf("}}", startIdx);
27
+ if (endIdx === -1) {
28
+ // No closing brace found, treat the rest as plain text
29
+ result.push(templateStr.slice(startIdx));
30
+ break;
31
+ }
32
+
33
+ // Extract the variable name
34
+ const varName = templateStr.slice(startIdx + 2, endIdx).trim();
35
+
36
+ // Replace the variable with its value from the data, or use an empty string if not found
37
+ const replacement = data[varName] !== undefined ? data[varName] : "";
38
+
39
+ // Push the replacement to the result array
40
+ result.push(replacement);
41
+
42
+ // Move the index past the current closing placeholder
43
+ currentIndex = endIdx + 2;
44
+ }
45
+
46
+ // Join all parts into a final string
47
+ return result.join("");
48
+ }
49
+
50
+ // Errors to return: recommend to not render files larger than 100KB
51
+ // To Explore: Doing the operation in C++ and return the data as stream back to the client
52
+ // @TODO: remove the file from static map
53
+ // @TODO: escape the string to prevent XSS
54
+ // @TODO: add another {{{ }}} option to not escape the string
55
+ const render = () => {
56
+ return function (req: CpeakRequest, res: CpeakResponse, next: Next): void {
57
+ res.render = async (
58
+ path: string,
59
+ data: Record<string, unknown>,
60
+ mime: string
61
+ ) => {
62
+ let fileStr = await fs.readFile(path, "utf-8");
63
+ const finalStr = renderTemplate(fileStr, data);
64
+ res.setHeader("Content-Type", mime);
65
+ res.end(finalStr);
66
+ };
67
+
68
+ next();
69
+ };
70
+ };
71
+
72
+ export { render };
@@ -1,7 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- const MIME_TYPES = {
4
+ import type { StringMap, CpeakRequest, CpeakResponse, Next } from "../types.js";
5
+
6
+ const MIME_TYPES: StringMap = {
5
7
  html: "text/html",
6
8
  css: "text/css",
7
9
  js: "application/javascript",
@@ -17,14 +19,14 @@ const MIME_TYPES = {
17
19
  woff2: "font/woff2",
18
20
  };
19
21
 
20
- const serveStatic = (folderPath, newMimeTypes) => {
22
+ const serveStatic = (folderPath: string, newMimeTypes?: StringMap) => {
21
23
  // For new user defined mime types
22
24
  if (newMimeTypes) {
23
25
  Object.assign(MIME_TYPES, newMimeTypes);
24
26
  }
25
27
 
26
- function processFolder(folderPath, parentFolder) {
27
- const staticFiles = [];
28
+ function processFolder(folderPath: string, parentFolder: string) {
29
+ const staticFiles: string[] = [];
28
30
 
29
31
  // Read the contents of the folder
30
32
  const files = fs.readdirSync(folderPath);
@@ -49,8 +51,8 @@ const serveStatic = (folderPath, newMimeTypes) => {
49
51
  return staticFiles;
50
52
  }
51
53
 
52
- const filesArrayToFilesMap = (filesArray) => {
53
- const filesMap = {};
54
+ const filesArrayToFilesMap = (filesArray: string[]) => {
55
+ const filesMap: Record<string, { path: string; mime: string }> = {};
54
56
  for (const file of filesArray) {
55
57
  const fileExtension = path.extname(file).slice(1);
56
58
  filesMap[file] = {
@@ -64,13 +66,16 @@ const serveStatic = (folderPath, newMimeTypes) => {
64
66
  // Start processing the folder
65
67
  const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));
66
68
 
67
- return function (req, res, next) {
68
- if (filesMap.hasOwnProperty(req.url)) {
69
- const fileRoute = filesMap[req.url];
69
+ return function (req: CpeakRequest, res: CpeakResponse, next: Next) {
70
+ const url = req.url;
71
+ if (typeof url !== "string") return next();
72
+
73
+ if (Object.prototype.hasOwnProperty.call(filesMap, url)) {
74
+ const fileRoute = filesMap[url];
70
75
  return res.sendFile(fileRoute.path, fileRoute.mime);
71
- } else {
72
- next();
73
76
  }
77
+
78
+ next();
74
79
  };
75
80
  };
76
81
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "cpeak",
3
- "version": "2.2.5",
3
+ "version": "2.4.0",
4
4
  "description": "A minimal and fast Node.js HTTP framework.",
5
5
  "type": "module",
6
- "main": "./lib/index.js",
7
6
  "scripts": {
8
- "test": "mocha test/**/*.js"
7
+ "build": "tsup lib/index.ts --format esm --dts --sourcemap --out-dir dist --clean",
8
+ "dev": "tsup lib/index.ts --watch --format esm --dts --sourcemap --out-dir dist",
9
+ "test": "tsx ./node_modules/mocha/bin/_mocha --extension ts \"test/**/*.test.ts\""
9
10
  },
10
11
  "repository": {
11
12
  "type": "git",
@@ -15,6 +16,15 @@
15
16
  "url": "https://github.com/agile8118/cpeak/issues"
16
17
  },
17
18
  "homepage": "https://github.com/agile8118/cpeak#readme",
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "files": [
23
+ "dist",
24
+ "lib",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
18
28
  "author": "Cododev Technology",
19
29
  "license": "MIT",
20
30
  "keywords": [
@@ -26,7 +36,14 @@
26
36
  "framework"
27
37
  ],
28
38
  "devDependencies": {
29
- "mocha": "^10.7.3",
30
- "supertest": "^7.0.0"
39
+ "@types/mocha": "^10.0.10",
40
+ "@types/node": "^24.3.0",
41
+ "@types/supertest": "^6.0.3",
42
+ "mocha": "^10.8.2",
43
+ "supertest": "^7.1.4",
44
+ "ts-node": "^10.9.2",
45
+ "tsup": "^8.5.0",
46
+ "tsx": "^4.20.5",
47
+ "typescript": "^5.9.2"
31
48
  }
32
49
  }
package/lib/index.js DELETED
@@ -1,137 +0,0 @@
1
- import http from "node:http";
2
- import fs from "node:fs/promises";
3
-
4
- import { serveStatic, parseJSON } from "./utils/index.js";
5
-
6
- class Cpeak {
7
- constructor() {
8
- this.server = http.createServer();
9
- this.routes = {};
10
- this.middleware = [];
11
- this.handleErr;
12
-
13
- this.server.on("request", (req, res) => {
14
- // Send a file back to the client
15
- res.sendFile = async (path, mime) => {
16
- const fileHandle = await fs.open(path, "r");
17
- const fileStream = fileHandle.createReadStream();
18
-
19
- res.setHeader("Content-Type", mime);
20
-
21
- fileStream.pipe(res);
22
- };
23
-
24
- // Set the status code of the response
25
- res.status = (code) => {
26
- res.statusCode = code;
27
- return res;
28
- };
29
-
30
- // Send a json data back to the client (for small json data, less than the highWaterMark)
31
- res.json = (data) => {
32
- // This is only good for bodies that their size is less than the highWaterMark value
33
- res.setHeader("Content-Type", "application/json");
34
- res.end(JSON.stringify(data));
35
- };
36
-
37
- // Get the url without the URL parameters
38
- const urlWithoutParams = req.url.split("?")[0];
39
-
40
- // Parse the URL parameters (like /users?key1=value1&key2=value2)
41
- // We put this here to also parse them for all the middleware functions
42
- const params = new URLSearchParams(req.url.split("?")[1]);
43
- req.params = Object.fromEntries(params.entries());
44
-
45
- // Run all the middleware functions before we run the corresponding route
46
- const runMiddleware = (req, res, middleware, index) => {
47
- // Out exit point...
48
- if (index === middleware.length) {
49
- const routes = this.routes[req.method.toLowerCase()];
50
- if (routes && typeof routes[Symbol.iterator] === "function")
51
- for (const route of routes) {
52
- const match = urlWithoutParams.match(route.regex);
53
-
54
- if (match) {
55
- // Parse the URL variables from the matched route (like /users/:id)
56
- const vars = this.#extractVars(route.path, match);
57
- req.vars = vars;
58
-
59
- // Call the route handler with the modified req and res objects
60
- return route.cb(req, res, (error) => {
61
- res.setHeader("Connection", "close");
62
- this.handleErr(error, req, res);
63
- });
64
- }
65
- }
66
-
67
- // If the requested route dose not exist, return 404
68
- return res
69
- .status(404)
70
- .json({ error: `Cannot ${req.method} ${urlWithoutParams}` });
71
- } else {
72
- middleware[index](req, res, () => {
73
- runMiddleware(req, res, middleware, index + 1);
74
- });
75
- }
76
- };
77
-
78
- runMiddleware(req, res, this.middleware, 0);
79
- });
80
- }
81
-
82
- route(method, path, cb) {
83
- if (!this.routes[method]) this.routes[method] = [];
84
-
85
- const regex = this.#pathToRegex(path);
86
- this.routes[method].push({ path, regex, cb });
87
- }
88
-
89
- beforeEach(cb) {
90
- this.middleware.push(cb);
91
- }
92
-
93
- handleErr(cb) {
94
- this.handleErr = cb;
95
- }
96
-
97
- listen(port, cb) {
98
- this.server.listen(port, cb);
99
- }
100
-
101
- close(cb) {
102
- this.server.close(cb);
103
- }
104
-
105
- // ------------------------------
106
- // PRIVATE METHODS:
107
- // ------------------------------
108
- #pathToRegex(path) {
109
- const varNames = [];
110
- const regexString =
111
- "^" +
112
- path.replace(/:\w+/g, (match, offset) => {
113
- varNames.push(match.slice(1));
114
- return "([^/]+)";
115
- }) +
116
- "$";
117
-
118
- const regex = new RegExp(regexString);
119
- return regex;
120
- }
121
-
122
- #extractVars(path, match) {
123
- // Extract url variable values from the matched route
124
- const varNames = (path.match(/:\w+/g) || []).map((varParam) =>
125
- varParam.slice(1)
126
- );
127
- const vars = {};
128
- varNames.forEach((name, index) => {
129
- vars[name] = match[index + 1];
130
- });
131
- return vars;
132
- }
133
- }
134
-
135
- export { serveStatic, parseJSON };
136
-
137
- export default Cpeak;
@@ -1,4 +0,0 @@
1
- import { parseJSON } from "./parseJSON.js";
2
- import { serveStatic } from "./serveStatic.js";
3
-
4
- export { serveStatic, parseJSON };
@@ -1,20 +0,0 @@
1
- // Parsing JSON
2
- const parseJSON = (req, res, next) => {
3
- // This is only good for bodies that their size is less than the highWaterMark value
4
- if (req.headers["content-type"] === "application/json") {
5
- let body = "";
6
- req.on("data", (chunk) => {
7
- body += chunk.toString("utf-8");
8
- });
9
-
10
- req.on("end", () => {
11
- body = JSON.parse(body);
12
- req.body = body;
13
- return next();
14
- });
15
- } else {
16
- next();
17
- }
18
- };
19
-
20
- export { parseJSON };