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/api/rest.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS REST API Builder
|
|
3
|
+
*
|
|
4
|
+
* Rapid REST API scaffolding with automatic CRUD, resource routing,
|
|
5
|
+
* versioning, and response formatting.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { RestAPI } = require('voltjs');
|
|
9
|
+
*
|
|
10
|
+
* const api = new RestAPI(app, { prefix: '/api/v1' });
|
|
11
|
+
*
|
|
12
|
+
* api.resource('users', {
|
|
13
|
+
* async index(req, res) { res.json(await User.all()); },
|
|
14
|
+
* async show(req, res) { res.json(await User.find(req.params.id)); },
|
|
15
|
+
* async store(req, res) { res.json(await User.create(req.body), 201); },
|
|
16
|
+
* async update(req, res) { res.json(await User.updateById(req.params.id, req.body)); },
|
|
17
|
+
* async destroy(req, res) { await User.deleteById(req.params.id); res.noContent(); },
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
class RestAPI {
|
|
24
|
+
constructor(app, options = {}) {
|
|
25
|
+
this.app = app;
|
|
26
|
+
this.prefix = options.prefix || '/api';
|
|
27
|
+
this.version = options.version || null;
|
|
28
|
+
this.middleware = options.middleware || [];
|
|
29
|
+
this.resources = new Map();
|
|
30
|
+
|
|
31
|
+
// Default response transformer
|
|
32
|
+
this.transformer = options.transformer || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Define a full REST resource */
|
|
36
|
+
resource(name, handlers, options = {}) {
|
|
37
|
+
const base = `${this.prefix}/${name}`;
|
|
38
|
+
const middleware = [...this.middleware, ...(options.middleware || [])];
|
|
39
|
+
|
|
40
|
+
const routes = {
|
|
41
|
+
index: { method: 'get', path: base, handler: handlers.index },
|
|
42
|
+
store: { method: 'post', path: base, handler: handlers.store || handlers.create },
|
|
43
|
+
show: { method: 'get', path: `${base}/:id`, handler: handlers.show },
|
|
44
|
+
update: { method: 'put', path: `${base}/:id`, handler: handlers.update },
|
|
45
|
+
patch: { method: 'patch', path: `${base}/:id`, handler: handlers.patch || handlers.update },
|
|
46
|
+
destroy: { method: 'delete', path: `${base}/:id`, handler: handlers.destroy || handlers.delete },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const [action, route] of Object.entries(routes)) {
|
|
50
|
+
if (route.handler) {
|
|
51
|
+
const only = options.only;
|
|
52
|
+
const except = options.except;
|
|
53
|
+
if (only && !only.includes(action)) continue;
|
|
54
|
+
if (except && except.includes(action)) continue;
|
|
55
|
+
|
|
56
|
+
this.app[route.method](route.path, ...middleware, this._wrap(route.handler));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Custom actions
|
|
61
|
+
if (handlers.custom) {
|
|
62
|
+
for (const [method, path, handler] of handlers.custom) {
|
|
63
|
+
this.app[method](`${base}${path}`, ...middleware, this._wrap(handler));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.resources.set(name, { base, routes });
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Define a single route */
|
|
72
|
+
route(method, path, handler, ...middleware) {
|
|
73
|
+
const fullPath = `${this.prefix}${path}`;
|
|
74
|
+
this.app[method](fullPath, ...this.middleware, ...middleware, this._wrap(handler));
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** GET shorthand */
|
|
79
|
+
get(path, handler, ...mw) { return this.route('get', path, handler, ...mw); }
|
|
80
|
+
/** POST shorthand */
|
|
81
|
+
post(path, handler, ...mw) { return this.route('post', path, handler, ...mw); }
|
|
82
|
+
/** PUT shorthand */
|
|
83
|
+
put(path, handler, ...mw) { return this.route('put', path, handler, ...mw); }
|
|
84
|
+
/** PATCH shorthand */
|
|
85
|
+
patch(path, handler, ...mw) { return this.route('patch', path, handler, ...mw); }
|
|
86
|
+
/** DELETE shorthand */
|
|
87
|
+
delete(path, handler, ...mw) { return this.route('delete', path, handler, ...mw); }
|
|
88
|
+
|
|
89
|
+
/** Create an API group with shared prefix and middleware */
|
|
90
|
+
group(prefix, middleware, fn) {
|
|
91
|
+
if (typeof middleware === 'function') {
|
|
92
|
+
fn = middleware;
|
|
93
|
+
middleware = [];
|
|
94
|
+
}
|
|
95
|
+
const sub = new RestAPI(this.app, {
|
|
96
|
+
prefix: `${this.prefix}${prefix}`,
|
|
97
|
+
middleware: [...this.middleware, ...middleware],
|
|
98
|
+
transformer: this.transformer,
|
|
99
|
+
});
|
|
100
|
+
fn(sub);
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** API versioning */
|
|
105
|
+
version(v, fn) {
|
|
106
|
+
return this.group(`/v${v}`, fn);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Standard success response */
|
|
110
|
+
static success(res, data, statusCode = 200, meta = {}) {
|
|
111
|
+
const response = {
|
|
112
|
+
success: true,
|
|
113
|
+
data,
|
|
114
|
+
...(Object.keys(meta).length > 0 ? { meta } : {}),
|
|
115
|
+
};
|
|
116
|
+
res.json(response, statusCode);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Standard error response */
|
|
120
|
+
static error(res, message, statusCode = 400, errors = null) {
|
|
121
|
+
const response = {
|
|
122
|
+
success: false,
|
|
123
|
+
error: { message, code: statusCode },
|
|
124
|
+
...(errors ? { errors } : {}),
|
|
125
|
+
};
|
|
126
|
+
res.json(response, statusCode);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Paginated response */
|
|
130
|
+
static paginated(res, data, meta) {
|
|
131
|
+
res.json({
|
|
132
|
+
success: true,
|
|
133
|
+
data,
|
|
134
|
+
meta: {
|
|
135
|
+
page: meta.page,
|
|
136
|
+
perPage: meta.perPage,
|
|
137
|
+
total: meta.total,
|
|
138
|
+
totalPages: meta.totalPages,
|
|
139
|
+
hasNext: meta.hasNext,
|
|
140
|
+
hasPrev: meta.hasPrev,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** List all registered API routes */
|
|
146
|
+
listRoutes() {
|
|
147
|
+
const routes = [];
|
|
148
|
+
for (const [name, resource] of this.resources) {
|
|
149
|
+
for (const [action, route] of Object.entries(resource.routes)) {
|
|
150
|
+
if (route.handler) {
|
|
151
|
+
routes.push({
|
|
152
|
+
resource: name,
|
|
153
|
+
action,
|
|
154
|
+
method: route.method.toUpperCase(),
|
|
155
|
+
path: route.path,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return routes;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ===== INTERNAL =====
|
|
164
|
+
|
|
165
|
+
_wrap(handler) {
|
|
166
|
+
return async (req, res) => {
|
|
167
|
+
try {
|
|
168
|
+
const result = await handler(req, res);
|
|
169
|
+
// Auto-send if handler returns a value but didn't respond
|
|
170
|
+
if (result !== undefined && !res.writableEnded) {
|
|
171
|
+
if (this.transformer) {
|
|
172
|
+
res.json(this.transformer(result));
|
|
173
|
+
} else {
|
|
174
|
+
res.json(result);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!res.writableEnded) {
|
|
179
|
+
const status = error.statusCode || error.status || 500;
|
|
180
|
+
RestAPI.error(res, error.message || 'Internal Server Error', status);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** API Error helper */
|
|
188
|
+
class ApiError extends Error {
|
|
189
|
+
constructor(message, statusCode = 400) {
|
|
190
|
+
super(message);
|
|
191
|
+
this.statusCode = statusCode;
|
|
192
|
+
this.name = 'ApiError';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static badRequest(msg = 'Bad Request') { return new ApiError(msg, 400); }
|
|
196
|
+
static unauthorized(msg = 'Unauthorized') { return new ApiError(msg, 401); }
|
|
197
|
+
static forbidden(msg = 'Forbidden') { return new ApiError(msg, 403); }
|
|
198
|
+
static notFound(msg = 'Not Found') { return new ApiError(msg, 404); }
|
|
199
|
+
static conflict(msg = 'Conflict') { return new ApiError(msg, 409); }
|
|
200
|
+
static validation(msg = 'Validation Failed') { return new ApiError(msg, 422); }
|
|
201
|
+
static internal(msg = 'Internal Server Error') { return new ApiError(msg, 500); }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { RestAPI, ApiError };
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS WebSocket Server
|
|
3
|
+
*
|
|
4
|
+
* Real-time WebSocket server with rooms, broadcasting, authentication,
|
|
5
|
+
* and automatic heartbeat.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { WebSocketServer } = require('voltjs');
|
|
9
|
+
*
|
|
10
|
+
* const wss = new WebSocketServer(app, { path: '/ws' });
|
|
11
|
+
*
|
|
12
|
+
* wss.on('connection', (client) => {
|
|
13
|
+
* client.join('lobby');
|
|
14
|
+
* client.on('chat', (data) => {
|
|
15
|
+
* wss.to('lobby').emit('chat', { user: client.id, ...data });
|
|
16
|
+
* });
|
|
17
|
+
* });
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const { EventEmitter } = require('events');
|
|
24
|
+
|
|
25
|
+
class WebSocketServer extends EventEmitter {
|
|
26
|
+
constructor(app, options = {}) {
|
|
27
|
+
super();
|
|
28
|
+
this.path = options.path || '/ws';
|
|
29
|
+
this.clients = new Map();
|
|
30
|
+
this.rooms = new Map();
|
|
31
|
+
this.heartbeatInterval = options.heartbeat || 30000;
|
|
32
|
+
this.authHandler = options.auth || null;
|
|
33
|
+
this._wss = null;
|
|
34
|
+
|
|
35
|
+
if (app && app._server) {
|
|
36
|
+
this._attachToServer(app._server);
|
|
37
|
+
} else if (app) {
|
|
38
|
+
// Defer attachment until server starts
|
|
39
|
+
const originalListen = app.listen?.bind(app);
|
|
40
|
+
if (originalListen) {
|
|
41
|
+
app.listen = (...args) => {
|
|
42
|
+
const server = originalListen(...args);
|
|
43
|
+
this._attachToServer(server);
|
|
44
|
+
return server;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_attachToServer(server) {
|
|
51
|
+
try {
|
|
52
|
+
const { WebSocketServer: WSS } = require('ws');
|
|
53
|
+
this._wss = new WSS({ server, path: this.path });
|
|
54
|
+
|
|
55
|
+
this._wss.on('connection', (ws, req) => this._onConnection(ws, req));
|
|
56
|
+
|
|
57
|
+
// Heartbeat timer
|
|
58
|
+
this._heartbeatTimer = setInterval(() => this._heartbeat(), this.heartbeatInterval);
|
|
59
|
+
if (this._heartbeatTimer.unref) this._heartbeatTimer.unref();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.warn('[Volt] WebSocket: "ws" package not installed. Run: npm install ws');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async _onConnection(ws, req) {
|
|
66
|
+
const client = new WebSocketClient(ws, req, this);
|
|
67
|
+
|
|
68
|
+
// Authentication
|
|
69
|
+
if (this.authHandler) {
|
|
70
|
+
try {
|
|
71
|
+
const authResult = await this.authHandler(req, client);
|
|
72
|
+
if (authResult === false) {
|
|
73
|
+
ws.close(4001, 'Unauthorized');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (typeof authResult === 'object') {
|
|
77
|
+
client.user = authResult;
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
ws.close(4001, 'Auth failed');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.clients.set(client.id, client);
|
|
86
|
+
|
|
87
|
+
// Message handling
|
|
88
|
+
ws.on('message', (raw) => {
|
|
89
|
+
try {
|
|
90
|
+
const message = JSON.parse(raw.toString());
|
|
91
|
+
const event = message.event || 'message';
|
|
92
|
+
const data = message.data || message;
|
|
93
|
+
|
|
94
|
+
client.emit(event, data);
|
|
95
|
+
this.emit(event, data, client);
|
|
96
|
+
} catch {
|
|
97
|
+
client.emit('message', raw.toString());
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
ws.on('close', () => {
|
|
102
|
+
// Leave all rooms
|
|
103
|
+
for (const [roomName, members] of this.rooms) {
|
|
104
|
+
members.delete(client.id);
|
|
105
|
+
if (members.size === 0) this.rooms.delete(roomName);
|
|
106
|
+
}
|
|
107
|
+
this.clients.delete(client.id);
|
|
108
|
+
this.emit('disconnect', client);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
ws.on('error', (err) => {
|
|
112
|
+
this.emit('error', err, client);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
ws.isAlive = true;
|
|
116
|
+
ws.on('pong', () => { ws.isAlive = true; });
|
|
117
|
+
|
|
118
|
+
this.emit('connection', client);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Broadcast to all connected clients */
|
|
122
|
+
broadcast(event, data, exclude = null) {
|
|
123
|
+
const payload = JSON.stringify({ event, data });
|
|
124
|
+
for (const [id, client] of this.clients) {
|
|
125
|
+
if (exclude && (exclude === id || exclude === client)) continue;
|
|
126
|
+
client._send(payload);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Send to a specific room */
|
|
131
|
+
to(room) {
|
|
132
|
+
return {
|
|
133
|
+
emit: (event, data) => {
|
|
134
|
+
const members = this.rooms.get(room);
|
|
135
|
+
if (!members) return;
|
|
136
|
+
const payload = JSON.stringify({ event, data });
|
|
137
|
+
for (const clientId of members) {
|
|
138
|
+
const client = this.clients.get(clientId);
|
|
139
|
+
if (client) client._send(payload);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
clients: () => {
|
|
143
|
+
const members = this.rooms.get(room);
|
|
144
|
+
if (!members) return [];
|
|
145
|
+
return [...members].map(id => this.clients.get(id)).filter(Boolean);
|
|
146
|
+
},
|
|
147
|
+
count: () => {
|
|
148
|
+
return this.rooms.get(room)?.size || 0;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Get a client by ID */
|
|
154
|
+
client(id) {
|
|
155
|
+
return this.clients.get(id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Get all connected client IDs */
|
|
159
|
+
get clientIds() {
|
|
160
|
+
return [...this.clients.keys()];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Get connected client count */
|
|
164
|
+
get clientCount() {
|
|
165
|
+
return this.clients.size;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Get all room names */
|
|
169
|
+
get roomNames() {
|
|
170
|
+
return [...this.rooms.keys()];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Require authentication */
|
|
174
|
+
requireAuth(handler) {
|
|
175
|
+
this.authHandler = handler;
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Close the WebSocket server */
|
|
180
|
+
close() {
|
|
181
|
+
if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
|
|
182
|
+
if (this._wss) this._wss.close();
|
|
183
|
+
this.clients.clear();
|
|
184
|
+
this.rooms.clear();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_heartbeat() {
|
|
188
|
+
if (!this._wss) return;
|
|
189
|
+
for (const [id, client] of this.clients) {
|
|
190
|
+
if (!client._ws.isAlive) {
|
|
191
|
+
client._ws.terminate();
|
|
192
|
+
this.clients.delete(id);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
client._ws.isAlive = false;
|
|
196
|
+
client._ws.ping();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class WebSocketClient extends EventEmitter {
|
|
202
|
+
constructor(ws, req, server) {
|
|
203
|
+
super();
|
|
204
|
+
this._ws = ws;
|
|
205
|
+
this._server = server;
|
|
206
|
+
this.id = crypto.randomBytes(8).toString('hex');
|
|
207
|
+
this.ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress;
|
|
208
|
+
this.user = null;
|
|
209
|
+
this.data = {}; // Custom per-client data
|
|
210
|
+
|
|
211
|
+
// Parse URL params
|
|
212
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
213
|
+
this.query = Object.fromEntries(url.searchParams);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Send event+data to this client */
|
|
217
|
+
emit(event, data) {
|
|
218
|
+
if (event === 'newListener' || event === 'removeListener') {
|
|
219
|
+
return super.emit(event, data);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If there are listeners, fire them (internal event)
|
|
223
|
+
if (this.listenerCount(event) > 0) {
|
|
224
|
+
super.emit(event, data);
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Otherwise, send to the WebSocket
|
|
229
|
+
this._send(JSON.stringify({ event, data }));
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Send raw data */
|
|
234
|
+
send(data) {
|
|
235
|
+
this._send(typeof data === 'string' ? data : JSON.stringify(data));
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Join a room */
|
|
240
|
+
join(room) {
|
|
241
|
+
if (!this._server.rooms.has(room)) {
|
|
242
|
+
this._server.rooms.set(room, new Set());
|
|
243
|
+
}
|
|
244
|
+
this._server.rooms.get(room).add(this.id);
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Leave a room */
|
|
249
|
+
leave(room) {
|
|
250
|
+
const members = this._server.rooms.get(room);
|
|
251
|
+
if (members) {
|
|
252
|
+
members.delete(this.id);
|
|
253
|
+
if (members.size === 0) this._server.rooms.delete(room);
|
|
254
|
+
}
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Get rooms this client is in */
|
|
259
|
+
get rooms() {
|
|
260
|
+
const rooms = [];
|
|
261
|
+
for (const [name, members] of this._server.rooms) {
|
|
262
|
+
if (members.has(this.id)) rooms.push(name);
|
|
263
|
+
}
|
|
264
|
+
return rooms;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Broadcast to all except this client */
|
|
268
|
+
broadcast(event, data) {
|
|
269
|
+
this._server.broadcast(event, data, this.id);
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Disconnect this client */
|
|
274
|
+
disconnect(code = 1000, reason = '') {
|
|
275
|
+
this._ws.close(code, reason);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_send(payload) {
|
|
279
|
+
if (this._ws.readyState === 1) { // OPEN
|
|
280
|
+
this._ws.send(payload);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = { WebSocketServer, WebSocketClient };
|
package/src/cli/build.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS CLI — `volt build`
|
|
3
|
+
* Prepares the application for production.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const c = {
|
|
12
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
13
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
module.exports = function build(args) {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const distDir = path.join(cwd, 'dist');
|
|
19
|
+
|
|
20
|
+
console.log(`${c.cyan}${c.bold}⚡ VoltJS Build${c.reset}\n`);
|
|
21
|
+
|
|
22
|
+
// Clean dist
|
|
23
|
+
if (fs.existsSync(distDir)) {
|
|
24
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// Copy relevant files to dist
|
|
29
|
+
const copyDirs = ['pages', 'api', 'views', 'components', 'models', 'middleware', 'config', 'public', 'database'];
|
|
30
|
+
const copyFiles = ['app.js', 'index.js', 'server.js', 'volt.config.js', 'package.json'];
|
|
31
|
+
|
|
32
|
+
let fileCount = 0;
|
|
33
|
+
|
|
34
|
+
for (const dir of copyDirs) {
|
|
35
|
+
const src = path.join(cwd, dir);
|
|
36
|
+
if (fs.existsSync(src)) {
|
|
37
|
+
copyRecursive(src, path.join(distDir, dir));
|
|
38
|
+
console.log(` ${c.green}✓${c.reset} ${dir}/`);
|
|
39
|
+
fileCount++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const file of copyFiles) {
|
|
44
|
+
const src = path.join(cwd, file);
|
|
45
|
+
if (fs.existsSync(src)) {
|
|
46
|
+
fs.copyFileSync(src, path.join(distDir, file));
|
|
47
|
+
console.log(` ${c.green}✓${c.reset} ${file}`);
|
|
48
|
+
fileCount++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create production-ready package.json
|
|
53
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
54
|
+
if (fs.existsSync(pkgPath)) {
|
|
55
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
56
|
+
const prodPkg = {
|
|
57
|
+
name: pkg.name,
|
|
58
|
+
version: pkg.version,
|
|
59
|
+
private: true,
|
|
60
|
+
scripts: { start: 'node app.js' },
|
|
61
|
+
dependencies: pkg.dependencies || {},
|
|
62
|
+
};
|
|
63
|
+
fs.writeFileSync(path.join(distDir, 'package.json'), JSON.stringify(prodPkg, null, 2));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Minify CSS files
|
|
67
|
+
const cssDir = path.join(distDir, 'public', 'css');
|
|
68
|
+
if (fs.existsSync(cssDir)) {
|
|
69
|
+
const cssFiles = fs.readdirSync(cssDir).filter(f => f.endsWith('.css'));
|
|
70
|
+
for (const file of cssFiles) {
|
|
71
|
+
const content = fs.readFileSync(path.join(cssDir, file), 'utf-8');
|
|
72
|
+
const minified = content
|
|
73
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
|
|
74
|
+
.replace(/\s+/g, ' ') // Collapse whitespace
|
|
75
|
+
.replace(/\s*([{}:;,])\s*/g, '$1') // Remove spaces around symbols
|
|
76
|
+
.trim();
|
|
77
|
+
fs.writeFileSync(path.join(cssDir, file), minified);
|
|
78
|
+
}
|
|
79
|
+
console.log(` ${c.green}✓${c.reset} CSS minified`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Create necessary dirs
|
|
83
|
+
for (const dir of ['logs', 'storage']) {
|
|
84
|
+
fs.mkdirSync(path.join(distDir, dir), { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`
|
|
88
|
+
${c.green}${c.bold}✓ Build complete!${c.reset} ${c.dim}(${fileCount} items)${c.reset}
|
|
89
|
+
|
|
90
|
+
${c.bold}To deploy:${c.reset}
|
|
91
|
+
${c.cyan}cd dist${c.reset}
|
|
92
|
+
${c.cyan}npm install --production${c.reset}
|
|
93
|
+
${c.cyan}npm start${c.reset}
|
|
94
|
+
`);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function copyRecursive(src, dest) {
|
|
98
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
99
|
+
|
|
100
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const srcPath = path.join(src, entry.name);
|
|
103
|
+
const destPath = path.join(dest, entry.name);
|
|
104
|
+
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
copyRecursive(srcPath, destPath);
|
|
107
|
+
} else {
|
|
108
|
+
fs.copyFileSync(srcPath, destPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|