vibe-gx 4.1.0 → 4.1.2

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
@@ -46,7 +46,7 @@ npm install vibe-gx
46
46
  | :---------------------------- | :--------------------------------------------------------- |
47
47
  | 🚀 **Code-Gen Serialization** | Schema-compiled JSON serializers via `new Function()` |
48
48
  | 🎯 **Hybrid Router** | O(1) static + O(log n) Trie routing |
49
- | 🔌 **Plugin System** | Fastify-style `register()` with encapsulation |
49
+ | 🔌 **Plugin System** | Encapsulated `register()` with optional route prefixes |
50
50
  | 🎨 **Decorators** | Extend app, request, and response |
51
51
  | ⚡ **Cluster Mode** | Built-in multi-process scaling |
52
52
  | 💾 **LRU Cache** | Built-in response caching with ETag |
@@ -133,7 +133,52 @@ app.post("/users", (req) => {
133
133
 
134
134
  ---
135
135
 
136
- ## 🔌 Plugins (Fastify-style)
136
+ ## 📝 Logging & Error Handling
137
+
138
+ Vibe ships with a structured JSON logger (Pino-compatible) and a powerful error interception system. Errors thrown, returned, or sent from any route are automatically caught and routed through a central error handler.
139
+
140
+ ### JSON Structured Logging
141
+
142
+ Initialize the app with `logger: { lifecycle: true }` or add `prettyPrint: true` for development to get beautiful, human-readable terminal output.
143
+
144
+ ```javascript
145
+ const app = vibe({
146
+ logger: {
147
+ lifecycle: true,
148
+ prettyPrint: process.env.NODE_ENV !== "production",
149
+ },
150
+ });
151
+
152
+ // JSON native bindings
153
+ app.log.info({ database: "online" }, "System booting...");
154
+ ```
155
+
156
+ ### Contextual Sub-Loggers
157
+
158
+ Every incoming request dynamically extracts a fast UUID exposed securely on `req.id` natively piping through to `req.log`.
159
+
160
+ ```javascript
161
+ app.get("/users/:id", (req) => {
162
+ req.log.warn("Database lookup constraint fired");
163
+ // Production Output -> {"level":40,"time":123,"reqId":"abcd-123", "msg":"..."}
164
+
165
+ return { success: true };
166
+ });
167
+ ```
168
+
169
+ ### Central Error Abstraction
170
+
171
+ To route an error into the central handler without halting execution via `throw`, simply return an `Error` object from your handler — Vibe intercepts it automatically:
172
+
173
+ ```javascript
174
+ app.get("/test", (req, res) => {
175
+ return new Error("Something went wrong");
176
+ });
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 🔌 Plugin System
137
182
 
138
183
  Plugins provide encapsulated route groups with optional prefixes:
139
184
 
@@ -215,13 +260,13 @@ Extend app, request, or response with custom properties:
215
260
  // App decorator - shared config
216
261
  app.decorate("config", { env: "production", version: "1.0.0" });
217
262
 
218
- // Access via app.decorators in main app
219
- app.get("/version", () => ({ version: app.decorators.config.version }));
263
+ // Access directly on the app
264
+ app.get("/version", () => ({ version: app.config.version }));
220
265
 
221
- // In plugins, decorators are spread directly (no .decorators)
266
+ // Same in plugins decorators are spread directly
222
267
  app.register(
223
268
  async (api) => {
224
- api.get("/env", () => ({ env: api.config.env })); // Direct access
269
+ api.get("/env", () => ({ env: api.config.env }));
225
270
  },
226
271
  { prefix: "/api" },
227
272
  );
@@ -541,22 +586,26 @@ app.post(
541
586
 
542
587
  ### Application
543
588
 
544
- | Method | Description |
545
- | :----------------------------------------------- | :------------------- |
546
- | `app.get/post/put/del/patch/head(path, handler)` | Register route |
547
- | `app.listen(port, host?, callback?)` | Start server |
548
- | `app.register(fn, { prefix })` | Register plugin |
549
- | `app.plugin(fn)` | Global interceptor |
550
- | `app.decorate(name, value)` | Add app property |
551
- | `app.decorateRequest(name, value)` | Add to all requests |
552
- | `app.decorateReply(name, value)` | Add to all responses |
553
- | `app.setPublicFolder(path)` | Set static folder |
554
- | `app.logRoutes()` | Log all routes |
589
+ | Method | Description |
590
+ | :----------------------------------------------- | :--------------------- |
591
+ | `vibe({ logger?: LoggerConfig })` | Initialize app |
592
+ | `app.setErrorHandler(fn)` | Override error handler |
593
+ | `app.get/post/put/del/patch/head(path, handler)` | Register route |
594
+ | `app.listen(port, host?, callback?)` | Start server |
595
+ | `app.register(fn, { prefix })` | Register plugin |
596
+ | `app.plugin(fn)` | Global interceptor |
597
+ | `app.decorate(name, value)` | Add app property |
598
+ | `app.decorateRequest(name, value)` | Add to all requests |
599
+ | `app.decorateReply(name, value)` | Add to all responses |
600
+ | `app.setPublicFolder(path)` | Set static folder |
601
+ | `app.logRoutes()` | Log all routes |
555
602
 
556
603
  ### Request (`req`)
557
604
 
558
605
  | Property | Description |
559
606
  | :------------ | :------------------------- |
607
+ | `req.id` | Auto-generated UUID logic |
608
+ | `req.log` | Context-bound logger API |
560
609
  | `req.params` | Route parameters (`:id`) |
561
610
  | `req.query` | Query string (`?page=1`) |
562
611
  | `req.body` | Parsed JSON/form body |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-gx",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "A lightweight, high-performance Node.js web framework.",
5
5
  "type": "module",
6
6
  "main": "vibe.js",
@@ -142,11 +142,16 @@ export function handleError(error, req, res) {
142
142
  const isDev = process.env.NODE_ENV !== "production";
143
143
  const message = error.message || "Unknown error";
144
144
 
145
- // Log error (full stack in dev, message only in production)
146
- if (isDev) {
147
- console.error("[VIBE ERROR]:", error);
145
+ // Log error using context-aware structured logger if available
146
+ if (req && req.log) {
147
+ req.log.error(error);
148
148
  } else {
149
- console.error("[VIBE ERROR]:", message);
149
+ // Fallback: full stack in dev, message only in production
150
+ if (isDev) {
151
+ console.error("[VIBE ERROR]:", error);
152
+ } else {
153
+ console.error("[VIBE ERROR]:", message);
154
+ }
150
155
  }
151
156
 
152
157
  if (!res.headersSent) {
@@ -0,0 +1,185 @@
1
+ import os from "os";
2
+ import fs from "fs";
3
+ import { color } from "../helpers/colors.js";
4
+
5
+ const LOG_LEVELS = {
6
+ trace: 10,
7
+ debug: 20,
8
+ info: 30,
9
+ warn: 40,
10
+ error: 50,
11
+ fatal: 60,
12
+ };
13
+
14
+ const LEVEL_NAMES = {
15
+ 10: "TRACE",
16
+ 20: "DEBUG",
17
+ 30: "INFO",
18
+ 40: "WARN",
19
+ 50: "ERROR",
20
+ 60: "FATAL",
21
+ };
22
+
23
+ /**
24
+ * High-performance structured JSON logger (Fastify/Pino style).
25
+ */
26
+ export class Logger {
27
+ constructor(options = {}) {
28
+ this.level = LOG_LEVELS[options.level || "info"] || 30;
29
+ this.colors = options.colors !== undefined ? options.colors : true;
30
+ this.prettyPrint =
31
+ options.prettyPrint !== undefined ? options.prettyPrint : this.colors;
32
+ this.lifecycle = options.lifecycle || false;
33
+ this.stream = options.stream || process.stdout;
34
+ this.dest = options.dest || "console"; // "console", "file", "both"
35
+ this.logFile = options.logFile;
36
+ this.bindings = options.bindings || {};
37
+
38
+ // Initialize file stream if needed
39
+ if (this.logFile && (this.dest === "file" || this.dest === "both")) {
40
+ this.fileStream = fs.createWriteStream(this.logFile, { flags: "a" });
41
+ }
42
+
43
+ if (!this.bindings.pid) this.bindings.pid = process.pid;
44
+ if (!this.bindings.hostname) this.bindings.hostname = os.hostname();
45
+ }
46
+
47
+ /**
48
+ * Creates a sub-logger with scoped bindings (e.g. reqId).
49
+ */
50
+ child(bindings) {
51
+ return new Logger({
52
+ level: Object.keys(LOG_LEVELS).find(
53
+ (key) => LOG_LEVELS[key] === this.level,
54
+ ),
55
+ colors: this.colors,
56
+ prettyPrint: this.prettyPrint,
57
+ lifecycle: this.lifecycle,
58
+ stream: this.stream,
59
+ dest: this.dest,
60
+ logFile: this.logFile,
61
+ bindings: { ...this.bindings, ...bindings },
62
+ });
63
+ }
64
+
65
+ trace(obj, msg, c) {
66
+ this._log(10, obj, msg, c);
67
+ }
68
+ debug(obj, msg, c) {
69
+ this._log(20, obj, msg, c);
70
+ }
71
+ info(obj, msg, c) {
72
+ this._log(30, obj, msg, c);
73
+ }
74
+ warn(obj, msg, c) {
75
+ this._log(40, obj, msg, c);
76
+ }
77
+ error(obj, msg, c) {
78
+ this._log(50, obj, msg, c);
79
+ }
80
+ fatal(obj, msg, c) {
81
+ this._log(60, obj, msg, c);
82
+ }
83
+
84
+ _log(level, obj, msg, c) {
85
+ if (level < this.level) return;
86
+
87
+ const base = {
88
+ level,
89
+ time: Date.now(),
90
+ ...this.bindings,
91
+ };
92
+
93
+ let logData = {};
94
+ let customColor = undefined;
95
+
96
+ if (obj instanceof Error) {
97
+ logData.err = {
98
+ type: obj.name || "Error",
99
+ message: obj.message,
100
+ stack: obj.stack,
101
+ };
102
+ if (typeof msg === "string") logData.msg = msg;
103
+ else logData.msg = obj.message;
104
+ if (typeof c === "string") customColor = c;
105
+ } else if (typeof obj === "string") {
106
+ logData.msg = obj;
107
+ if (typeof msg === "string") customColor = msg;
108
+ } else if (typeof obj === "object" && obj !== null) {
109
+ logData = { ...obj };
110
+ if (typeof msg === "string") logData.msg = msg;
111
+ if (typeof c === "string") customColor = c;
112
+ } else {
113
+ logData.msg = String(obj);
114
+ if (typeof msg === "string") customColor = msg;
115
+ }
116
+
117
+ if (customColor) {
118
+ logData.color = customColor;
119
+ }
120
+
121
+ const finalLog = { ...base, ...logData };
122
+
123
+ if (this.dest === "console" || this.dest === "both") {
124
+ if (this.prettyPrint) {
125
+ this._printPretty(finalLog);
126
+ } else {
127
+ this.stream.write(JSON.stringify(finalLog) + "\n");
128
+ }
129
+ }
130
+
131
+ if ((this.dest === "file" || this.dest === "both") && this.fileStream) {
132
+ this.fileStream.write(JSON.stringify(finalLog) + "\n");
133
+ }
134
+ }
135
+
136
+ _printPretty(log) {
137
+ const time = new Date(log.time).toLocaleTimeString();
138
+ const lvlName = LEVEL_NAMES[log.level] || "INFO";
139
+ let prefixC = color.cyan;
140
+ if (log.level >= 50) prefixC = color.red;
141
+ else if (log.level === 40) prefixC = color.yellow;
142
+ else if (log.level <= 20) prefixC = color.dim;
143
+
144
+ const prefix = prefixC(`[VIBE ${lvlName} ${time}]`);
145
+ let context = "";
146
+ if (log.reqId) {
147
+ context = `\x1b[90m[${log.reqId}]\x1b[0m `;
148
+ }
149
+
150
+ let content = log.msg || "";
151
+ if (log.color && color[log.color]) {
152
+ content = color[log.color](content);
153
+ }
154
+
155
+ if (log.err && log.err.stack) {
156
+ content += "\n" + prefixC(log.err.stack);
157
+ }
158
+
159
+ // Attempt to print remaining metadata if it's not standard
160
+ const skipKeys = [
161
+ "level",
162
+ "time",
163
+ "pid",
164
+ "hostname",
165
+ "reqId",
166
+ "msg",
167
+ "err",
168
+ "color",
169
+ ];
170
+ let metaStr = "";
171
+ for (const key of Object.keys(log)) {
172
+ if (!skipKeys.includes(key)) {
173
+ metaStr += ` \x1b[90m${key}=${JSON.stringify(log[key])}\x1b[0m`;
174
+ }
175
+ }
176
+
177
+ this.stream.write(`${prefix} ${context}${content}${metaStr}\n`);
178
+ }
179
+ }
180
+
181
+ export function createLogger(options = {}) {
182
+ return new Logger(options);
183
+ }
184
+
185
+ export default createLogger;
@@ -35,6 +35,10 @@ const vibeResponseMethods = {
35
35
  throw new Error("Response data is not a sendable data type");
36
36
  }
37
37
 
38
+ if (data instanceof Error) {
39
+ return this._vibeOptions.errorHandler(data, this.req, this);
40
+ }
41
+
38
42
  if (typeof data === "object" && data !== null) {
39
43
  if (!this.headersSent) this.writeHead(this.statusCode || 200, JSON_CT);
40
44
  this.end(JSON.stringify(data));
@@ -50,6 +54,9 @@ const vibeResponseMethods = {
50
54
  * @param {Object} data
51
55
  */
52
56
  json(data) {
57
+ if (data instanceof Error) {
58
+ return this._vibeOptions.errorHandler(data, this.req, this);
59
+ }
53
60
  if (!this.headersSent) this.writeHead(this.statusCode || 200, JSON_CT);
54
61
  this.end(JSON.stringify(data));
55
62
  },
@@ -1,8 +1,8 @@
1
1
  import http from "http";
2
+ import crypto from "crypto";
2
3
  import { error, getNetworkIP, handleError, isSendAble } from "./handler.js";
3
4
  import bodyParser from "./parser.js";
4
5
  import { installResponseMethods, initResponse } from "./response.js";
5
- import dns from "node:dns/promises";
6
6
  import { parseQuery } from "../native.js";
7
7
 
8
8
  // Pre-allocated headers (frozen for V8 optimization)
@@ -90,6 +90,33 @@ async function server(options, port, host, callback) {
90
90
 
91
91
  // Main request handler - ULTRA OPTIMIZED
92
92
  function reqListener(req, res) {
93
+ req.id = crypto.randomUUID();
94
+ req.log = options.logger.child({ reqId: req.id });
95
+
96
+ if (options.loggerConfig && options.loggerConfig.lifecycle) {
97
+ req.startTime = Date.now();
98
+
99
+ // Determine sender IP early for logging
100
+ const sender =
101
+ req.socket.remoteAddress || req.headers["x-forwarded-for"] || "unknown";
102
+
103
+ req.log.info(
104
+ { type: "req", url: req.url, method: req.method, sender },
105
+ "Incoming request",
106
+ );
107
+
108
+ res.on("finish", () => {
109
+ req.log.info(
110
+ {
111
+ type: "res",
112
+ statusCode: res.statusCode,
113
+ responseTimeMs: Date.now() - req.startTime,
114
+ },
115
+ "Request completed",
116
+ );
117
+ });
118
+ }
119
+
93
120
  // Fast pathname extraction
94
121
  const url = req.url;
95
122
  const qIdx = url.indexOf("?");
@@ -151,13 +178,19 @@ async function server(options, port, host, callback) {
151
178
  if (result && typeof result.then === "function") {
152
179
  result
153
180
  .then((val) => {
181
+ if (val instanceof Error) {
182
+ return options.errorHandler(val, req, res);
183
+ }
154
184
  if (val !== undefined && !res.writableEnded) {
155
185
  res.writeHead(200, JSON_HEADERS);
156
186
  res.end(serialize ? serialize(val) : JSON.stringify(val));
157
187
  }
158
188
  })
159
- .catch((err) => handleError(err, req, res));
189
+ .catch((err) => options.errorHandler(err, req, res));
160
190
  } else if (typeof result === "object" && result !== null) {
191
+ if (result instanceof Error) {
192
+ return options.errorHandler(result, req, res);
193
+ }
161
194
  res.writeHead(200, JSON_HEADERS);
162
195
  res.end(serialize ? serialize(result) : JSON.stringify(result));
163
196
  } else {
@@ -166,7 +199,7 @@ async function server(options, port, host, callback) {
166
199
  }
167
200
  }
168
201
  } catch (err) {
169
- handleError(err, req, res);
202
+ options.errorHandler(err, req, res);
170
203
  }
171
204
  return;
172
205
  }
@@ -226,6 +259,9 @@ async function server(options, port, host, callback) {
226
259
  // Execute handler
227
260
  if (typeof handler === "function") {
228
261
  const result = await handler(req, res);
262
+ if (result instanceof Error) {
263
+ return options.errorHandler(result, req, res);
264
+ }
229
265
  if (result !== undefined && !res.writableEnded) {
230
266
  if (serialize) {
231
267
  // Pre-compiled schema serializer — fastest path
@@ -247,7 +283,7 @@ async function server(options, port, host, callback) {
247
283
  throw new Error("Invalid handler type");
248
284
  }
249
285
  } catch (err) {
250
- handleError(err, req, res);
286
+ options.errorHandler(err, req, res);
251
287
  }
252
288
  }
253
289
 
@@ -256,10 +292,7 @@ async function server(options, port, host, callback) {
256
292
 
257
293
  const vibe_server = http.createServer(reqListener);
258
294
 
259
- vibe_server.listen(port, mainHost, async () => {
260
- try {
261
- await dns.lookup("::", { all: true });
262
- } catch {}
295
+ vibe_server.listen(port, mainHost, () => {
263
296
  getNetworkIP(mainHost, port);
264
297
 
265
298
  const strategy = useTrieMatching ? "Trie (O(log n))" : "Linear (O(n))";
@@ -271,7 +304,29 @@ async function server(options, port, host, callback) {
271
304
  });
272
305
 
273
306
  vibe_server.on("error", (err) => {
274
- error(`Port ${port} is already in use! \n${err.message}`);
307
+ if (err.code === "EADDRINUSE") {
308
+ error(`Port ${port} is already in use! \n${err.message}`);
309
+ process.exit(1);
310
+ } else {
311
+ error(`Server error: \n${err.message}`);
312
+ }
313
+ });
314
+
315
+ // Graceful shutdown support for node --watch, nodemon, and cluster mode
316
+ const shutdown = () => {
317
+ // vibe_server.close stops accepting new connections
318
+ // Existing keep-alive connections will still prevent instant exit,
319
+ // so we force an exit if it takes longer than 3 seconds.
320
+ vibe_server.close(() => {
321
+ process.exit(0);
322
+ });
323
+ setTimeout(() => process.exit(0), 3000).unref();
324
+ };
325
+
326
+ process.on("SIGTERM", shutdown);
327
+ process.on("SIGINT", shutdown);
328
+ process.on("message", (msg) => {
329
+ if (msg === "shutdown") shutdown();
275
330
  });
276
331
  }
277
332
 
@@ -145,7 +145,17 @@ export class LRUCache {
145
145
  */
146
146
  export function cacheMiddleware(cache) {
147
147
  return (req, res) => {
148
- const key = LRUCache.key(req.method, req.url);
148
+ // Use the full original URL (includes query string) for the cache key.
149
+ // req.url is overwritten with just the pathname by the server internals,
150
+ // so we fall back to req._rawUrl which preserves the full URL.
151
+ // We also append serialized route params so that parameterised routes
152
+ // (e.g. /users/:id) with different param values get distinct cache entries.
153
+ const rawUrl = req._rawUrl || req.url;
154
+ const paramsStr =
155
+ req.params && Object.keys(req.params).length > 0
156
+ ? JSON.stringify(req.params)
157
+ : "";
158
+ const key = LRUCache.key(req.method, rawUrl + paramsStr);
149
159
  const entry = cache.get(key);
150
160
 
151
161
  if (entry) {
@@ -164,16 +174,45 @@ export function cacheMiddleware(cache) {
164
174
  return false; // Stop execution
165
175
  }
166
176
 
167
- // Store original json method to intercept response
177
+ // Store original json and end methods to intercept response
168
178
  const originalJson = res.json.bind(res);
179
+ const originalEnd = res.end.bind(res);
180
+
181
+ // Intercept res.json (explicit json calls by handler)
169
182
  res.json = (data) => {
170
- // Cache the response
171
183
  const newEntry = cache.set(key, data);
172
184
  res.setHeader("ETag", newEntry.etag);
173
185
  res.setHeader("X-Cache", "MISS");
174
186
  originalJson(data);
175
187
  };
176
188
 
189
+ // Intercept res.end (implicit return-value path in server.js uses
190
+ // res.writeHead + res.end directly, bypassing res.json).
191
+ // Note: res.getHeader() does NOT see headers set via res.writeHead(),
192
+ // so we can't check Content-Type that way. Instead, try JSON.parse directly.
193
+ res.end = (body) => {
194
+ if (body && !res._vibeCached) {
195
+ try {
196
+ const parsed = JSON.parse(body);
197
+ // Only cache plain objects/arrays — not error objects, not primitives
198
+ if (
199
+ typeof parsed === "object" &&
200
+ parsed !== null &&
201
+ !parsed.error // skip error responses
202
+ ) {
203
+ res._vibeCached = true;
204
+ const newEntry = cache.set(key, parsed);
205
+ // setHeader is safe here — headers not yet flushed
206
+ res.setHeader("ETag", newEntry.etag);
207
+ res.setHeader("X-Cache", "MISS");
208
+ }
209
+ } catch {
210
+ // Not JSON — skip caching
211
+ }
212
+ }
213
+ originalEnd(body);
214
+ };
215
+
177
216
  return true; // Continue to handler
178
217
  };
179
218
  }
package/vibe.d.ts CHANGED
@@ -172,6 +172,57 @@ export interface RegisterOptions {
172
172
  [key: string]: any;
173
173
  }
174
174
 
175
+ // ==========================================
176
+ // Logging System
177
+ // ==========================================
178
+
179
+ export interface LoggerConfig {
180
+ /** If true, automatically logs request lifecycle hooks (Incoming Request, Request Completed) */
181
+ lifecycle?: boolean;
182
+ /** If true, formats JSON output into human-readable Vibe-styled terminal lines (like pino-pretty) */
183
+ prettyPrint?: boolean;
184
+ /** If true, applies ANSI color formatting to terminal logs. Default: true (unless overridden) */
185
+ colors?: boolean;
186
+ /** Destination for the logs. Accepts "console", "file", or "both". Default: "console" */
187
+ dest?: "console" | "file" | "both";
188
+ /** Absolute or relative path to the target log file. Active when dest is "file" or "both" */
189
+ logFile?: string;
190
+ /** Custom writable stream to output logs to (defaults to process.stdout) */
191
+ stream?: NodeJS.WritableStream;
192
+ }
193
+
194
+ /**
195
+ * Fastify/Pino-compatible structured logger API.
196
+ *
197
+ * All methods accept a message string OR a structured object (Pino-style).
198
+ * An optional color string can be passed as the last argument — in prettyPrint
199
+ * mode it will colorize the terminal output, in production mode it writes as
200
+ * a plain JSON `{ color: "..." }` key for log pipelines.
201
+ *
202
+ * @example
203
+ * req.log.info("Processing payment");
204
+ * req.log.info({ userId: 42, amount: 100 }, "Payment initiated");
205
+ * req.log.error(new Error("DB timeout"));
206
+ * req.log.warn("Slow query detected", "yellow"); // color override
207
+ */
208
+ export interface LoggerAPI {
209
+ trace(obj: object | string | Error, msg?: string, color?: ColorName): void;
210
+ debug(obj: object | string | Error, msg?: string, color?: ColorName): void;
211
+ info(obj: object | string | Error, msg?: string, color?: ColorName): void;
212
+ warn(obj: object | string | Error, msg?: string, color?: ColorName): void;
213
+ error(obj: object | string | Error, msg?: string, color?: ColorName): void;
214
+ fatal(obj: object | string | Error, msg?: string, color?: ColorName): void;
215
+ /** Returns a child logger with merged bindings (e.g., { reqId }) */
216
+ child(bindings: Record<string, any>): LoggerAPI;
217
+ }
218
+
219
+ export interface VibeConfig {
220
+ /** Configuration for the native Vibe terminal logger */
221
+ logger?: LoggerConfig | boolean;
222
+ /** Enable automatic process restarting on crash. Spawns an internal cluster manager. Default: false */
223
+ autoRestart?: boolean;
224
+ }
225
+
175
226
  // ==========================================
176
227
  // Request & Response Extensions
177
228
  // ==========================================
@@ -192,6 +243,10 @@ export interface VibeRequest extends IncomingMessage {
192
243
  ip?: string;
193
244
  /** Detailed client IP info */
194
245
  fullIp?: string;
246
+ /** Automatically generated UUID for the request lifecycle */
247
+ id: string;
248
+ /** Context-bound logger automatically stamped with the req.id constraint */
249
+ log: LoggerAPI;
195
250
  /** Custom properties added via decorateRequest */
196
251
  [key: string]: any;
197
252
  }
@@ -338,11 +393,14 @@ export interface RouterAPI {
338
393
  head: RouteRegistrar;
339
394
 
340
395
  /**
341
- * Log helper
342
- * @param value The message to log
343
- * @param color Optional color name (e.g. 'green', 'red')
396
+ * Log helper supporting native colors and Vibe-stylized log levels
397
+ * @param value The message or object to log
398
+ * @param typeOrColor Optional color name (e.g. 'green') or level ('info', 'warn', 'error', 'req')
344
399
  */
345
- log: (value: any, color?: ColorName) => void;
400
+ log: (
401
+ value: any,
402
+ typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
403
+ ) => void;
346
404
 
347
405
  /** Register a global interceptor */
348
406
  plugin: (interceptor: Interceptor) => void;
@@ -401,15 +459,44 @@ export interface VibeApp extends RouterAPI {
401
459
  maybeFunc?: (router: RouterAPI) => void,
402
460
  ) => void;
403
461
 
404
- /** Access app decorators */
462
+ /**
463
+ * Access app decorators
464
+ * @type {Record<string, any>}
465
+ */
405
466
  readonly decorators: Record<string, any>;
467
+
468
+ /**
469
+ * Override the default error handler (Fastify-style).
470
+ * Called for any unhandled `throw`, `return new Error()`, or `res.send(error)`.
471
+ * @example
472
+ * app.setErrorHandler((error, req, res) => {
473
+ * req.log.error(error);
474
+ * res.status(503).json({ success: false, message: error.message });
475
+ * });
476
+ */
477
+ setErrorHandler(
478
+ fn: (error: Error, req: VibeRequest, res: VibeResponse) => void,
479
+ ): void;
480
+
481
+ /**
482
+ * Pino/Fastify-compatible structured logger instance.
483
+ * Use for application-level logging outside of routes.
484
+ * @example
485
+ * app.log.info({ db: "connected" }, "Server ready");
486
+ * app.log.info("Server ready", "green"); // with color in prettyPrint mode
487
+ */
488
+ log: LoggerAPI;
489
+
490
+ /** Alias for `app.log` */
491
+ logger: LoggerAPI;
406
492
  }
407
493
 
408
494
  /**
409
495
  * Initialize a new Vibe application.
496
+ * @param config Optional application configuration
410
497
  * @returns Vibe application instance
411
498
  */
412
- export default function vibe(): VibeApp;
499
+ export default function vibe(config?: VibeConfig): VibeApp;
413
500
 
414
501
  // ==========================================
415
502
  // LRU Cache
package/vibe.js CHANGED
@@ -4,6 +4,9 @@ import { color } from "./utils/helpers/colors.js";
4
4
  import { RouteTrie } from "./utils/core/trie.js";
5
5
  import { PathToRegex } from "./utils/core/handler.js";
6
6
  import { compileSerializer } from "./utils/core/compile-serializer.js";
7
+ import { createLogger, Logger } from "./utils/core/logger.js";
8
+ import { handleError } from "./utils/core/handler.js";
9
+ import { clusterize } from "./utils/scaling/cluster.js";
7
10
 
8
11
  /**
9
12
  * Helper to generate regex for a path
@@ -129,9 +132,12 @@ function pathToRegex(path) {
129
132
 
130
133
  /**
131
134
  * Initializes a Vibe application instance.
135
+ * @param {Object} [config={}]
136
+ * @param {Object|boolean} [config.logger] - Logger configuration
137
+ * @param {boolean} [config.autoRestart] - Restart server automatically on crash
132
138
  * @returns {VibeApp}
133
139
  */
134
- const vibe = () => {
140
+ const vibe = (config = {}) => {
135
141
  // Route trie for O(log n) matching (used when routes > threshold)
136
142
  const trie = new RouteTrie();
137
143
 
@@ -144,6 +150,14 @@ const vibe = () => {
144
150
  // Static routes Map for O(1) lookup (routes without params)
145
151
  const staticRoutes = new Map();
146
152
 
153
+ // Logger initialization
154
+ const loggerConfig =
155
+ config.logger !== false ? config.logger || {} : { level: "silent" };
156
+ const appLogger =
157
+ config.logger instanceof Logger
158
+ ? config.logger
159
+ : createLogger(loggerConfig);
160
+
147
161
  // Internal configuration
148
162
  const options = {
149
163
  trie,
@@ -156,8 +170,27 @@ const vibe = () => {
156
170
  decorators: {},
157
171
  requestDecorators: {},
158
172
  replyDecorators: {},
173
+ logger: appLogger,
174
+ loggerConfig,
175
+ errorHandler: handleError,
159
176
  };
160
177
 
178
+ // Add global uncaught exception handler to prevent silent deaths and log cleanly
179
+ process.on("uncaughtException", (err) => {
180
+ appLogger.fatal(err, "Uncaught Exception crashed the server");
181
+ // Only exit here if we're not exiting gracefully anyway,
182
+ // cluster manager will restart it.
183
+ setTimeout(() => process.exit(1), 100);
184
+ });
185
+
186
+ process.on("unhandledRejection", (reason, promise) => {
187
+ appLogger.fatal(
188
+ { err: reason },
189
+ "Unhandled Promise Rejection crashed the server",
190
+ );
191
+ setTimeout(() => process.exit(1), 100);
192
+ });
193
+
161
194
  // Register default landing route
162
195
  const defaultRoute = {
163
196
  method: "GET",
@@ -362,7 +395,13 @@ const vibe = () => {
362
395
  host = undefined;
363
396
  }
364
397
 
365
- server(options, Number(port), host, callback);
398
+ const startServer = () => server(options, Number(port), host, callback);
399
+
400
+ if (config.autoRestart) {
401
+ clusterize(startServer, { workers: 1, restart: true });
402
+ } else {
403
+ startServer();
404
+ }
366
405
  }
367
406
 
368
407
  /**
@@ -460,6 +499,8 @@ const vibe = () => {
460
499
  throw new Error(`Decorator '${name}' already exists`);
461
500
  }
462
501
  options.decorators[name] = value;
502
+ // Also set directly on the app object for easy access (app.name)
503
+ if (app) app[name] = value;
463
504
  }
464
505
 
465
506
  /**
@@ -532,12 +573,13 @@ const vibe = () => {
532
573
  }
533
574
 
534
575
  /**
535
- * Logs a message with optional color
536
- * @param {string} message
537
- * @param {string} [colorValue="reset"]
576
+ * Log messages out using the Vibe stylized legacy logger.
577
+ * Native string logging bypasses the Pino JSON interface.
538
578
  */
539
- const log = (message, colorValue = "reset") =>
540
- process.stdout.write(`${color[colorValue](message)}\n`);
579
+ const log = (message, typeOrColor = "reset") => {
580
+ const c = color[typeOrColor] || color.reset;
581
+ process.stdout.write(c(message) + "\n");
582
+ };
541
583
 
542
584
  // Build the app object with decorators
543
585
  const app = {
@@ -549,8 +591,13 @@ const vibe = () => {
549
591
  head,
550
592
  listen,
551
593
  logRoutes,
552
- log,
594
+ log: appLogger, // Standard Fastify-like exposure (app.log.info())
595
+ logger: appLogger,
596
+ logLegacy: log,
553
597
  setPublicFolder,
598
+ setErrorHandler: (fn) => {
599
+ options.errorHandler = fn;
600
+ },
554
601
  include,
555
602
  plugin,
556
603
  register,