vibe-gx 1.0.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/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nnamdi "Joe" Amaga
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ <div align="center">
2
+ <img src="./assets/vlogo.png" alt="Vibe Logo" width="180" />
3
+ <h1>Vibe</h1>
4
+ <p>
5
+ <b>A lightweight, high-performance Node.js web framework built for speed and scalability.</b>
6
+ </p>
7
+ </div>
8
+
9
+ ---
10
+
11
+ Vibe (part of the **GeNeSix** ecosystem) is a zero-dependency\* web framework with **Radix Trie routing**, **cluster mode**, **response caching**, and a **Fastify-style plugin system**.
12
+
13
+ > **Dependency Note:** The only dependency is `busboy` for multipart file parsing.
14
+
15
+ ## ⚡ Features
16
+
17
+ | Feature | Description |
18
+ | :----------------------- | :-------------------------------------------- |
19
+ | 🚀 **Radix Trie Router** | O(log n) route matching with hybrid mode |
20
+ | 🔌 **Plugin System** | Fastify-style `register()` with encapsulation |
21
+ | 🎨 **Decorators** | Extend app, request, and response |
22
+ | ⚡ **Cluster Mode** | Multi-process scaling |
23
+ | 💾 **Response Caching** | LRU cache with ETag support |
24
+ | 🔗 **Connection Pool** | Generic pool for database connections |
25
+ | 📂 **Streaming** | Stream large files without buffering |
26
+
27
+ ## 🚀 Quick Start
28
+
29
+ ```javascript
30
+ import vibe from "./vibe.js";
31
+
32
+ const app = vibe();
33
+
34
+ app.get("/", "Hello Vibe!");
35
+ app.get("/users/:id", (req, res) => ({ userId: req.params.id }));
36
+
37
+ app.listen(3000);
38
+ ```
39
+
40
+ ## 📖 Core API
41
+
42
+ ### Routes
43
+
44
+ ```javascript
45
+ app.get("/path", handler);
46
+ app.post("/path", { intercept: authMiddleware }, handler);
47
+ app.del("/path", handler); // DELETE
48
+ ```
49
+
50
+ ### Plugins (Fastify-style)
51
+
52
+ ```javascript
53
+ app.register(
54
+ async (app) => {
55
+ app.get("/status", { status: "ok" });
56
+ },
57
+ { prefix: "/api" },
58
+ );
59
+ ```
60
+
61
+ ### Decorators
62
+
63
+ ```javascript
64
+ app.decorate("config", { env: "prod" });
65
+ app.decorateRequest("user", null);
66
+ app.decorateReply("sendSuccess", function (d) {
67
+ this.success(d);
68
+ });
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 🔥 Scalability Features
74
+
75
+ ### Cluster Mode
76
+
77
+ ```javascript
78
+ import vibe, { clusterize } from "./vibe.js";
79
+
80
+ clusterize(
81
+ () => {
82
+ const app = vibe();
83
+ app.get("/", "Hello from worker!");
84
+ app.listen(3000);
85
+ },
86
+ { workers: 4, restart: true },
87
+ );
88
+ ```
89
+
90
+ ### Response Caching
91
+
92
+ ```javascript
93
+ import vibe, { LRUCache, cacheMiddleware } from "./vibe.js";
94
+
95
+ const app = vibe();
96
+ const cache = new LRUCache({ max: 1000, ttl: 60000 });
97
+
98
+ app.get("/data", { intercept: cacheMiddleware(cache) }, () => {
99
+ return { expensive: "computation" };
100
+ });
101
+ ```
102
+
103
+ ### Connection Pool
104
+
105
+ ```javascript
106
+ import vibe, { createPool } from "./vibe.js";
107
+
108
+ const dbPool = createPool({
109
+ create: async () => new DBConnection(),
110
+ destroy: async (conn) => conn.close(),
111
+ max: 10,
112
+ });
113
+
114
+ app.decorate("db", dbPool);
115
+
116
+ app.get("/users", async (req, res) => {
117
+ return await app.decorators.db.use(async (conn) => {
118
+ return conn.query("SELECT * FROM users");
119
+ });
120
+ });
121
+ ```
122
+
123
+ ### Streaming Uploads
124
+
125
+ ```javascript
126
+ app.post("/upload", { media: { streaming: true } }, (req, res) => {
127
+ req.on("file", (name, stream, info) => {
128
+ stream.pipe(fs.createWriteStream(`/uploads/${info.filename}`));
129
+ });
130
+ return { status: "uploading" };
131
+ });
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 🛠️ API Reference
137
+
138
+ ### Application
139
+
140
+ | Method | Description |
141
+ | :------------------------ | :---------------- |
142
+ | `app.listen(port)` | Start server |
143
+ | `app.register(fn, opts)` | Register plugin |
144
+ | `app.decorate(name, val)` | Add app property |
145
+ | `app.plugin(fn)` | Global middleware |
146
+
147
+ ### Request (`req`)
148
+
149
+ | Property | Description |
150
+ | :----------- | :--------------- |
151
+ | `req.params` | Route parameters |
152
+ | `req.query` | Query strings |
153
+ | `req.body` | Parsed body |
154
+ | `req.files` | Uploaded files |
155
+
156
+ ### Response (`res`)
157
+
158
+ | Method | Description |
159
+ | :------------------ | :------------ |
160
+ | `res.json(data)` | Send JSON |
161
+ | `res.send(data)` | Send response |
162
+ | `res.status(code)` | Set status |
163
+ | `res.success(data)` | 200 OK |
164
+ | `res.notFound()` | 404 |
165
+
166
+ ---
167
+
168
+ ## 📝 License
169
+
170
+ Part of the **GeNeSix** brand. Created by **Nnamdi "Joe" Amaga**. MIT License.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "vibe-gx",
3
+ "version": "1.0.1",
4
+ "description": "A lightweight, regex-based Node.js web framework built for speed and simplicity.",
5
+ "type": "module",
6
+ "main": "vibe.js",
7
+ "types": "vibe.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./vibe.js",
11
+ "types": "./vibe.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "vibe.js",
16
+ "vibe.d.ts",
17
+ "utils/"
18
+ ],
19
+ "scripts": {
20
+ "test": "echo \"No tests yet\"",
21
+ "start": "node server.js"
22
+ },
23
+ "keywords": [
24
+ "node",
25
+ "framework",
26
+ "http",
27
+ "router",
28
+ "middleware",
29
+ "web",
30
+ "backend",
31
+ "regex-router",
32
+ "express-alternative"
33
+ ],
34
+ "author": "Nnamdi \"Joe\" Amaga",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "busboy": "^1.6.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "devDependencies": {
43
+ "express": "^5.2.1",
44
+ "fastify": "^5.7.4"
45
+ }
46
+ }
@@ -0,0 +1,219 @@
1
+ import os from "os";
2
+ import { color } from "../helpers/colors.js";
3
+
4
+ /**
5
+ * Parses query string from URL into an object.
6
+ * @param {string} url
7
+ * @returns {Object}
8
+ */
9
+ export function extractQuery(url) {
10
+ const query = {};
11
+ if (!url.includes("?")) return query;
12
+ for (const rq of url.split("?")[1].split("&")) {
13
+ const parts = rq.split("=");
14
+ if (parts.length === 2) {
15
+ query[parts[0]] = parts[1];
16
+ }
17
+ }
18
+ return query;
19
+ }
20
+
21
+ /**
22
+ * Extracts raw parameters from URL based on route definition.
23
+ * @param {string} routePath
24
+ * @param {string} requestPath
25
+ * @returns {Object}
26
+ */
27
+ export function extractParams(routePath, requestPath) {
28
+ const routeSegments = routePath.split("/").filter(Boolean);
29
+ const requestSegments = requestPath.split("/").filter(Boolean);
30
+ const params = {};
31
+
32
+ routeSegments.forEach((segment, index) => {
33
+ if (segment.startsWith(":")) {
34
+ const paramName = segment.slice(1);
35
+ params[paramName] = requestSegments[index];
36
+ }
37
+ });
38
+
39
+ return params;
40
+ }
41
+
42
+ /**
43
+ * Checks if the request URL matches the route Regex.
44
+ * @param {RegExp} pathRegex
45
+ * @param {string} requestPath
46
+ * @returns {RegExpExecArray | null}
47
+ */
48
+ export function matchPath(pathRegex, requestPath) {
49
+ return pathRegex.exec(requestPath);
50
+ }
51
+
52
+ /**
53
+ * Converts a route path string (e.g., "/users/:id") into a RegExp.
54
+ * Captures named groups for parameters.
55
+ * * @param {string} path - The path to register
56
+ * @returns {{ pathRegex: RegExp, paramKeys: string[] }}
57
+ */
58
+ export function PathToRegex(path) {
59
+ const pathSegments = path.split("/").filter(Boolean);
60
+ const paramKeys = [];
61
+
62
+ // Handle root path specially
63
+ if (pathSegments.length === 0) {
64
+ return { pathRegex: /^\/$/, paramKeys: [] };
65
+ }
66
+
67
+ let pathRegex = "^";
68
+ for (let index = 0; index < pathSegments.length; index++) {
69
+ const segment = pathSegments[index];
70
+ if (segment.startsWith(":")) {
71
+ paramKeys.push(segment.slice(1));
72
+ pathRegex += `/(?<${segment.slice(1)}>[^/]+)`;
73
+ continue;
74
+ }
75
+
76
+ if (segment === "*") {
77
+ pathRegex += "/(.*)";
78
+ continue;
79
+ }
80
+
81
+ pathRegex += `/${segment}`;
82
+ }
83
+
84
+ pathRegex += "$";
85
+ pathRegex = new RegExp(pathRegex);
86
+
87
+ return { pathRegex, paramKeys };
88
+ }
89
+
90
+ /**
91
+ * Validates if data is safe to send via HTTP (string, number, boolean, object).
92
+ * @param {any} value
93
+ * @returns {boolean}
94
+ */
95
+ export function isSendAble(value) {
96
+ return (
97
+ (value !== null && typeof value === "object") ||
98
+ typeof value === "string" ||
99
+ typeof value === "number" ||
100
+ typeof value === "boolean"
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Executes a list of interceptor functions (middleware).
106
+ * Stops execution if response is ended.
107
+ * * @param {Function | Function[]} intercept - Single function or array of functions
108
+ * @param {import("../vibe.js").VibeRequest} req
109
+ * @param {import("../vibe.js").VibeResponse} res
110
+ * @param {boolean} [isRoute=true] - Context flag for error messages
111
+ * @returns {Promise<boolean>} - Returns false if response ended, true otherwise
112
+ */
113
+ export async function runIntercept(intercept, req, res, isRoute = true) {
114
+ if (!intercept || (Array.isArray(intercept) && intercept.length === 0))
115
+ return true;
116
+
117
+ const funcs = Array.isArray(intercept) ? intercept : [intercept];
118
+
119
+ for (const func of funcs) {
120
+ if (typeof func !== "function") {
121
+ throw new Error(
122
+ `All ${isRoute ? "Route" : "Global"} intercepts must be functions`,
123
+ );
124
+ }
125
+
126
+ await func(req, res);
127
+
128
+ if (res.writableEnded) return false;
129
+ }
130
+
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * Centralized error handler for routes.
136
+ * In production, only logs error message (not full stack).
137
+ * @param {Error} error
138
+ * @param {import("../../vibe.js").VibeRequest} req
139
+ * @param {import("../../vibe.js").VibeResponse} res
140
+ */
141
+ export function handleError(error, req, res) {
142
+ const isDev = process.env.NODE_ENV !== "production";
143
+
144
+ // Log error (full stack in dev, message only in production)
145
+ if (isDev) {
146
+ console.error("[VIBE ERROR]:", error);
147
+ } else {
148
+ console.error("[VIBE ERROR]:", error.message || "Unknown error");
149
+ }
150
+
151
+ if (!res.headersSent) {
152
+ res.writeHead(500, { "content-type": "application/json" });
153
+
154
+ // Only expose error details in development
155
+ const responseBody = isDev
156
+ ? { error: "Internal Server Error", message: error.message }
157
+ : { error: "Internal Server Error" };
158
+
159
+ res.end(JSON.stringify(responseBody));
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Finds the local network IP address (IPv4)
165
+ * @param {string} host
166
+ * @param {number} port
167
+ * @returns {void}
168
+ */
169
+ export function getNetworkIP(host, port) {
170
+ const interfaces = os.networkInterfaces();
171
+ const addresses = [];
172
+
173
+ for (const name of Object.keys(interfaces)) {
174
+ addresses.push(
175
+ ...interfaces[name]
176
+ .map((iface) =>
177
+ iface.address === "::1"
178
+ ? { address: "[::1]", fam: iface.family }
179
+ : { address: iface.address, fam: iface.family },
180
+ )
181
+ .filter((addr) => !addr.address.startsWith("fe80")),
182
+ );
183
+ }
184
+
185
+ for (const addrs of addresses) {
186
+ if (host === "0.0.0.0") {
187
+ // => listens on all ipv4 hosts
188
+ if (addrs.fam === "IPv4")
189
+ log(`Server listening at - \x1b[4mhttp://${addrs.address}:${port}`);
190
+ }
191
+
192
+ if (host === "::") {
193
+ // => listens on all ipv6/ipv4 hosts
194
+ log(`Server listening at - \x1b[4mhttp://${addrs.address}:${port}`);
195
+ }
196
+
197
+ if (addrs.address === host) {
198
+ log(`Server listening at - \x1b[4mhttp://${addrs.address}:${port}`);
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Logs a message with a prefix.
205
+ * @param {string} message
206
+ */
207
+ export function log(message) {
208
+ process.stdout.write(
209
+ `${color.green("[VIBE LOG]:")} ${color.bright(message)}\n`,
210
+ );
211
+ }
212
+
213
+ /**
214
+ * Logs an error with a prefix.
215
+ * @param {string} message
216
+ */
217
+ export function error(message) {
218
+ process.stderr.write(`${color.red(`[VIBE ERROR]: ${message}`)}\n`);
219
+ }
@@ -0,0 +1,242 @@
1
+ import busboy from "busboy";
2
+ import fs from "fs";
3
+ import crypto from "crypto";
4
+ import path from "path";
5
+ import { EventEmitter } from "events";
6
+
7
+ /**
8
+ * Default streaming threshold (1MB)
9
+ */
10
+ const DEFAULT_STREAM_THRESHOLD = 1024 * 1024;
11
+
12
+ /**
13
+ * Parses incoming request bodies.
14
+ * Supports JSON and multipart/form-data (file uploads).
15
+ *
16
+ * Streaming mode: For large files, emits events instead of buffering:
17
+ * - req.emit("file", fieldName, stream, info) for each file
18
+ *
19
+ * @param {import("../vibe.js").VibeRequest} req - Incoming request
20
+ * @param {import("../vibe.js").VibeResponse} res - Response object
21
+ * @param {import("../vibe.js").MediaOptions} [media={}] - Route-specific file config
22
+ * @param {import("../vibe.js").VibeConfig} [options={}] - Global framework config
23
+ * @returns {Promise<void>} Resolves when parsing completes
24
+ */
25
+ export default function bodyParser(req, res, media = {}, options = {}) {
26
+ return new Promise((resolve, reject) => {
27
+ const contentType = req.headers["content-type"];
28
+ if (!contentType) return resolve();
29
+
30
+ req.body ||= {};
31
+ req.files ||= [];
32
+
33
+ /* ---------- Multipart / File Uploads ---------- */
34
+ if (contentType.includes("multipart/form-data")) {
35
+ parseMultipart(req, res, media, options, resolve, reject);
36
+ return;
37
+ }
38
+
39
+ /* ---------- JSON ---------- */
40
+ if (contentType.includes("application/json")) {
41
+ parseJson(req, res, media, options, resolve, reject);
42
+ return;
43
+ }
44
+
45
+ // Other content-types are ignored
46
+ resolve();
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Parse multipart/form-data with optional streaming support
52
+ */
53
+ function parseMultipart(req, res, media, options, resolve, reject) {
54
+ let bb;
55
+ let fileError = null;
56
+ const streaming = media.streaming === true;
57
+
58
+ try {
59
+ bb = busboy({
60
+ headers: req.headers,
61
+ limits: {
62
+ fileSize: media.maxSize || 10 * 1024 * 1024,
63
+ },
64
+ });
65
+ } catch (err) {
66
+ console.error("Busboy init failed:", err);
67
+ return resolve();
68
+ }
69
+
70
+ bb.on("field", (name, value) => {
71
+ req.body[name] = value;
72
+ });
73
+
74
+ bb.on("file", (name, file, info) => {
75
+ const { filename, mimeType } = info;
76
+ if (!filename) return file.resume();
77
+
78
+ // File type validation
79
+ if (media.allowedTypes && Array.isArray(media.allowedTypes)) {
80
+ if (!media.allowedTypes.includes(mimeType)) {
81
+ fileError = new Error(
82
+ `File type '${mimeType}' not allowed. Allowed: ${media.allowedTypes.join(", ")}`,
83
+ );
84
+ return file.resume();
85
+ }
86
+ }
87
+
88
+ // STREAMING MODE: Emit file event, let handler deal with it
89
+ if (streaming) {
90
+ req.emit("file", name, file, { filename, mimeType });
91
+ return;
92
+ }
93
+
94
+ // BUFFERING MODE: Write to disk
95
+ const parent = media.public ? options.publicFolder || "" : "";
96
+ const dest = path.resolve(
97
+ path.join(parent, media.dest || (media.public ? "uploads" : "private")),
98
+ );
99
+
100
+ // Prevent path traversal
101
+ if (
102
+ media.public &&
103
+ !dest.startsWith(path.resolve(options.publicFolder || ""))
104
+ ) {
105
+ console.warn("Attempted upload outside public folder, skipping");
106
+ return file.resume();
107
+ }
108
+
109
+ try {
110
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
111
+ } catch (err) {
112
+ console.error("Failed to create upload folder:", err);
113
+ return file.resume();
114
+ }
115
+
116
+ const ext =
117
+ path.extname(filename) ||
118
+ (mimeType?.includes("/") ? "." + mimeType.split("/")[1] : "");
119
+
120
+ const safeName = `${path.basename(filename, ext)}-${crypto
121
+ .randomBytes(3)
122
+ .toString("hex")}${ext}`;
123
+ const filePath = path.join(dest, safeName);
124
+
125
+ const writeStream = fs.createWriteStream(filePath);
126
+ let size = 0;
127
+ let truncated = false;
128
+
129
+ file.on("data", (d) => (size += d.length));
130
+
131
+ // Handle file size limit exceeded
132
+ file.on("limit", () => {
133
+ truncated = true;
134
+ fileError = new Error(
135
+ `File '${filename}' exceeds max size of ${media.maxSize || 10 * 1024 * 1024} bytes`,
136
+ );
137
+ file.unpipe(writeStream);
138
+ writeStream.end();
139
+ // Clean up partial file
140
+ fs.unlink(filePath, () => {});
141
+ });
142
+
143
+ file.on("error", (err) => {
144
+ console.error("File stream error:", err);
145
+ writeStream.end();
146
+ });
147
+
148
+ writeStream.on("error", (err) => {
149
+ console.error("Write stream error:", err);
150
+ file.resume();
151
+ });
152
+
153
+ writeStream.on("finish", () => {
154
+ if (!truncated) {
155
+ req.files.push({
156
+ filename: safeName,
157
+ originalName: filename,
158
+ type: mimeType,
159
+ filePath,
160
+ size,
161
+ });
162
+ }
163
+ });
164
+
165
+ file.pipe(writeStream);
166
+ });
167
+
168
+ bb.on("error", (err) => {
169
+ console.error("Busboy error:", err);
170
+ req.unpipe(bb);
171
+ reject(err);
172
+ });
173
+
174
+ bb.on("finish", () => {
175
+ if (fileError) {
176
+ reject(fileError);
177
+ } else {
178
+ resolve();
179
+ }
180
+ });
181
+
182
+ req.pipe(bb);
183
+ }
184
+
185
+ /**
186
+ * Parse JSON body with streaming support for large payloads
187
+ */
188
+ function parseJson(req, res, media, options, resolve, reject) {
189
+ const limit = options.maxJsonSize || 1e6;
190
+ const streamThreshold = media.streamThreshold || DEFAULT_STREAM_THRESHOLD;
191
+ const contentLength = parseInt(req.headers["content-length"] || "0", 10);
192
+
193
+ // STREAMING MODE: For very large JSON, let handler process incrementally
194
+ if (media.streaming && contentLength > streamThreshold) {
195
+ req.body = null; // Signal that body should be consumed via stream
196
+ req.emit("jsonStream", req);
197
+ resolve();
198
+ return;
199
+ }
200
+
201
+ // BUFFERING MODE: Collect and parse
202
+ let body = "";
203
+
204
+ req.on("data", (chunk) => {
205
+ body += chunk;
206
+ if (body.length > limit) {
207
+ console.warn("JSON payload too large, destroying connection");
208
+ req.destroy();
209
+ }
210
+ });
211
+
212
+ req.on("end", () => {
213
+ try {
214
+ req.body = JSON.parse(body || "{}");
215
+ } catch {
216
+ req.body = {};
217
+ }
218
+ resolve();
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Stream JSON parser helper
224
+ * Use with streaming mode to parse large JSON incrementally
225
+ *
226
+ * @param {NodeJS.ReadableStream} stream
227
+ * @returns {Promise<any>}
228
+ */
229
+ export async function parseJsonStream(stream) {
230
+ return new Promise((resolve, reject) => {
231
+ let body = "";
232
+ stream.on("data", (chunk) => (body += chunk));
233
+ stream.on("end", () => {
234
+ try {
235
+ resolve(JSON.parse(body));
236
+ } catch (err) {
237
+ reject(err);
238
+ }
239
+ });
240
+ stream.on("error", reject);
241
+ });
242
+ }