voltjs-framework 1.0.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,701 @@
1
+ /**
2
+ * VoltJS Core Application
3
+ *
4
+ * The main application class that ties everything together.
5
+ * Handles HTTP server, routing, middleware, and lifecycle management.
6
+ *
7
+ * @example
8
+ * const { Volt } = require('voltjs');
9
+ *
10
+ * const app = new Volt({
11
+ * port: 3000,
12
+ * security: { csrf: true, xss: true, rateLimit: true },
13
+ * });
14
+ *
15
+ * app.get('/', (req, res) => res.json({ hello: 'world' }));
16
+ * app.listen();
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const http = require('http');
22
+ const https = require('https');
23
+ const url = require('url');
24
+ const path = require('path');
25
+ const fs = require('fs');
26
+ const { Router } = require('./router');
27
+ const { Middleware } = require('./middleware');
28
+ const { Config } = require('./config');
29
+ const { Renderer } = require('./renderer');
30
+ const { PluginManager } = require('./plugins');
31
+ const { ReactRenderer } = require('./react-renderer');
32
+ const React = require('react');
33
+ const ReactDOMServer = require('react-dom/server');
34
+
35
+ class Volt {
36
+ constructor(userConfig = {}) {
37
+ this.config = new Config(userConfig);
38
+ this.router = new Router();
39
+ this.middleware = new Middleware();
40
+ this.renderer = new Renderer(this.config);
41
+ this.react = new ReactRenderer(userConfig.react || {});
42
+ this.plugins = new PluginManager(this);
43
+ this.server = null;
44
+ this._hooks = { beforeStart: [], afterStart: [], beforeStop: [], afterStop: [], onError: [] };
45
+ this._staticDirs = [];
46
+ this._viewsDir = null;
47
+
48
+ // Auto-apply security middleware if enabled
49
+ this._applyDefaultSecurity();
50
+
51
+ // Auto-discover routes from pages/ directory
52
+ this._autoDiscoverRoutes();
53
+ }
54
+
55
+ // ===== HTTP METHODS =====
56
+ get(path, ...handlers) { return this.router.add('GET', path, handlers); }
57
+ post(path, ...handlers) { return this.router.add('POST', path, handlers); }
58
+ put(path, ...handlers) { return this.router.add('PUT', path, handlers); }
59
+ patch(path, ...handlers) { return this.router.add('PATCH', path, handlers); }
60
+ delete(path, ...handlers) { return this.router.add('DELETE', path, handlers); }
61
+ options(path, ...handlers) { return this.router.add('OPTIONS', path, handlers); }
62
+ head(path, ...handlers) { return this.router.add('HEAD', path, handlers); }
63
+ all(path, ...handlers) { return this.router.add('ALL', path, handlers); }
64
+
65
+ // ===== MIDDLEWARE =====
66
+ use(...args) {
67
+ if (typeof args[0] === 'string') {
68
+ // Path-specific middleware
69
+ this.middleware.addPath(args[0], args.slice(1));
70
+ } else if (typeof args[0] === 'function') {
71
+ // Global middleware
72
+ this.middleware.addGlobal(args[0]);
73
+ } else if (args[0] && typeof args[0] === 'object' && args[0].install) {
74
+ // Plugin
75
+ this.plugins.register(args[0], args[1]);
76
+ }
77
+ return this;
78
+ }
79
+
80
+ // ===== ROUTE GROUPS =====
81
+ group(prefix, callback) {
82
+ const group = this.router.group(prefix);
83
+ callback(group);
84
+ return this;
85
+ }
86
+
87
+ // ===== RESOURCE ROUTES (auto CRUD) =====
88
+ resource(name, controller) {
89
+ const base = `/${name}`;
90
+ this.get(base, controller.index || controller.list);
91
+ this.get(`${base}/:id`, controller.show || controller.get);
92
+ this.post(base, controller.create || controller.store);
93
+ this.put(`${base}/:id`, controller.update);
94
+ this.patch(`${base}/:id`, controller.update);
95
+ this.delete(`${base}/:id`, controller.destroy || controller.remove || controller.delete);
96
+ return this;
97
+ }
98
+
99
+ // ===== STATIC FILES =====
100
+ static(urlPath, dirPath) {
101
+ this._staticDirs.push({
102
+ url: urlPath,
103
+ dir: path.resolve(dirPath || urlPath),
104
+ });
105
+ return this;
106
+ }
107
+
108
+ // ===== VIEWS =====
109
+ views(dirPath) {
110
+ this._viewsDir = path.resolve(dirPath);
111
+ this.renderer.setViewsDir(this._viewsDir);
112
+ return this;
113
+ }
114
+
115
+ // ===== LIFECYCLE HOOKS =====
116
+ beforeStart(fn) { this._hooks.beforeStart.push(fn); return this; }
117
+ afterStart(fn) { this._hooks.afterStart.push(fn); return this; }
118
+ beforeStop(fn) { this._hooks.beforeStop.push(fn); return this; }
119
+ afterStop(fn) { this._hooks.afterStop.push(fn); return this; }
120
+ onError(fn) { this._hooks.onError.push(fn); return this; }
121
+
122
+ // ===== REQUEST HANDLER =====
123
+ async _handleRequest(req, res) {
124
+ const startTime = Date.now();
125
+
126
+ try {
127
+ // Parse URL
128
+ const parsedUrl = url.parse(req.url, true);
129
+ req.path = parsedUrl.pathname;
130
+ req.query = parsedUrl.query;
131
+ req.params = {};
132
+
133
+ // Enhanced response object
134
+ this._enhanceResponse(res, req);
135
+
136
+ // Parse body for POST/PUT/PATCH
137
+ if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
138
+ await this._parseBody(req);
139
+ }
140
+
141
+ // Parse cookies
142
+ this._parseCookies(req);
143
+
144
+ // Try static files first
145
+ const staticResult = await this._serveStatic(req, res);
146
+ if (staticResult) return;
147
+
148
+ // Run global middleware
149
+ const middlewareResult = await this.middleware.runGlobal(req, res);
150
+ if (middlewareResult === false || res.writableEnded) return;
151
+
152
+ // Match route
153
+ const match = this.router.match(req.method, req.path);
154
+ if (!match) {
155
+ return this._handleNotFound(req, res);
156
+ }
157
+
158
+ // Set params from route matching
159
+ req.params = match.params;
160
+
161
+ // Run path-specific middleware
162
+ const pathMiddlewareResult = await this.middleware.runPath(req.path, req, res);
163
+ if (pathMiddlewareResult === false || res.writableEnded) return;
164
+
165
+ // Run route handlers (support multiple handlers as middleware chain)
166
+ for (const handler of match.handlers) {
167
+ if (res.writableEnded) break;
168
+ const result = await handler(req, res);
169
+ if (result !== undefined && !res.writableEnded) {
170
+ // Auto-send response if handler returns a value
171
+ if (typeof result === 'object') {
172
+ res.json(result);
173
+ } else {
174
+ res.send(String(result));
175
+ }
176
+ }
177
+ }
178
+
179
+ // Log request
180
+ const duration = Date.now() - startTime;
181
+ if (this.config.get('logging.requests', true)) {
182
+ const statusColor = res.statusCode >= 400 ? '\x1b[31m' : '\x1b[32m';
183
+ console.log(
184
+ ` ${statusColor}${req.method}${'\x1b[0m'} ${req.path} ${'\x1b[2m'}${res.statusCode} ${duration}ms${'\x1b[0m'}`
185
+ );
186
+ }
187
+ } catch (err) {
188
+ this._handleError(err, req, res);
189
+ }
190
+ }
191
+
192
+ // ===== RESPONSE ENHANCEMENT =====
193
+ _enhanceResponse(res, req) {
194
+ // JSON response
195
+ res.json = (data, statusCode = 200) => {
196
+ res.writeHead(statusCode, {
197
+ 'Content-Type': 'application/json',
198
+ 'X-Powered-By': 'VoltJS',
199
+ });
200
+ res.end(JSON.stringify(data));
201
+ };
202
+
203
+ // HTML response
204
+ res.html = (content, statusCode = 200) => {
205
+ res.writeHead(statusCode, {
206
+ 'Content-Type': 'text/html; charset=utf-8',
207
+ 'X-Powered-By': 'VoltJS',
208
+ });
209
+ res.end(content);
210
+ };
211
+
212
+ // Plain text
213
+ res.send = (content, statusCode = 200) => {
214
+ const contentType = typeof content === 'object'
215
+ ? 'application/json'
216
+ : 'text/plain; charset=utf-8';
217
+ res.writeHead(statusCode, {
218
+ 'Content-Type': contentType,
219
+ 'X-Powered-By': 'VoltJS',
220
+ });
221
+ res.end(typeof content === 'object' ? JSON.stringify(content) : String(content));
222
+ };
223
+
224
+ // Status setter (chainable)
225
+ res.status = (code) => {
226
+ res.statusCode = code;
227
+ return res;
228
+ };
229
+
230
+ // Redirect
231
+ res.redirect = (location, statusCode = 302) => {
232
+ res.writeHead(statusCode, { Location: location });
233
+ res.end();
234
+ };
235
+
236
+ // Render template
237
+ res.render = (template, data = {}) => {
238
+ try {
239
+ const html = this.renderer.render(template, { ...data, req });
240
+ res.html(html);
241
+ } catch (err) {
242
+ this._handleError(err, req, res);
243
+ }
244
+ };
245
+
246
+ // React SSR: render a React component to a full HTML page
247
+ // Usage: res.jsx(Component, props, pageOptions)
248
+ // or: res.jsx('ComponentName', props, pageOptions) (if registered)
249
+ res.jsx = (componentOrName, props = {}, pageOptions = {}) => {
250
+ try {
251
+ const html = this.react.renderPage(componentOrName, props, pageOptions);
252
+ const statusCode = pageOptions.statusCode || 200;
253
+ res.html(html, statusCode);
254
+ } catch (err) {
255
+ this._handleError(err, req, res);
256
+ }
257
+ };
258
+
259
+ // Download file
260
+ res.download = (filePath, filename) => {
261
+ const fname = filename || path.basename(filePath);
262
+ res.setHeader('Content-Disposition', `attachment; filename="${fname}"`);
263
+ const stream = fs.createReadStream(filePath);
264
+ stream.pipe(res);
265
+ };
266
+
267
+ // Set cookie
268
+ res.cookie = (name, value, options = {}) => {
269
+ const parts = [`${name}=${encodeURIComponent(value)}`];
270
+ if (options.maxAge) parts.push(`Max-Age=${options.maxAge}`);
271
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
272
+ if (options.path) parts.push(`Path=${options.path}`);
273
+ else parts.push('Path=/');
274
+ if (options.domain) parts.push(`Domain=${options.domain}`);
275
+ if (options.secure) parts.push('Secure');
276
+ if (options.httpOnly !== false) parts.push('HttpOnly');
277
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
278
+ else parts.push('SameSite=Lax');
279
+
280
+ const existing = res.getHeader('Set-Cookie') || [];
281
+ const cookies = Array.isArray(existing) ? existing : [existing];
282
+ cookies.push(parts.join('; '));
283
+ res.setHeader('Set-Cookie', cookies);
284
+ return res;
285
+ };
286
+
287
+ // Clear cookie
288
+ res.clearCookie = (name, options = {}) => {
289
+ return res.cookie(name, '', { ...options, maxAge: 0 });
290
+ };
291
+
292
+ // Send file
293
+ res.sendFile = (filePath) => {
294
+ const ext = path.extname(filePath).toLowerCase();
295
+ const mimeTypes = {
296
+ '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
297
+ '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
298
+ '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml',
299
+ '.ico': 'image/x-icon', '.pdf': 'application/pdf', '.txt': 'text/plain',
300
+ '.xml': 'application/xml', '.mp4': 'video/mp4', '.webm': 'video/webm',
301
+ '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf',
302
+ };
303
+ res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' });
304
+ fs.createReadStream(filePath).pipe(res);
305
+ };
306
+
307
+ // JSONP
308
+ res.jsonp = (data, callback = 'callback') => {
309
+ const cb = req.query[callback] || callback;
310
+ res.writeHead(200, { 'Content-Type': 'application/javascript' });
311
+ res.end(`${cb}(${JSON.stringify(data)})`);
312
+ };
313
+
314
+ // No content
315
+ res.noContent = () => {
316
+ res.writeHead(204);
317
+ res.end();
318
+ };
319
+ }
320
+
321
+ // ===== BODY PARSING =====
322
+ _parseBody(req) {
323
+ return new Promise((resolve, reject) => {
324
+ let body = '';
325
+ const maxSize = this.config.get('server.maxBodySize', 10 * 1024 * 1024); // 10MB default
326
+ let size = 0;
327
+
328
+ req.on('data', (chunk) => {
329
+ size += chunk.length;
330
+ if (size > maxSize) {
331
+ req.destroy();
332
+ reject(new Error('Request body too large'));
333
+ return;
334
+ }
335
+ body += chunk.toString();
336
+ });
337
+
338
+ req.on('end', () => {
339
+ const contentType = req.headers['content-type'] || '';
340
+
341
+ if (contentType.includes('application/json')) {
342
+ try {
343
+ req.body = JSON.parse(body);
344
+ } catch {
345
+ req.body = {};
346
+ }
347
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
348
+ req.body = Object.fromEntries(new URLSearchParams(body));
349
+ } else if (contentType.includes('multipart/form-data')) {
350
+ req.body = {};
351
+ req.rawBody = body;
352
+ // Parse multipart
353
+ this._parseMultipart(req, body, contentType);
354
+ } else {
355
+ req.body = body;
356
+ req.rawBody = body;
357
+ }
358
+ resolve();
359
+ });
360
+
361
+ req.on('error', reject);
362
+ });
363
+ }
364
+
365
+ // ===== MULTIPART PARSING =====
366
+ _parseMultipart(req, body, contentType) {
367
+ const boundaryMatch = contentType.match(/boundary=(.+)/);
368
+ if (!boundaryMatch) return;
369
+
370
+ const boundary = boundaryMatch[1];
371
+ const parts = body.split(`--${boundary}`);
372
+ req.files = {};
373
+ req.body = {};
374
+
375
+ for (const part of parts) {
376
+ if (part.trim() === '' || part.trim() === '--') continue;
377
+
378
+ const headerEnd = part.indexOf('\r\n\r\n');
379
+ if (headerEnd === -1) continue;
380
+
381
+ const headers = part.substring(0, headerEnd);
382
+ const content = part.substring(headerEnd + 4).replace(/\r\n$/, '');
383
+
384
+ const nameMatch = headers.match(/name="([^"]+)"/);
385
+ const filenameMatch = headers.match(/filename="([^"]+)"/);
386
+
387
+ if (nameMatch) {
388
+ if (filenameMatch) {
389
+ const contentTypeMatch = headers.match(/Content-Type:\s*(.+)/i);
390
+ req.files[nameMatch[1]] = {
391
+ name: filenameMatch[1],
392
+ data: Buffer.from(content, 'binary'),
393
+ type: contentTypeMatch ? contentTypeMatch[1].trim() : 'application/octet-stream',
394
+ size: content.length,
395
+ };
396
+ } else {
397
+ req.body[nameMatch[1]] = content.trim();
398
+ }
399
+ }
400
+ }
401
+ }
402
+
403
+ // ===== COOKIE PARSING =====
404
+ _parseCookies(req) {
405
+ req.cookies = {};
406
+ const cookieHeader = req.headers.cookie;
407
+ if (cookieHeader) {
408
+ cookieHeader.split(';').forEach(cookie => {
409
+ const [name, ...rest] = cookie.split('=');
410
+ if (name) {
411
+ req.cookies[name.trim()] = decodeURIComponent(rest.join('=').trim());
412
+ }
413
+ });
414
+ }
415
+ }
416
+
417
+ // ===== STATIC FILE SERVING =====
418
+ async _serveStatic(req, res) {
419
+ if (req.method !== 'GET' && req.method !== 'HEAD') return false;
420
+
421
+ for (const staticDir of this._staticDirs) {
422
+ if (req.path.startsWith(staticDir.url)) {
423
+ const relativePath = req.path.substring(staticDir.url.length);
424
+ const filePath = path.join(staticDir.dir, relativePath);
425
+
426
+ // Security: prevent directory traversal
427
+ if (!filePath.startsWith(staticDir.dir)) {
428
+ return false;
429
+ }
430
+
431
+ try {
432
+ const stat = fs.statSync(filePath);
433
+ if (stat.isFile()) {
434
+ res.sendFile(filePath);
435
+ return true;
436
+ }
437
+ } catch {
438
+ return false;
439
+ }
440
+ }
441
+ }
442
+ return false;
443
+ }
444
+
445
+ // ===== AUTO-DISCOVER ROUTES =====
446
+ _autoDiscoverRoutes() {
447
+ const pagesDir = path.join(process.cwd(), 'pages');
448
+ const apiDir = path.join(process.cwd(), 'api');
449
+
450
+ if (fs.existsSync(pagesDir)) {
451
+ this._discoverFileRoutes(pagesDir, '', 'page');
452
+ }
453
+ if (fs.existsSync(apiDir)) {
454
+ this._discoverFileRoutes(apiDir, '/api', 'api');
455
+ }
456
+ }
457
+
458
+ _discoverFileRoutes(dir, prefix, type) {
459
+ try {
460
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
461
+
462
+ for (const entry of entries) {
463
+ const fullPath = path.join(dir, entry.name);
464
+
465
+ if (entry.isDirectory()) {
466
+ // Handle dynamic routes: [id] -> :id
467
+ const dirName = entry.name.replace(/^\[(.+)\]$/, ':$1');
468
+ this._discoverFileRoutes(fullPath, `${prefix}/${dirName}`, type);
469
+ } else if (entry.isFile()) {
470
+ const ext = path.extname(entry.name);
471
+ if (!['.js', '.ts', '.volt'].includes(ext)) continue;
472
+
473
+ let routePath = entry.name.replace(ext, '');
474
+
475
+ // Handle dynamic routes: [id].js -> :id
476
+ routePath = routePath.replace(/^\[(.+)\]$/, ':$1');
477
+
478
+ // index.js maps to /
479
+ if (routePath === 'index') {
480
+ routePath = '';
481
+ }
482
+
483
+ const fullRoute = `${prefix}/${routePath}`.replace(/\/+/g, '/') || '/';
484
+
485
+ try {
486
+ const handler = require(fullPath);
487
+
488
+ if (type === 'api') {
489
+ // API routes export method handlers
490
+ if (typeof handler === 'function') {
491
+ this.all(fullRoute, handler);
492
+ } else {
493
+ if (handler.GET || handler.get) this.get(fullRoute, handler.GET || handler.get);
494
+ if (handler.POST || handler.post) this.post(fullRoute, handler.POST || handler.post);
495
+ if (handler.PUT || handler.put) this.put(fullRoute, handler.PUT || handler.put);
496
+ if (handler.PATCH || handler.patch) this.patch(fullRoute, handler.PATCH || handler.patch);
497
+ if (handler.DELETE || handler.delete) this.delete(fullRoute, handler.DELETE || handler.delete);
498
+ }
499
+ } else {
500
+ // Page routes
501
+ if (typeof handler === 'function') {
502
+ this.get(fullRoute, handler);
503
+ } else if (handler.default) {
504
+ this.get(fullRoute, handler.default);
505
+ }
506
+ }
507
+ } catch (err) {
508
+ console.warn(`\x1b[33mWarning: Failed to load route ${fullRoute}: ${err.message}\x1b[0m`);
509
+ }
510
+ }
511
+ }
512
+ } catch {
513
+ // Directory doesn't exist or can't be read
514
+ }
515
+ }
516
+
517
+ // ===== DEFAULT SECURITY =====
518
+ _applyDefaultSecurity() {
519
+ const secConfig = this.config.get('security', {});
520
+
521
+ // Always add security headers unless explicitly disabled
522
+ if (secConfig.headers !== false) {
523
+ this.middleware.addGlobal((req, res) => {
524
+ res.setHeader('X-Content-Type-Options', 'nosniff');
525
+ res.setHeader('X-Frame-Options', 'DENY');
526
+ res.setHeader('X-XSS-Protection', '1; mode=block');
527
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
528
+ res.setHeader('X-Powered-By', 'VoltJS');
529
+ if (secConfig.hsts !== false) {
530
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
531
+ }
532
+ });
533
+ }
534
+
535
+ // CORS
536
+ if (secConfig.cors !== false) {
537
+ const corsOptions = typeof secConfig.cors === 'object' ? secConfig.cors : {};
538
+ const { CORSHandler } = require('../security/cors');
539
+ this.middleware.addGlobal(CORSHandler.middleware(corsOptions));
540
+ }
541
+
542
+ // Rate limiting
543
+ if (secConfig.rateLimit !== false) {
544
+ const rlOptions = typeof secConfig.rateLimit === 'object' ? secConfig.rateLimit : {};
545
+ const { RateLimiter } = require('../security/rateLimit');
546
+ this.middleware.addGlobal(RateLimiter.middleware(rlOptions));
547
+ }
548
+
549
+ // XSS Protection
550
+ if (secConfig.xss !== false) {
551
+ const { XSSProtection } = require('../security/xss');
552
+ this.middleware.addGlobal(XSSProtection.middleware());
553
+ }
554
+ }
555
+
556
+ // ===== ERROR HANDLING =====
557
+ _handleNotFound(req, res) {
558
+ const payload = {
559
+ error: 'Not Found',
560
+ message: `Route ${req.method} ${req.path} not found`,
561
+ statusCode: 404,
562
+ };
563
+
564
+ if (req.headers.accept && req.headers.accept.includes('text/html')) {
565
+ res.html(`
566
+ <!DOCTYPE html>
567
+ <html>
568
+ <head><title>404 - Not Found</title>
569
+ <style>
570
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #fff; }
571
+ .container { text-align: center; }
572
+ h1 { font-size: 120px; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
573
+ p { color: #888; font-size: 18px; }
574
+ a { color: #667eea; text-decoration: none; }
575
+ </style></head>
576
+ <body>
577
+ <div class="container">
578
+ <h1>404</h1>
579
+ <p>${req.method} ${req.path} not found</p>
580
+ <a href="/">← Go Home</a>
581
+ </div>
582
+ </body>
583
+ </html>
584
+ `, 404);
585
+ } else {
586
+ res.json(payload, 404);
587
+ }
588
+ }
589
+
590
+ _handleError(err, req, res) {
591
+ // Run error hooks
592
+ for (const hook of this._hooks.onError) {
593
+ try { hook(err, req, res); } catch {}
594
+ }
595
+
596
+ const statusCode = err.statusCode || 500;
597
+ const isDev = this.config.get('env', 'development') === 'development';
598
+
599
+ console.error(`\x1b[31m ERROR: ${err.message}\x1b[0m`);
600
+ if (isDev) console.error(`\x1b[2m${err.stack}\x1b[0m`);
601
+
602
+ if (req.headers.accept && req.headers.accept.includes('text/html')) {
603
+ res.html(`
604
+ <!DOCTYPE html>
605
+ <html>
606
+ <head><title>Error ${statusCode}</title>
607
+ <style>
608
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; padding: 40px; background: #0a0a0a; color: #fff; }
609
+ .error-box { background: #1a1a2e; border: 1px solid #e94560; border-radius: 8px; padding: 24px; max-width: 800px; margin: 0 auto; }
610
+ h1 { color: #e94560; margin-top: 0; }
611
+ pre { background: #16213e; padding: 16px; border-radius: 4px; overflow-x: auto; color: #a8a8a8; }
612
+ .status { color: #e94560; font-size: 48px; font-weight: bold; }
613
+ </style></head>
614
+ <body>
615
+ <div class="error-box">
616
+ <div class="status">${statusCode}</div>
617
+ <h1>${err.message}</h1>
618
+ ${isDev ? `<pre>${err.stack}</pre>` : '<p>An internal error occurred.</p>'}
619
+ </div>
620
+ </body>
621
+ </html>
622
+ `, statusCode);
623
+ } else {
624
+ res.json({
625
+ error: err.message,
626
+ statusCode,
627
+ ...(isDev && { stack: err.stack }),
628
+ }, statusCode);
629
+ }
630
+ }
631
+
632
+ // ===== SERVER =====
633
+ async listen(port, callback) {
634
+ const serverPort = port || this.config.get('port', 3000);
635
+ const host = this.config.get('host', '0.0.0.0');
636
+
637
+ // Run beforeStart hooks
638
+ for (const hook of this._hooks.beforeStart) {
639
+ await hook(this);
640
+ }
641
+
642
+ // Create server
643
+ const sslConfig = this.config.get('ssl');
644
+ if (sslConfig && sslConfig.key && sslConfig.cert) {
645
+ this.server = https.createServer({
646
+ key: fs.readFileSync(sslConfig.key),
647
+ cert: fs.readFileSync(sslConfig.cert),
648
+ }, (req, res) => this._handleRequest(req, res));
649
+ } else {
650
+ this.server = http.createServer((req, res) => this._handleRequest(req, res));
651
+ }
652
+
653
+ // Start listening
654
+ return new Promise((resolve) => {
655
+ this.server.listen(serverPort, host, async () => {
656
+ const protocol = sslConfig ? 'https' : 'http';
657
+ console.log(`
658
+ \x1b[36m\x1b[1m ⚡ VoltJS server running!\x1b[0m
659
+ \x1b[2m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
660
+ \x1b[32m→ Local:\x1b[0m ${protocol}://localhost:${serverPort}
661
+ \x1b[32m→ Network:\x1b[0m ${protocol}://${host}:${serverPort}
662
+ \x1b[32m→ Mode:\x1b[0m ${this.config.get('env', 'development')}
663
+ \x1b[2m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
664
+ `);
665
+
666
+ // Run afterStart hooks
667
+ for (const hook of this._hooks.afterStart) {
668
+ await hook(this);
669
+ }
670
+
671
+ if (callback) callback(this);
672
+ resolve(this);
673
+ });
674
+ });
675
+ }
676
+
677
+ async close() {
678
+ for (const hook of this._hooks.beforeStop) {
679
+ await hook(this);
680
+ }
681
+
682
+ if (this.server) {
683
+ return new Promise((resolve) => {
684
+ this.server.close(async () => {
685
+ for (const hook of this._hooks.afterStop) {
686
+ await hook(this);
687
+ }
688
+ console.log('\x1b[33m ⚡ VoltJS server stopped.\x1b[0m');
689
+ resolve();
690
+ });
691
+ });
692
+ }
693
+ }
694
+
695
+ // ===== UTILITY =====
696
+ getRoutes() {
697
+ return this.router.getRoutes();
698
+ }
699
+ }
700
+
701
+ module.exports = { Volt };