cpeak 2.1.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.
@@ -30,6 +32,7 @@ This is an educational project that was started as part of the [Understanding No
30
32
  - [serveStatic](#servestatic)
31
33
  - [parseJSON](#parsejson)
32
34
  - [Complete Example](#complete-example)
35
+ - [Versioning Notice](#versioning-notice)
33
36
 
34
37
  ## Getting Started
35
38
 
@@ -39,6 +42,8 @@ Ready to dive in? Install **Cpeak** via npm:
39
42
  npm install cpeak
40
43
  ```
41
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
+
42
47
  ### Hello World App:
43
48
 
44
49
  ```javascript
@@ -132,7 +137,7 @@ Here’s how we can read both:
132
137
  // Imagine request URL is example.com/test/my-title/more-text?filter=newest
133
138
  server.route("patch", "/test/:title/more-text", (req, res) => {
134
139
  const title = req.vars.title;
135
- const filter = req.params.get("filter");
140
+ const filter = req.params.filter;
136
141
 
137
142
  console.log(title); // my-title
138
143
  console.log(filter); // newest
@@ -294,7 +299,7 @@ server.route("get", "/api/document/:title", (req, res, handleErr) => {
294
299
  const title = req.vars.title;
295
300
 
296
301
  // Reading URL parameters (like /users?filter=active)
297
- const filter = req.params.get("filter");
302
+ const filter = req.params.filter;
298
303
 
299
304
  // Reading JSON request body
300
305
  const anything = req.body.anything;
@@ -329,3 +334,18 @@ server.listen(3000, () => {
329
334
  console.log("Server has started on port 3000");
330
335
  });
331
336
  ```
337
+
338
+ ## Versioning Notice
339
+
340
+ #### Version `1.x.x`
341
+
342
+ Version `1.x.x` represents the initial release of our framework, developed during the _Understanding Node.js Core Concepts_ course. These versions laid the foundation for our project.
343
+
344
+ #### Version `2.x.x`
345
+
346
+ All version `2.x.x` releases are considered to be in active development, following the completion of the course. These versions include ongoing feature additions and API changes as we refine the framework. Frequent updates may require code changes, so version `2.x.x` is not recommended for production environments.
347
+ For new features, bug fixes, and other changes that don't break existing code, the patch version will be increased. For changes that break existing code, the minor version will be increased.
348
+
349
+ #### Version `3.x.x`
350
+
351
+ Version `3.x.x` and beyond will be our first production-ready releases. These versions are intended for stable, long-term use, with a focus on backward compatibility and minimal breaking changes.
package/lib/index.js CHANGED
@@ -34,28 +34,35 @@ class Cpeak {
34
34
  res.end(JSON.stringify(data));
35
35
  };
36
36
 
37
- // Parse the URL parameters (like /users?name=John)
37
+ // Get the url without the URL parameters
38
38
  const urlWithoutParams = req.url.split("?")[0];
39
- req.params = new URLSearchParams(req.url.split("?")[1]);
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());
40
44
 
41
45
  // Run all the middleware functions before we run the corresponding route
42
46
  const runMiddleware = (req, res, middleware, index) => {
43
47
  // Out exit point...
44
48
  if (index === middleware.length) {
45
- for (const route of this.routes[req.method.toLowerCase()]) {
46
- const match = urlWithoutParams.match(route.regex);
47
-
48
- if (match) {
49
- // Parse the URL variables from the matched route (like /users/:id)
50
- const vars = this.#extractVars(route.path, match);
51
- // Call the route handler with request and URL variables
52
- req.vars = vars;
53
- return route.cb(req, res, (error) => {
54
- res.setHeader("Connection", "close");
55
- this.handleErr(error, req, res);
56
- });
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
+ }
57
65
  }
58
- }
59
66
 
60
67
  // If the requested route dose not exist, return 404
61
68
  return res
@@ -88,9 +95,11 @@ class Cpeak {
88
95
  }
89
96
 
90
97
  listen(port, cb) {
91
- this.server.listen(port, () => {
92
- cb();
93
- });
98
+ this.server.listen(port, cb);
99
+ }
100
+
101
+ close(cb) {
102
+ this.server.close(cb);
94
103
  }
95
104
 
96
105
  // ------------------------------
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "cpeak",
3
- "version": "2.1.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
+ });