Initial commit
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
! /server/actions.js
|
||||
? Dynamic action loader for Socket.IO events using a recursive file system scan.
|
||||
*/
|
||||
|
||||
// % imports
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// % Target directory where the actual action files reside
|
||||
const actionsDir = path.join(__dirname, '..', 'actions');
|
||||
|
||||
// $ debug-output
|
||||
console.info('[Paths] Actions:', actionsDir);
|
||||
|
||||
/*
|
||||
$ Recursive function to load action files and build a flat map for direct dot-notation access
|
||||
= Maps all JavaScript files inside the actions directory into a flat key-value structure (e.g. 'chat.message').
|
||||
|
||||
^ Usage in WebSocket (Socket.io) Handlers:
|
||||
?? import actions from './actions/index.js';
|
||||
? Example: "socket.on('chat.message', (data) => actions['chat.message'](socket, data));"
|
||||
? Example: "socket.on('system.ping', () => actions['system.ping'](socket));"
|
||||
*/
|
||||
|
||||
async function loadActions(dir = actionsDir, baseDir = actionsDir, actionsObj = {}) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// ~ Pass the main object (actionsObj) further down recursively
|
||||
await loadActions(fullPath, baseDir, actionsObj);
|
||||
} else if (entry.isFile() && entry.name !== 'index.js' && !entry.name.startsWith('.') && entry.name.endsWith('.js')) {
|
||||
// ~ Calculate the relative path from the root directory
|
||||
const relativePath = path.relative(baseDir, fullPath);
|
||||
const parts = relativePath.split(path.sep).map(p => p.replace(/\.js$/, ''));
|
||||
|
||||
// ~ Import the module
|
||||
const fileUrl = pathToFileURL(fullPath).href;
|
||||
const module = await import(fileUrl);
|
||||
|
||||
// ~ Map the module directly as a flat string key (e.g. "chat.message") into the main object
|
||||
const eventName = parts.join('.');
|
||||
actionsObj[eventName] = module.default || module;
|
||||
}
|
||||
}
|
||||
return actionsObj;
|
||||
}
|
||||
|
||||
// $ Initialize asynchronously and export the loaded actions object
|
||||
const actions = await loadActions();
|
||||
export default actions;
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
! /server/index.js
|
||||
? Main entry point initializing Express, Socket.IO, and dynamic autoloaders for routes and actions.
|
||||
*/
|
||||
|
||||
// % imports
|
||||
import http from 'http';
|
||||
import express from 'express';
|
||||
import { Server as SocketServer } from 'socket.io';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dbWorker from '../database/index.js'; // import the database worker & factory
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/*
|
||||
= The Socket.IO Action-Autoloader:
|
||||
|
||||
^ How it works:
|
||||
? Recursively scans the "/actions" directory.
|
||||
? Maps all JavaScript files into a nested object structure for dot-notation access.
|
||||
? Ignores "index.js" and files starting with a dot (e.g., ".templateAction.js").
|
||||
? Example mapping: "/actions/chat/message.js" becomes "chat.message(socket, data)".
|
||||
|
||||
^ Usage:
|
||||
?? Import the actions: "import actions from './actions.js';"
|
||||
?? Trigger an action inside a connection: "socket.on('chat.message', (data) => chat.message(socket, data));"
|
||||
? The action template expects the active "socket" and incoming "data" as arguments.
|
||||
*/
|
||||
import actions from './actions.js';
|
||||
|
||||
/*
|
||||
= The Express Router-Autoloader:
|
||||
|
||||
^ How it works:
|
||||
? Recursively scans the "/routers" directory.
|
||||
? Automatically generates a nested Express Router tree based on folder and file names.
|
||||
? Ignores "index.js" and files starting with a dot (e.g., ".templateRouter.js").
|
||||
? Files named "root.js" are mounted directly to their current directory path (/).
|
||||
? Example mapping: "/routers/api/v1/users.js" becomes the endpoint "/api/v1/users".
|
||||
|
||||
^ Usage:
|
||||
?? Import the master router: "import routers from './routers.js';"
|
||||
?? Mount to Express app: "app.use('/', routers);"
|
||||
? Each router template receives an Express sub-router instance to define its methods (GET, POST, etc.).
|
||||
*/
|
||||
import routers, { registeredEndpoints } from './routers.js';
|
||||
|
||||
// $ debug-output
|
||||
console.info('[Autoloader] Loaded Actions:', Object.keys(actions));
|
||||
console.info('[Autoloader] Loaded Routers:', registeredEndpoints);
|
||||
|
||||
// % server initialization
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
// % socket.io initialization
|
||||
const io = new SocketServer(server, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST']
|
||||
}
|
||||
});
|
||||
|
||||
// % view engine & static resources
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, '..', 'frontend', 'views'));
|
||||
app.use(express.static(path.join(__dirname, '..', 'frontend', 'resources')));
|
||||
|
||||
// % middleware
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// $ Inject database worker into every request
|
||||
app.use((req, res, next) => {
|
||||
req.db = dbWorker;
|
||||
next();
|
||||
});
|
||||
|
||||
// % mount routers
|
||||
app.use('/', routers);
|
||||
|
||||
// % socket.io connection handling
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`[Socket.IO] Client connected: ${socket.id}`);
|
||||
|
||||
// $ Automatically map all loaded actions to this socket
|
||||
for (const [eventName, actionHandler] of Object.entries(actions)) {
|
||||
if (typeof actionHandler === 'function') {
|
||||
socket.on(eventName, (data) => actionHandler(socket, dbWorker, data));
|
||||
}
|
||||
}
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`[Socket.IO] Client disconnected: ${socket.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
// % start server
|
||||
const PORT = process.env.PORT;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[Server] Express and Socket.IO are running on port ${PORT}`);
|
||||
});
|
||||
|
||||
// $ export the server instance
|
||||
export default server;
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
! /server/routers.js
|
||||
? Dynamic Express router loader matching the action loader pattern.
|
||||
*/
|
||||
|
||||
// % imports
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
import express from 'express';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// % Target directory where the actual route files reside
|
||||
const routersDir = path.join(__dirname, '..', 'routers');
|
||||
|
||||
// $ debug-output
|
||||
console.info('[Paths] Routers:', routersDir);
|
||||
export const registeredEndpoints = [];
|
||||
|
||||
// $ recursive function to load routers and extract clean endpoints
|
||||
async function loadRouters(dir = routersDir, baseDir = routersDir, currentPrefix = '') {
|
||||
let mainRouter = express.Router();
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// ~ handle subdirectory and extend prefix
|
||||
const subPrefix = `${currentPrefix}/${entry.name}`;
|
||||
const subRouter = await loadRouters(fullPath, baseDir, subPrefix);
|
||||
mainRouter.use(`/${entry.name}`, subRouter);
|
||||
} else if (entry.isFile() && entry.name !== 'index.js' && !entry.name.startsWith('.') && entry.name.endsWith('.js')) {
|
||||
const fileUrl = pathToFileURL(fullPath).href;
|
||||
const module = await import(fileUrl);
|
||||
const routeHandler = module.default || module;
|
||||
|
||||
const routeName = entry.name.replace(/\.js$/, '');
|
||||
const subRouter = express.Router();
|
||||
|
||||
// ~ pass subrouter into the handler function
|
||||
if (typeof routeHandler === 'function') {
|
||||
await routeHandler(subRouter);
|
||||
}
|
||||
|
||||
const finalFolderPrefix = currentPrefix === '' ? '' : currentPrefix;
|
||||
const endpointPath = routeName === 'root' ? finalFolderPrefix || '/' : `${finalFolderPrefix}/${routeName}`;
|
||||
|
||||
// ~ extract methods and paths directly from the sub-router stack
|
||||
subRouter.stack.forEach(layer => {
|
||||
if (layer.route) {
|
||||
const methods = Object.keys(layer.route.methods).join(', ').toUpperCase();
|
||||
const subRoutePath = layer.route.path === '/' ? '' : layer.route.path;
|
||||
registeredEndpoints.push(`${methods} ${endpointPath}${subRoutePath}`.replace(/\/+/g, '/'));
|
||||
}
|
||||
});
|
||||
|
||||
if (routeName === 'root') {
|
||||
mainRouter.use('/', subRouter);
|
||||
} else {
|
||||
mainRouter.use(`/${routeName}`, subRouter);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mainRouter;
|
||||
}
|
||||
|
||||
// $ Initialize asynchronously and export the loaded routers object
|
||||
const routers = await loadRouters();
|
||||
|
||||
// $ export
|
||||
export default routers;
|
||||
Reference in New Issue
Block a user