cpeak 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Cpeak
2
2
 
3
+ [![npm version](https://badge.fury.io/js/cpeak.svg)](https://www.npmjs.com/package/cpeak)
4
+
3
5
  Cpeak is a minimal and fast Node.js framework inspired by Express.js.
4
6
 
5
7
  This project is designed to be improved until it's ready for use in complex production applications, aiming to be more performant and minimal than Express.js. This framework is intended for HTTP applications that primarily deal with JSON and file-based message bodies.
@@ -40,6 +42,8 @@ Ready to dive in? Install **Cpeak** via npm:
40
42
  npm install cpeak
41
43
  ```
42
44
 
45
+ Cpeak is a **pure ESM** package, and to use it, your project needs to be an ESM as well. You can learn more about that [here](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).
46
+
43
47
  ### Hello World App:
44
48
 
45
49
  ```javascript
package/lib/index.js CHANGED
@@ -37,29 +37,32 @@ class Cpeak {
37
37
  // Get the url without the URL parameters
38
38
  const urlWithoutParams = req.url.split("?")[0];
39
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
+
40
45
  // Run all the middleware functions before we run the corresponding route
41
46
  const runMiddleware = (req, res, middleware, index) => {
42
47
  // Out exit point...
43
48
  if (index === middleware.length) {
44
- for (const route of this.routes[req.method.toLowerCase()]) {
45
- const match = urlWithoutParams.match(route.regex);
46
-
47
- if (match) {
48
- // Parse the URL parameters (like /users?key1=value1&key2=value2)
49
- const params = new URLSearchParams(req.url.split("?")[1]);
50
- req.params = Object.fromEntries(params.entries());
51
-
52
- // Parse the URL variables from the matched route (like /users/:id)
53
- const vars = this.#extractVars(route.path, match);
54
- req.vars = vars;
55
-
56
- // Call the route handler with the modified req and res objects
57
- return route.cb(req, res, (error) => {
58
- res.setHeader("Connection", "close");
59
- this.handleErr(error, req, res);
60
- });
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
+ }
61
65
  }
62
- }
63
66
 
64
67
  // If the requested route dose not exist, return 404
65
68
  return res
@@ -92,9 +95,11 @@ class Cpeak {
92
95
  }
93
96
 
94
97
  listen(port, cb) {
95
- this.server.listen(port, () => {
96
- cb();
97
- });
98
+ this.server.listen(port, cb);
99
+ }
100
+
101
+ close(cb) {
102
+ this.server.close(cb);
98
103
  }
99
104
 
100
105
  // ------------------------------
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "cpeak",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "A minimal and fast Node.js HTTP framework.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "scripts": {
8
- "test": "echo \"Error: no test specified\" && exit 1"
8
+ "test": "mocha test/**/*.js"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
@@ -22,5 +22,9 @@
22
22
  "nodejs",
23
23
  "http",
24
24
  "framework"
25
- ]
25
+ ],
26
+ "devDependencies": {
27
+ "mocha": "^10.7.3",
28
+ "supertest": "^7.0.0"
29
+ }
26
30
  }
@@ -0,0 +1,40 @@
1
+ import assert from "node:assert";
2
+ import supertest from "supertest";
3
+ import cpeak from "../lib/index.js";
4
+
5
+ const PORT = 7543;
6
+ const request = supertest(`http://localhost:${PORT}`);
7
+
8
+ describe("Error handling with handleErr", function () {
9
+ let server;
10
+
11
+ before(function (done) {
12
+ server = new cpeak();
13
+
14
+ server.route("patch", "/foo/:bar", (req, res, handleErr) => {
15
+ const bar = req.vars.bar;
16
+
17
+ if (bar === "random") {
18
+ return handleErr({ status: 403, message: "an error msg" });
19
+ }
20
+
21
+ return res.status(200).json({ bar });
22
+ });
23
+
24
+ server.handleErr((error, req, res) => {
25
+ return res.status(error.status).json({ error: error.message });
26
+ });
27
+
28
+ server.listen(PORT, done);
29
+ });
30
+
31
+ after(function (done) {
32
+ server.close(done);
33
+ });
34
+
35
+ it("should get an error using the handleErr function from a router", async function () {
36
+ const res = await request.patch("/foo/random");
37
+ assert.strictEqual(res.status, 403);
38
+ assert.deepStrictEqual(res.body, { error: "an error msg" });
39
+ });
40
+ });
Binary file
@@ -0,0 +1,9 @@
1
+ /* some styles for testing... */
2
+ .my-class {
3
+ color: red;
4
+ }
5
+
6
+ .my-class-2 {
7
+ color: blue;
8
+ }
9
+
@@ -0,0 +1 @@
1
+ This is a test file.
@@ -0,0 +1,35 @@
1
+ import assert from "node:assert";
2
+ import supertest from "supertest";
3
+ import fs from "node:fs/promises";
4
+ import cpeak from "../lib/index.js";
5
+
6
+ const PORT = 7543;
7
+ const request = supertest(`http://localhost:${PORT}`);
8
+
9
+ describe("Returning files with sendFile", function () {
10
+ let server;
11
+
12
+ before(function (done) {
13
+ server = new cpeak();
14
+
15
+ server.route("get", "/file", (req, res) => {
16
+ res.status(200).sendFile("./test/files/test.txt", "text/plain");
17
+ });
18
+
19
+ server.listen(PORT, done);
20
+ });
21
+
22
+ after(function (done) {
23
+ server.close(done);
24
+ });
25
+
26
+ it("should get a file as the response with the correct MIME type", async function () {
27
+ const res = await request.get("/file");
28
+
29
+ const fileContent = await fs.readFile("./test/files/test.txt", "utf-8");
30
+
31
+ assert.strictEqual(res.status, 200);
32
+ assert.strictEqual(res.headers["content-type"], "text/plain");
33
+ assert.strictEqual(res.text, fileContent);
34
+ });
35
+ });
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert";
2
+ import supertest from "supertest";
3
+ import cpeak from "../lib/index.js";
4
+
5
+ const PORT = 7543;
6
+ const request = supertest(`http://localhost:${PORT}`);
7
+
8
+ describe("Middleware functions", function () {
9
+ let server;
10
+
11
+ before(function (done) {
12
+ server = new cpeak();
13
+
14
+ server.beforeEach((req, res, next) => {
15
+ const value = req.params.value;
16
+
17
+ if (value === "random")
18
+ return res.status(400).json({ error: "an error msg" });
19
+
20
+ next();
21
+ });
22
+
23
+ server.beforeEach((req, res, next) => {
24
+ req.foo = "text";
25
+ next();
26
+ });
27
+
28
+ server.beforeEach((req, res, next) => {
29
+ res.unauthorized = () => {
30
+ res.statusCode = 401;
31
+ return res;
32
+ };
33
+ next();
34
+ });
35
+
36
+ server.route("get", "/bar", (req, res) => {
37
+ res.status(200).json({ message: req.foo });
38
+ });
39
+
40
+ server.route("get", "/bar-more", (req, res) => {
41
+ res.unauthorized().json({});
42
+ });
43
+
44
+ server.listen(PORT, done);
45
+ });
46
+
47
+ after(function (done) {
48
+ server.close(done);
49
+ });
50
+
51
+ it("should modify the req object with a new property", async function () {
52
+ const res = await request.get("/bar");
53
+ assert.strictEqual(res.status, 200);
54
+ assert.strictEqual(res.body.message, "text");
55
+ });
56
+
57
+ it("should modify the res object with a new method", async function () {
58
+ const res = await request.get("/bar-more");
59
+ assert.strictEqual(res.status, 401);
60
+ });
61
+
62
+ it("should exit the middleware and route chain if a middleware wants to", async function () {
63
+ const res = await request.get("/bar?value=random");
64
+ assert.strictEqual(res.status, 400);
65
+ assert.strictEqual(res.body.message, undefined);
66
+ assert.deepStrictEqual(res.body, { error: "an error msg" });
67
+ });
68
+ });
@@ -0,0 +1,43 @@
1
+ import assert from "node:assert";
2
+ import supertest from "supertest";
3
+ import cpeak, { parseJSON } from "../lib/index.js";
4
+
5
+ const PORT = 7543;
6
+ const request = supertest(`http://localhost:${PORT}`);
7
+
8
+ describe("Parsing request bodies with parseJSON", function () {
9
+ let server;
10
+
11
+ before(function (done) {
12
+ server = new cpeak();
13
+
14
+ server.beforeEach(parseJSON);
15
+
16
+ server.route("post", "/do-something", (req, res) => {
17
+ res.status(205).json({ receivedData: req.body });
18
+ });
19
+
20
+ server.listen(PORT, done);
21
+ });
22
+
23
+ after(function (done) {
24
+ server.close(done);
25
+ });
26
+
27
+ it("should return the same data that was sent in request body as JSON", async function () {
28
+ const obj = {
29
+ key1: "value1",
30
+ key2: 42,
31
+ key3: {
32
+ nestedKey1: "nestedValue1",
33
+ nestedKey2: ["arrayValue1", "arrayValue2", 1000],
34
+ },
35
+ key4: true,
36
+ };
37
+
38
+ const res = await request.post("/do-something").send(obj);
39
+
40
+ assert.strictEqual(res.status, 205);
41
+ assert.deepStrictEqual(res.body.receivedData, obj);
42
+ });
43
+ });
@@ -0,0 +1,85 @@
1
+ import assert from "node:assert";
2
+ import supertest from "supertest";
3
+ import cpeak from "../lib/index.js";
4
+
5
+ const PORT = 7543;
6
+ const request = supertest(`http://localhost:${PORT}`);
7
+
8
+ describe("General route logic & URL variables and parameters", function () {
9
+ let server;
10
+
11
+ before(function (done) {
12
+ server = new cpeak();
13
+
14
+ server.route("get", "/hello", (req, res) => {
15
+ res.status(200).json({ message: "Hello, World!" });
16
+ });
17
+
18
+ server.route("get", "/document/:title/more/:another/final", (req, res) => {
19
+ const title = req.vars.title;
20
+ const another = req.vars.another;
21
+ const params = req.params;
22
+
23
+ res.status(200).json({ title, another, params });
24
+ });
25
+
26
+ server.listen(PORT, done);
27
+ });
28
+
29
+ after(function (done) {
30
+ server.close(done);
31
+ });
32
+
33
+ it("should return a simple response with no variables and parameters", async function () {
34
+ const res = await request.get("/hello");
35
+ assert.strictEqual(res.status, 200);
36
+ assert.deepStrictEqual(res.body, { message: "Hello, World!" });
37
+ });
38
+
39
+ it("should return a 404 for unknown routes", async function () {
40
+ const res = await request.get("/unknown");
41
+ assert.strictEqual(res.status, 404);
42
+ assert.deepStrictEqual(res.body, {
43
+ error: "Cannot GET /unknown",
44
+ });
45
+ });
46
+
47
+ it("should return a 404 for not handled methods", async function () {
48
+ const res = await request.patch("/random");
49
+ assert.strictEqual(res.status, 404);
50
+ assert.deepStrictEqual(res.body, {
51
+ error: "Cannot PATCH /random",
52
+ });
53
+ });
54
+
55
+ it("should return the correct URL variables and parameters", async function () {
56
+ const expectedResponseBody = {
57
+ title: "some-title",
58
+ another: "thisISsome__more-text",
59
+ params: {
60
+ filter: "comments-date",
61
+ page: "2",
62
+ sortBy: "date-desc",
63
+ tags: JSON.stringify(["nodejs", "express", "url-params"]),
64
+ author: JSON.stringify({ name: "John Doe", id: 123 }),
65
+ isPublished: "true",
66
+ metadata: JSON.stringify({ version: "1.0.0", language: "en" }),
67
+ },
68
+ };
69
+
70
+ const res = await request
71
+ .get("/document/some-title/more/thisISsome__more-text/final")
72
+ .query({
73
+ filter: "comments-date",
74
+ page: 2,
75
+ sortBy: "date-desc",
76
+ tags: JSON.stringify(["nodejs", "express", "url-params"]),
77
+ author: JSON.stringify({ name: "John Doe", id: 123 }),
78
+ isPublished: true,
79
+ metadata: JSON.stringify({ version: "1.0.0", language: "en" }),
80
+ });
81
+
82
+ assert.strictEqual(res.status, 200);
83
+ assert.deepStrictEqual(res.body, expectedResponseBody);
84
+ });
85
+ });
@@ -0,0 +1,53 @@
1
+ import assert from "node:assert";
2
+ import supertest from "supertest";
3
+ import fs from "node:fs/promises";
4
+ import cpeak, { serveStatic } from "../lib/index.js";
5
+
6
+ const PORT = 7543;
7
+ const request = supertest(`http://localhost:${PORT}`);
8
+
9
+ describe("Serving static files with serveStatic", function () {
10
+ let server;
11
+
12
+ before(function (done) {
13
+ server = new cpeak();
14
+
15
+ server.beforeEach(serveStatic("./test/files", { m4a: "audio/mp4" }));
16
+
17
+ server.listen(PORT, done);
18
+ });
19
+
20
+ after(function (done) {
21
+ server.close(done);
22
+ });
23
+
24
+ it("should return the correct file with the correct MIME type", async function () {
25
+ const textRes = await request.get("/test.txt");
26
+ const cssRes = await request.get("/styles.css");
27
+
28
+ const fileTextContent = await fs.readFile("./test/files/test.txt", "utf-8");
29
+ const fileCssContent = await fs.readFile(
30
+ "./test/files/styles.css",
31
+ "utf-8"
32
+ );
33
+
34
+ assert.strictEqual(textRes.status, 200);
35
+ assert.strictEqual(textRes.headers["content-type"], "text/plain");
36
+ assert.strictEqual(textRes.text, fileTextContent);
37
+
38
+ assert.strictEqual(cssRes.status, 200);
39
+ assert.strictEqual(cssRes.headers["content-type"], "text/css");
40
+ assert.strictEqual(cssRes.text, fileCssContent);
41
+ });
42
+
43
+ it("should return the correct file with the specified MIME type by the developer", async function () {
44
+ const res = await request.get("/audio.m4a");
45
+
46
+ // read the file as binary
47
+ const fileBuffer = await fs.readFile("./test/files/audio.m4a");
48
+
49
+ assert.strictEqual(res.status, 200);
50
+ assert.strictEqual(res.headers["content-type"], "audio/mp4");
51
+ assert.deepStrictEqual(res.body, fileBuffer);
52
+ });
53
+ });