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