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,337 @@
1
+ /**
2
+ * VoltJS Template Renderer
3
+ *
4
+ * Built-in template engine with:
5
+ * - Mustache-like syntax {{ variable }}
6
+ * - Conditionals {#if} {#else} {/if}
7
+ * - Loops {#each items as item} {/each}
8
+ * - Partials {> partial}
9
+ * - Layouts {#layout "base"} {/layout}
10
+ * - Auto-escaping (XSS safe by default)
11
+ * - Raw output {{{ raw }}}
12
+ * - Slots {#slot "name"} {/slot}
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ class Renderer {
21
+ constructor(config) {
22
+ this._viewsDir = config ? config.get('views.dir', 'views') : 'views';
23
+ this._cache = new Map();
24
+ this._cacheEnabled = config ? config.get('views.cache', false) : false;
25
+ this._helpers = new Map();
26
+ this._layouts = new Map();
27
+ this._partials = new Map();
28
+
29
+ // Register default helpers
30
+ this._registerDefaultHelpers();
31
+ }
32
+
33
+ setViewsDir(dir) {
34
+ this._viewsDir = dir;
35
+ }
36
+
37
+ /** Register a template helper */
38
+ helper(name, fn) {
39
+ this._helpers.set(name, fn);
40
+ return this;
41
+ }
42
+
43
+ /** Register a layout */
44
+ layout(name, template) {
45
+ this._layouts.set(name, template);
46
+ return this;
47
+ }
48
+
49
+ /** Register a partial */
50
+ partial(name, template) {
51
+ this._partials.set(name, template);
52
+ return this;
53
+ }
54
+
55
+ /** Render a template string or file */
56
+ render(templateOrPath, data = {}) {
57
+ let template;
58
+
59
+ // Check if it's a file path
60
+ if (!templateOrPath.includes('{{') && !templateOrPath.includes('{#')) {
61
+ template = this._loadTemplate(templateOrPath);
62
+ } else {
63
+ template = templateOrPath;
64
+ }
65
+
66
+ // Add helpers to data
67
+ const context = {
68
+ ...data,
69
+ _helpers: Object.fromEntries(this._helpers),
70
+ };
71
+
72
+ // Process the template
73
+ let result = this._process(template, context);
74
+
75
+ return result;
76
+ }
77
+
78
+ /** Load template from file */
79
+ _loadTemplate(name) {
80
+ if (this._cacheEnabled && this._cache.has(name)) {
81
+ return this._cache.get(name);
82
+ }
83
+
84
+ const extensions = ['.volt', '.html', '.htm', '.vt'];
85
+ const viewsDir = path.resolve(this._viewsDir);
86
+
87
+ for (const ext of extensions) {
88
+ const filePath = path.join(viewsDir, name + ext);
89
+ try {
90
+ if (fs.existsSync(filePath)) {
91
+ const content = fs.readFileSync(filePath, 'utf-8');
92
+ if (this._cacheEnabled) {
93
+ this._cache.set(name, content);
94
+ }
95
+ return content;
96
+ }
97
+ } catch {}
98
+ }
99
+
100
+ // Try without extension
101
+ const directPath = path.join(viewsDir, name);
102
+ try {
103
+ if (fs.existsSync(directPath)) {
104
+ return fs.readFileSync(directPath, 'utf-8');
105
+ }
106
+ } catch {}
107
+
108
+ // If no file found, treat the input as a template string
109
+ return `<!DOCTYPE html>
110
+ <html><head><title>VoltJS</title>
111
+ <style>
112
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 40px; background: #0a0a0a; color: #e0e0e0; }
113
+ .container { max-width: 800px; margin: 0 auto; }
114
+ h1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
115
+ </style></head>
116
+ <body><div class="container"><h1>${name}</h1></div></body></html>`;
117
+ }
118
+
119
+ /** Process template with data */
120
+ _process(template, data) {
121
+ let result = template;
122
+
123
+ // Process layouts: {#layout "name"} content {/layout}
124
+ result = this._processLayouts(result, data);
125
+
126
+ // Process partials: {> partialName}
127
+ result = this._processPartials(result, data);
128
+
129
+ // Process each loops: {#each items as item} ... {/each}
130
+ result = this._processEach(result, data);
131
+
132
+ // Process conditionals: {#if condition} ... {#else} ... {/if}
133
+ result = this._processIf(result, data);
134
+
135
+ // Process raw output: {{{ variable }}}
136
+ result = result.replace(/\{\{\{\s*(.+?)\s*\}\}\}/g, (_, key) => {
137
+ return this._resolve(key.trim(), data) || '';
138
+ });
139
+
140
+ // Process escaped output: {{ variable }}
141
+ result = result.replace(/\{\{\s*(.+?)\s*\}\}/g, (_, key) => {
142
+ const value = this._resolve(key.trim(), data);
143
+ return value !== undefined && value !== null ? this._escape(String(value)) : '';
144
+ });
145
+
146
+ return result;
147
+ }
148
+
149
+ /** Process layout directives */
150
+ _processLayouts(template, data) {
151
+ const layoutRegex = /\{#layout\s+"([^"]+)"\s*\}([\s\S]*?)\{\/layout\}/g;
152
+
153
+ return template.replace(layoutRegex, (_, layoutName, content) => {
154
+ let layout = this._layouts.get(layoutName) || this._loadTemplate(`layouts/${layoutName}`);
155
+
156
+ // Replace {#slot "content"} in layout with the content
157
+ layout = layout.replace(/\{#slot\s+"content"\s*\}[\s\S]*?\{\/slot\}/g, content);
158
+
159
+ // Process named slots
160
+ const slotRegex = /\{#slot\s+"(\w+)"\s*\}([\s\S]*?)\{\/slot\}/g;
161
+ layout = layout.replace(slotRegex, (_, slotName, defaultContent) => {
162
+ const slotContentRegex = new RegExp(`\\{#fill\\s+"${slotName}"\\s*\\}([\\s\\S]*?)\\{\\/fill\\}`, 'g');
163
+ const slotMatch = slotContentRegex.exec(content);
164
+ return slotMatch ? slotMatch[1] : defaultContent;
165
+ });
166
+
167
+ return this._process(layout, data);
168
+ });
169
+ }
170
+
171
+ /** Process partial includes */
172
+ _processPartials(template, data) {
173
+ return template.replace(/\{>\s*(\S+)\s*\}/g, (_, partialName) => {
174
+ const partial = this._partials.get(partialName) || this._loadTemplate(`partials/${partialName}`);
175
+ return this._process(partial, data);
176
+ });
177
+ }
178
+
179
+ /** Process each loops */
180
+ _processEach(template, data) {
181
+ const eachRegex = /\{#each\s+(\S+)\s+as\s+(\S+)\s*\}([\s\S]*?)\{\/each\}/g;
182
+
183
+ return template.replace(eachRegex, (_, arrayKey, itemName, content) => {
184
+ const array = this._resolve(arrayKey, data);
185
+ if (!Array.isArray(array)) return '';
186
+
187
+ return array.map((item, index) => {
188
+ const itemData = {
189
+ ...data,
190
+ [itemName]: item,
191
+ [`${itemName}_index`]: index,
192
+ [`${itemName}_first`]: index === 0,
193
+ [`${itemName}_last`]: index === array.length - 1,
194
+ };
195
+ return this._process(content, itemData);
196
+ }).join('');
197
+ });
198
+ }
199
+
200
+ /** Process conditionals */
201
+ _processIf(template, data) {
202
+ // Handle nested if/else/elseif
203
+ const ifRegex = /\{#if\s+(.+?)\s*\}([\s\S]*?)\{\/if\}/g;
204
+
205
+ return template.replace(ifRegex, (_, condition, content) => {
206
+ // Split by else
207
+ const parts = content.split(/\{#else\s*\}/);
208
+ const ifContent = parts[0];
209
+ const elseContent = parts[1] || '';
210
+
211
+ // Evaluate condition
212
+ const result = this._evaluateCondition(condition, data);
213
+
214
+ const chosen = result ? ifContent : elseContent;
215
+ return this._process(chosen, data);
216
+ });
217
+ }
218
+
219
+ /** Evaluate a condition */
220
+ _evaluateCondition(condition, data) {
221
+ // Handle negation
222
+ if (condition.startsWith('!')) {
223
+ return !this._evaluateCondition(condition.slice(1).trim(), data);
224
+ }
225
+
226
+ // Handle comparison operators
227
+ const operators = ['===', '!==', '>=', '<=', '>', '<', '==', '!='];
228
+ for (const op of operators) {
229
+ if (condition.includes(op)) {
230
+ const [left, right] = condition.split(op).map(s => s.trim());
231
+ const leftVal = this._resolve(left, data) ?? left;
232
+ const rightVal = this._resolve(right, data) ?? right;
233
+
234
+ // Remove quotes from string literals
235
+ const cleanLeft = typeof leftVal === 'string' ? leftVal.replace(/^['"]|['"]$/g, '') : leftVal;
236
+ const cleanRight = typeof rightVal === 'string' ? rightVal.replace(/^['"]|['"]$/g, '') : rightVal;
237
+
238
+ switch (op) {
239
+ case '===': case '==': return cleanLeft == cleanRight;
240
+ case '!==': case '!=': return cleanLeft != cleanRight;
241
+ case '>': return cleanLeft > cleanRight;
242
+ case '<': return cleanLeft < cleanRight;
243
+ case '>=': return cleanLeft >= cleanRight;
244
+ case '<=': return cleanLeft <= cleanRight;
245
+ }
246
+ }
247
+ }
248
+
249
+ // Handle && and ||
250
+ if (condition.includes('&&')) {
251
+ return condition.split('&&').every(c => this._evaluateCondition(c.trim(), data));
252
+ }
253
+ if (condition.includes('||')) {
254
+ return condition.split('||').some(c => this._evaluateCondition(c.trim(), data));
255
+ }
256
+
257
+ // Simple truthiness check
258
+ const value = this._resolve(condition, data);
259
+ return !!value;
260
+ }
261
+
262
+ /** Resolve a dot-notation key from data */
263
+ _resolve(key, data) {
264
+ // Handle string literals
265
+ if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
266
+ return key.slice(1, -1);
267
+ }
268
+
269
+ // Handle numbers
270
+ if (!isNaN(key)) return Number(key);
271
+
272
+ // Handle booleans
273
+ if (key === 'true') return true;
274
+ if (key === 'false') return false;
275
+ if (key === 'null') return null;
276
+
277
+ // Handle helper calls: helperName(arg1, arg2)
278
+ const helperMatch = key.match(/^(\w+)\((.+)\)$/);
279
+ if (helperMatch) {
280
+ const helperName = helperMatch[1];
281
+ const helper = this._helpers.get(helperName);
282
+ if (helper) {
283
+ const args = helperMatch[2].split(',').map(a => {
284
+ const trimmed = a.trim();
285
+ return this._resolve(trimmed, data);
286
+ });
287
+ return helper(...args);
288
+ }
289
+ }
290
+
291
+ // Dot-notation resolution
292
+ const parts = key.split('.');
293
+ let value = data;
294
+ for (const part of parts) {
295
+ if (value === undefined || value === null) return undefined;
296
+ value = value[part];
297
+ }
298
+ return value;
299
+ }
300
+
301
+ /** HTML escape (XSS protection) */
302
+ _escape(str) {
303
+ const escapeMap = {
304
+ '&': '&amp;',
305
+ '<': '&lt;',
306
+ '>': '&gt;',
307
+ '"': '&quot;',
308
+ "'": '&#x27;',
309
+ '`': '&#x60;',
310
+ };
311
+ return str.replace(/[&<>"'`]/g, c => escapeMap[c]);
312
+ }
313
+
314
+ /** Register default helpers */
315
+ _registerDefaultHelpers() {
316
+ this.helper('uppercase', (str) => String(str).toUpperCase());
317
+ this.helper('lowercase', (str) => String(str).toLowerCase());
318
+ this.helper('capitalize', (str) => String(str).charAt(0).toUpperCase() + String(str).slice(1));
319
+ this.helper('truncate', (str, len) => {
320
+ const s = String(str);
321
+ return s.length > len ? s.substring(0, len) + '...' : s;
322
+ });
323
+ this.helper('date', (d) => new Date(d).toLocaleDateString());
324
+ this.helper('json', (v) => JSON.stringify(v, null, 2));
325
+ this.helper('length', (arr) => Array.isArray(arr) ? arr.length : String(arr).length);
326
+ this.helper('currency', (num, symbol = '$') => `${symbol}${Number(num).toFixed(2)}`);
327
+ this.helper('pluralize', (count, singular, plural) => count === 1 ? singular : (plural || singular + 's'));
328
+ this.helper('default', (val, defaultVal) => val || defaultVal);
329
+ }
330
+
331
+ /** Clear template cache */
332
+ clearCache() {
333
+ this._cache.clear();
334
+ }
335
+ }
336
+
337
+ module.exports = { Renderer };
@@ -0,0 +1,183 @@
1
+ /**
2
+ * VoltJS Router
3
+ *
4
+ * High-performance router with support for:
5
+ * - Dynamic parameters (:id)
6
+ * - Wildcards (*)
7
+ * - Route groups with prefixes
8
+ * - Named routes
9
+ * - Route middleware
10
+ * - Optional parameters (:id?)
11
+ * - Regex constraints
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ class Router {
17
+ constructor(prefix = '') {
18
+ this._routes = [];
19
+ this._prefix = prefix;
20
+ this._namedRoutes = new Map();
21
+ this._routeMiddleware = new Map();
22
+ }
23
+
24
+ /**
25
+ * Add a route
26
+ */
27
+ add(method, routePath, handlers) {
28
+ const fullPath = this._normalizePath(this._prefix + routePath);
29
+ const route = {
30
+ method: method.toUpperCase(),
31
+ path: fullPath,
32
+ handlers: Array.isArray(handlers) ? handlers : [handlers],
33
+ pattern: this._buildPattern(fullPath),
34
+ paramNames: this._extractParamNames(fullPath),
35
+ name: null,
36
+ };
37
+
38
+ this._routes.push(route);
39
+
40
+ // Return chainable object for naming routes
41
+ return {
42
+ name: (routeName) => {
43
+ route.name = routeName;
44
+ this._namedRoutes.set(routeName, route);
45
+ return this;
46
+ },
47
+ middleware: (...mw) => {
48
+ route.handlers = [...mw, ...route.handlers];
49
+ return this;
50
+ },
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Match a request to a route
56
+ */
57
+ match(method, requestPath) {
58
+ const normalizedPath = this._normalizePath(requestPath);
59
+
60
+ for (const route of this._routes) {
61
+ if (route.method !== method.toUpperCase() && route.method !== 'ALL') {
62
+ continue;
63
+ }
64
+
65
+ const match = normalizedPath.match(route.pattern);
66
+ if (match) {
67
+ const params = {};
68
+ route.paramNames.forEach((name, i) => {
69
+ params[name] = decodeURIComponent(match[i + 1]);
70
+ });
71
+
72
+ return {
73
+ route,
74
+ params,
75
+ handlers: route.handlers.filter(h => typeof h === 'function'),
76
+ path: route.path,
77
+ };
78
+ }
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Create a route group with shared prefix
86
+ */
87
+ group(prefix) {
88
+ const groupRouter = new Router(this._prefix + prefix);
89
+ groupRouter._routes = this._routes; // Share routes array
90
+ groupRouter._namedRoutes = this._namedRoutes;
91
+
92
+ return {
93
+ get: (path, ...handlers) => groupRouter.add('GET', path, handlers),
94
+ post: (path, ...handlers) => groupRouter.add('POST', path, handlers),
95
+ put: (path, ...handlers) => groupRouter.add('PUT', path, handlers),
96
+ patch: (path, ...handlers) => groupRouter.add('PATCH', path, handlers),
97
+ delete: (path, ...handlers) => groupRouter.add('DELETE', path, handlers),
98
+ all: (path, ...handlers) => groupRouter.add('ALL', path, handlers),
99
+ group: (subPrefix) => groupRouter.group(subPrefix),
100
+ resource: (name, controller) => {
101
+ const base = `/${name}`;
102
+ groupRouter.add('GET', base, [controller.index || controller.list]);
103
+ groupRouter.add('GET', `${base}/:id`, [controller.show || controller.get]);
104
+ groupRouter.add('POST', base, [controller.create || controller.store]);
105
+ groupRouter.add('PUT', `${base}/:id`, [controller.update]);
106
+ groupRouter.add('PATCH', `${base}/:id`, [controller.update]);
107
+ groupRouter.add('DELETE', `${base}/:id`, [controller.destroy || controller.remove || controller.delete]);
108
+ },
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Get URL for named route
114
+ */
115
+ url(name, params = {}) {
116
+ const route = this._namedRoutes.get(name);
117
+ if (!route) throw new Error(`Route "${name}" not found`);
118
+
119
+ let url = route.path;
120
+ for (const [key, value] of Object.entries(params)) {
121
+ url = url.replace(`:${key}`, encodeURIComponent(value));
122
+ }
123
+ return url;
124
+ }
125
+
126
+ /**
127
+ * Get all registered routes
128
+ */
129
+ getRoutes() {
130
+ return this._routes.map(r => ({
131
+ method: r.method,
132
+ path: r.path,
133
+ name: r.name,
134
+ handlers: r.handlers.length,
135
+ }));
136
+ }
137
+
138
+ // ===== INTERNAL =====
139
+
140
+ _normalizePath(p) {
141
+ return '/' + p.split('/').filter(Boolean).join('/');
142
+ }
143
+
144
+ _buildPattern(routePath) {
145
+ const parts = routePath.split('/').filter(Boolean);
146
+ const patternParts = parts.map(part => {
147
+ // Wildcard
148
+ if (part === '*') return '(.+)';
149
+
150
+ // Optional parameter :name?
151
+ if (part.startsWith(':') && part.endsWith('?')) {
152
+ const paramName = part.slice(1, -1);
153
+ return '([^/]*)?';
154
+ }
155
+
156
+ // Dynamic parameter :name
157
+ if (part.startsWith(':')) {
158
+ return '([^/]+)';
159
+ }
160
+
161
+ // Literal
162
+ return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
163
+ });
164
+
165
+ const pattern = '^/' + patternParts.join('/') + '/?$';
166
+ return new RegExp(pattern, 'i');
167
+ }
168
+
169
+ _extractParamNames(routePath) {
170
+ const names = [];
171
+ const parts = routePath.split('/').filter(Boolean);
172
+ for (const part of parts) {
173
+ if (part === '*') {
174
+ names.push('wildcard');
175
+ } else if (part.startsWith(':')) {
176
+ names.push(part.slice(1).replace('?', ''));
177
+ }
178
+ }
179
+ return names;
180
+ }
181
+ }
182
+
183
+ module.exports = { Router };