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.
- package/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
package/src/core/app.js
ADDED
|
@@ -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 };
|