UNPKG

kist

Version:

Package Pipeline Processor

234 lines (233 loc) 9.84 kB
"use strict"; // ============================================================================ // Import // ============================================================================ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LiveServer = void 0; const express_1 = __importDefault(require("express")); const express_rate_limit_1 = __importDefault(require("express-rate-limit")); const path_1 = __importDefault(require("path")); const ws_1 = require("ws"); const AbstractProcess_1 = require("../core/abstract/AbstractProcess"); const ConfigStore_1 = require("../core/config/ConfigStore"); // ============================================================================ // Class // ============================================================================ /** * LiveServer class provides functionality to serve static files, * inject live reload scripts into HTML responses, and manage WebSocket * connections to enable live reload capabilities. */ class LiveServer extends AbstractProcess_1.AbstractProcess { // Constructor // ======================================================================== /** * Initializes the LiveServer. // * @param port - The port on which the server will listen. */ constructor() { super(); // Parameters // ======================================================================== /** * Express application */ this.app = (0, express_1.default)(); /** * A set of WebSocket clients currently connected to the server. * Each client represents an active WebSocket connection. */ this.clients = new Set(); const configStore = ConfigStore_1.ConfigStore.getInstance(); const liveReloadOptions = configStore.get("options.live") || {}; // Extract and apply live reload options with defaults this.port = liveReloadOptions.port || 3000; this.root = path_1.default.resolve(process.cwd(), liveReloadOptions.root || "public"); this.watchPaths = (liveReloadOptions.watchPaths || [ "src/**/*", "config/**/*", "pack.yaml", ]).map((p) => path_1.default.resolve(process.cwd(), p)); this.ignoredPaths = (liveReloadOptions.ignoredPaths || ["node_modules"]).map((p) => path_1.default.resolve(process.cwd(), p)); // Log initialization details this.logInitializationDetails(); // Initialize server // this.initializeServer(); // Start the HTTP server this.server = this.app.listen(this.port, () => { this.logInfo(`Live Server running at http://localhost:${this.port}`); }); // Initialize WebSocket server this.wss = new ws_1.WebSocketServer({ server: this.server }); // Set up rate limiting this.setupRateLimiter(); // Set up WebSocket handlers this.setupWebSocketHandlers(); // Set up middleware this.setupMiddleware(); } // Methods // ======================================================================== /** * Initializes the HTTP server and WebSocket server. */ initializeServer() { // Start the HTTP server this.server = this.app.listen(this.port, () => { this.logInfo(`Live Server running at http://localhost:${this.port}`); }); // Initialize WebSocket server this.wss = new ws_1.WebSocketServer({ server: this.server }); } /** Logs initialization details for the LiveServer. */ logInitializationDetails() { this.logInfo(`LiveServer initialized with port: ${this.port}`); this.logInfo(`Serving static files from: ${this.root}`); this.logInfo(`Watching paths: ${JSON.stringify(this.watchPaths)}`); this.logInfo(`Ignoring paths: ${JSON.stringify(this.ignoredPaths)}`); } /** * Sets up rate limiting middleware to prevent abuse of HTTP requests. */ setupRateLimiter() { const limiter = (0, express_rate_limit_1.default)({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: "Too many requests from this IP, please try again later.", }); this.app.use(limiter); } /** * Sets up WebSocket handlers to manage client connections. */ setupWebSocketHandlers() { this.wss.on("connection", (ws) => { this.logInfo("New WebSocket connection established."); this.clients.add(ws); ws.on("message", (message) => { this.logInfo(`WebSocket message received: ${message.toString()}`); }); ws.on("close", () => { this.logInfo("WebSocket connection closed."); this.clients.delete(ws); }); ws.on("error", (error) => { console.error("WebSocket encountered an error:", error); this.clients.delete(ws); }); }); } /** * Sets up middleware for serving static files and injecting the live * reload script into HTML files. */ setupMiddleware() { // Securely serve static files from the "public" directory // const publicPath = path.resolve( // __dirname, // "public" // ); this.logInfo(`Resolved public directory: ${this.root}`); this.logInfo(`Serving static files from: ${this.root}`); this.app.use(express_1.default.static(this.root)); // Middleware to inject the live reload script into HTML files this.app.use(this.injectLiveReloadScript.bind(this)); } /** * Middleware function to inject the live reload script into HTML * responses. Prevents directory traversal attacks by sanitizing the * requested file path. * @param req - The HTTP request object. * @param res - The HTTP response object. * @param next - The next middleware function. */ injectLiveReloadScript(req, res, next) { if (req.url.endsWith(".html")) { const sanitizedPath = path_1.default.join(path_1.default.resolve(__dirname, "public"), // Prevent directory traversal path_1.default.normalize(req.url).replace(/^(\.\.(\/|\\|$))+/g, "")); res.sendFile(sanitizedPath, (err) => { if (err) { console.error("Error sending HTML file:", err); next(err); } else { res.write(`<script> const ws = new WebSocket("ws://localhost:${this.port}"); ws.onmessage = (event) => { if (event.data === "reload") { this.logInfo("Reloading page..."); window.location.reload(); } }; </script>`); res.end(); } }); } else { next(); } } /** * Sends a reload signal to all connected WebSocket clients. */ reloadClients() { this.logInfo("Reloading all connected clients..."); this.clients.forEach((client) => { if (client.readyState === ws_1.WebSocket.OPEN) { client.send("reload"); } }); } /** * Gracefully shuts down the server and all WebSocket connections. */ shutdown() { return __awaiter(this, void 0, void 0, function* () { this.logInfo("Shutting down Live Reload Server..."); this.clients.forEach((client) => client.close()); this.wss.close(); yield new Promise((resolve, reject) => { this.server.close((err) => { if (err) { if (this.isErrnoException(err) && err.code === "ERR_SERVER_NOT_RUNNING") { this.logWarn("Server is not running, skipping shutdown."); resolve(); } else { this.logError("Error shutting down server:", err); reject(err); } } else { resolve(); } }); }); this.logInfo("Live Reload Server has been shut down."); }); } /** * Type guard to check if an error is an instance of NodeJS.ErrnoException. * @param error - The error to check. * @returns True if the error has a `code` property. */ isErrnoException(error) { return typeof error === "object" && error !== null && "code" in error; } } exports.LiveServer = LiveServer;