kist
Version:
Package Pipeline Processor
234 lines (233 loc) • 9.84 kB
JavaScript
"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;