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 +134 -15
- package/dist/index.d.ts +42 -0
- package/dist/index.js +261 -0
- package/dist/index.js.map +1 -0
- package/lib/index.ts +219 -0
- package/lib/types.ts +51 -0
- package/lib/utils/index.ts +5 -0
- package/lib/utils/parseJSON.ts +30 -0
- package/lib/utils/render.ts +72 -0
- package/lib/utils/{serveStatic.js → serveStatic.ts} +16 -11
- package/package.json +22 -5
- package/lib/index.js +0 -137
- package/lib/utils/index.js +0 -4
- package/lib/utils/parseJSON.js +0 -20
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
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
302
|
-
|
|
388
|
+
if (req.vars.test !== "something special") {
|
|
389
|
+
return handleErr({ status: 400, message: "an error message" });
|
|
390
|
+
}
|
|
303
391
|
|
|
304
|
-
|
|
305
|
-
|
|
392
|
+
next();
|
|
393
|
+
};
|
|
306
394
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
res.
|
|
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...
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
30
|
-
"
|
|
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;
|
package/lib/utils/index.js
DELETED
package/lib/utils/parseJSON.js
DELETED
|
@@ -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 };
|