image-asset-manager
Version:
A comprehensive image asset management tool for frontend projects
916 lines (907 loc) β’ 40.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebServer = void 0;
const express_1 = __importDefault(require("express"));
const http_1 = require("http");
const ws_1 = require("ws");
const path_1 = __importDefault(require("path"));
const types_1 = require("../types");
// Import embedded web assets (will be generated during build)
let webAssets = null;
let getAsset = null;
let getAssetMimeType = null;
try {
const embeddedAssets = require("../web-assets");
webAssets = embeddedAssets.webAssets;
getAsset = embeddedAssets.getAsset;
getAssetMimeType = embeddedAssets.getAssetMimeType;
}
catch (error) {
console.warn("Embedded web assets not found, falling back to development mode");
}
class WebServer {
constructor() {
this.server = null;
this.wss = null;
this.port = 3000;
this.data = null;
this.clients = new Set();
this.app = (0, express_1.default)();
this.setupMiddleware();
this.setupAPIRoutes();
// Note: Static file routes and catch-all routes will be set up in start() method
}
setupMiddleware() {
// Enable CORS for development
this.app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
// Parse JSON bodies
this.app.use(express_1.default.json());
// Serve static files (images) - will be configured when server starts
// This will be set up in the start method with the actual project path
}
setupAPIRoutes() {
// Health check endpoint
this.app.get("/api/health", (req, res) => {
// Add diagnostic info to health check
let diagnosticInfo = {};
// TODO: Re-implement consistency validation when needed
// if (this.data) {
// try {
// const validation =
// this.consistencyValidator.validateCategoryStatistics(this.data);
// diagnosticInfo = {
// categoryConsistency: {
// isConsistent: validation.isConsistent,
// totalIssues: validation.totalIssues,
// discrepancies: validation.discrepancies.length,
// },
// };
// } catch (error) {
// diagnosticInfo = {
// diagnosticError:
// error instanceof Error ? error.message : "Unknown error",
// };
// }
// }
res.json({
status: "ok",
timestamp: new Date().toISOString(),
diagnostic: diagnosticInfo,
});
});
// Get all images with pagination and filtering
this.app.get("/api/images", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
const { page = "1", limit = "20", category, type, unused } = req.query;
const pageNum = parseInt(page, 10);
const limitNum = parseInt(limit, 10);
let filteredImages = [...this.data.images];
// Apply filters
if (category && category !== "all") {
filteredImages = filteredImages.filter((img) => img.category === category);
}
if (type && type !== "all") {
filteredImages = filteredImages.filter((img) => img.extension === type);
}
if (unused === "true") {
const unusedIds = new Set(this.data.usage.unusedFiles.map((f) => f.id));
filteredImages = filteredImages.filter((img) => unusedIds.has(img.id));
}
// Pagination
const startIndex = (pageNum - 1) * limitNum;
const endIndex = startIndex + limitNum;
const paginatedImages = filteredImages.slice(startIndex, endIndex);
res.json({
images: paginatedImages,
pagination: {
page: pageNum,
limit: limitNum,
total: filteredImages.length,
totalPages: Math.ceil(filteredImages.length / limitNum),
},
});
});
// Get single image details
this.app.get("/api/images/:id", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
const { id } = req.params;
const image = this.data.images.find((img) => img.id === id);
if (!image) {
return res.status(404).json({ error: "Image not found" });
}
// Get usage information
const usageInfo = this.data.usage.usedFiles.get(id);
const isUnused = this.data.usage.unusedFiles.some((f) => f.id === id);
res.json({
image,
usage: usageInfo || null,
isUnused,
duplicates: this.data.duplicates.find((group) => group.files.some((f) => f.id === id)) || null,
});
});
// Get project statistics
this.app.get("/api/stats", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
res.json(this.data.stats);
});
// Get duplicate groups
this.app.get("/api/duplicates", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
res.json(this.data.duplicates);
});
// Get project configuration
this.app.get("/api/config", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
res.json(this.data.config);
});
// Generate code for specific image and framework
this.app.post("/api/generate-code", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
const { imageId, framework, codeType = "usage" } = req.body;
if (!imageId || !framework) {
return res.status(400).json({
error: "Missing required parameters: imageId and framework",
});
}
const image = this.data.images.find((img) => img.id === imageId);
if (!image) {
return res.status(404).json({ error: "Image not found" });
}
try {
const generatedCode = this.generateCodeForImage(image, framework, codeType);
res.json({
image: {
id: image.id,
name: image.name,
path: image.relativePath,
},
framework,
codeType,
code: generatedCode,
});
}
catch (error) {
res.status(500).json({
error: "Failed to generate code",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
// Get usage analysis for specific image
this.app.get("/api/usage/:id", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
const { id } = req.params;
// Check if the image exists first
const image = this.data.images.find((img) => img.id === id);
if (!image) {
return res.status(404).json({ error: "Image not found" });
}
const usageInfo = this.data.usage.usedFiles.get(id);
const isUnused = this.data.usage.unusedFiles.some((f) => f.id === id);
res.json({
imageId: id,
isUnused,
usageInfo: usageInfo || null,
references: usageInfo?.references || [],
});
});
// Search images with advanced filtering
this.app.get("/api/search", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
const { q: query, category, type, unused, minSize, maxSize, sortBy = "name", sortOrder = "asc", page = "1", limit = "20", } = req.query;
let filteredImages = [...this.data.images];
// Text search
if (query) {
const searchTerm = query.toLowerCase();
filteredImages = filteredImages.filter((img) => img.name.toLowerCase().includes(searchTerm) ||
img.relativePath.toLowerCase().includes(searchTerm) ||
img.category.toLowerCase().includes(searchTerm));
}
// Category filter
if (category && category !== "all") {
filteredImages = filteredImages.filter((img) => img.category === category);
}
// Type filter
if (type && type !== "all") {
filteredImages = filteredImages.filter((img) => img.extension === type);
}
// Unused filter
if (unused === "true") {
const unusedIds = new Set(this.data.usage.unusedFiles.map((f) => f.id));
filteredImages = filteredImages.filter((img) => unusedIds.has(img.id));
}
// Size filters
if (minSize) {
const minSizeNum = parseInt(minSize, 10);
filteredImages = filteredImages.filter((img) => img.size >= minSizeNum);
}
if (maxSize) {
const maxSizeNum = parseInt(maxSize, 10);
filteredImages = filteredImages.filter((img) => img.size <= maxSizeNum);
}
// Sorting
filteredImages.sort((a, b) => {
let aValue, bValue;
switch (sortBy) {
case "name":
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
break;
case "size":
aValue = a.size;
bValue = b.size;
break;
case "modified":
aValue = a.modifiedAt.getTime();
bValue = b.modifiedAt.getTime();
break;
case "category":
aValue = a.category.toLowerCase();
bValue = b.category.toLowerCase();
break;
default:
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
}
if (aValue < bValue)
return sortOrder === "asc" ? -1 : 1;
if (aValue > bValue)
return sortOrder === "asc" ? 1 : -1;
return 0;
});
// Pagination
const pageNum = parseInt(page, 10);
const limitNum = parseInt(limit, 10);
const startIndex = (pageNum - 1) * limitNum;
const endIndex = startIndex + limitNum;
const paginatedImages = filteredImages.slice(startIndex, endIndex);
res.json({
query: {
text: query || "",
category,
type,
unused,
minSize,
maxSize,
sortBy,
sortOrder,
},
results: paginatedImages,
pagination: {
page: pageNum,
limit: limitNum,
total: filteredImages.length,
totalPages: Math.ceil(filteredImages.length / limitNum),
},
});
});
// Diagnostic endpoints for category consistency
this.app.get("/api/diagnostic/categories", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
try {
// TODO: Re-implement consistency validation when needed
// const validation =
// this.consistencyValidator.validateCategoryStatistics(this.data);
// const audit = this.consistencyValidator.auditCategoryAssignments(
// this.data.images
// );
res.json({
// validation,
// audit,
timestamp: new Date().toISOString(),
summary: {
totalImages: this.data.images.length,
categoriesInStats: Object.keys(this.data.stats.categoryBreakdown || {}).length,
// actualCategories: audit.categoriesAudited.length,
// isConsistent: validation.isConsistent,
// issuesFound: validation.totalIssues + audit.issues.length,
},
});
}
catch (error) {
res.status(500).json({
error: "Failed to run diagnostic",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
// Diagnostic report endpoint
this.app.get("/api/diagnostic/report", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
try {
// TODO: Re-implement diagnostic report when needed
// const report = this.consistencyValidator.createDiagnosticReport(
// this.data
// );
res.setHeader("Content-Type", "text/plain");
res.send("Diagnostic report temporarily unavailable");
}
catch (error) {
res.status(500).json({
error: "Failed to generate diagnostic report",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
// Manual reconciliation endpoint
this.app.post("/api/diagnostic/reconcile", (req, res) => {
if (!this.data) {
return res.status(503).json({ error: "Server data not available" });
}
try {
// TODO: Re-implement reconciliation when needed
// const reconciledData = this.consistencyValidator.reconcileStatistics(
// this.data
// );
// this.data = reconciledData;
// Broadcast updated data to WebSocket clients
this.broadcast("data-updated", {
stats: this.data.stats,
imageCount: this.data.images.length,
});
res.json({
success: true,
message: "Statistics reconciliation temporarily disabled",
// updatedStats: reconciledData.stats.categoryBreakdown,
});
}
catch (error) {
res.status(500).json({
error: "Failed to reconcile statistics",
details: error instanceof Error ? error.message : "Unknown error",
});
}
});
}
setupFrontendRoutes() {
// Serve embedded web assets
this.app.get("*", (req, res) => {
// Skip API routes and static assets
if (req.path.startsWith("/api/") ||
req.path.startsWith("/project-assets/")) {
return res.status(404).json({ error: "Endpoint not found" });
}
if (webAssets && getAsset && getAssetMimeType) {
// Production mode: serve embedded assets
let assetPath = req.path === "/" ? "index.html" : req.path.slice(1);
// Try to get the asset
let asset = getAsset(assetPath);
// If not found and it's not a file extension, try index.html (SPA routing)
if (!asset && !path_1.default.extname(assetPath)) {
assetPath = "index.html";
asset = getAsset(assetPath);
}
if (asset) {
const mimeType = getAssetMimeType(assetPath);
res.setHeader("Content-Type", mimeType);
if (mimeType.startsWith("text/") ||
mimeType.includes("javascript") ||
mimeType.includes("json")) {
res.setHeader("Cache-Control", "no-cache");
}
else {
res.setHeader("Cache-Control", "public, max-age=31536000");
}
return res.send(asset);
}
}
// Development mode or asset not found: try to serve from web/dist
const webDistPath = path_1.default.join(__dirname, "../../web/dist");
if (req.path === "/") {
// Try to serve index.html from web/dist
const indexPath = path_1.default.join(webDistPath, "index.html");
if (require("fs").existsSync(indexPath)) {
return res.sendFile(indexPath);
}
}
else {
// Try to serve static assets from web/dist
const assetPath = path_1.default.join(webDistPath, req.path);
if (require("fs").existsSync(assetPath)) {
return res.sendFile(assetPath);
}
}
// If web/dist doesn't exist, serve a simple placeholder that loads the React app
if (req.path === "/") {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Image Asset Manager</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function ImageGrid({ images }) {
const [selectedImage, setSelectedImage] = useState(null);
if (!images || images.length === 0) {
return (
<div className="text-center py-12">
<div className="text-gray-400 mb-4">
<svg className="w-24 h-24 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No images found</h3>
<p className="text-gray-500">Loading images...</p>
</div>
);
}
return (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 p-6">
{images.map((image) => (
<div key={image.id} className="group relative bg-white rounded-lg border border-gray-200 hover:border-gray-300 hover:shadow-md transition-all duration-200 cursor-pointer"
onClick={() => setSelectedImage(image)}>
<div className="aspect-square bg-gray-50 rounded-t-lg overflow-hidden">
<img
src={image.url || image.path}
alt={image.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
onError={(e) => {
e.target.style.display = 'none';
e.target.parentElement.innerHTML = '<div class="w-full h-full flex items-center justify-center bg-gray-100"><span class="text-gray-400 text-xs">' + image.extension.toUpperCase() + '</span></div>';
}}
/>
</div>
<div className="p-3">
<div className="flex items-center justify-between mb-2">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{image.extension.toUpperCase()}
</span>
</div>
<h3 className="text-sm font-medium text-gray-900 truncate mb-1" title={image.name}>
{image.name}
</h3>
<div className="text-xs text-gray-500">
{Math.round(image.size / 1024)} KB
</div>
</div>
</div>
))}
</div>
);
}
function App() {
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState(null);
useEffect(() => {
Promise.all([
fetch('/api/images?limit=100').then(r => r.json()),
fetch('/api/stats').then(r => r.json())
]).then(([imagesData, statsData]) => {
setImages(imagesData.images || []);
setStats(statsData);
setLoading(false);
}).catch(error => {
console.error('Error loading data:', error);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Loading images...</span>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4">
<div className="flex items-center">
<h1 className="text-2xl font-bold text-gray-900">πΌοΈ Image Asset Manager</h1>
</div>
{stats && (
<div className="flex items-center space-x-6 text-sm text-gray-600">
<span>{stats.totalImages} images</span>
<span>{Math.round(stats.totalSize / 1024 / 1024)} MB</span>
</div>
)}
</div>
</div>
</header>
<main>
<ImageGrid images={images} />
</main>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>
`);
}
else {
res.status(404).json({ error: "Asset not found" });
}
});
}
generateCodeForImage(image, framework, codeType) {
const relativePath = image.relativePath;
const name = image.name.replace(/\.[^/.]+$/, ""); // Remove extension
const extension = image.extension.toLowerCase();
// Generate different types of code based on framework and codeType
switch (framework.toLowerCase()) {
case "react":
return this.generateReactCode(image, codeType);
case "vue":
return this.generateVueCode(image, codeType);
case "html":
return this.generateHtmlCode(image, codeType);
case "angular":
return this.generateAngularCode(image, codeType);
case "svelte":
return this.generateSvelteCode(image, codeType);
default:
throw new Error(`Unsupported framework: ${framework}`);
}
}
generateReactCode(image, codeType) {
const importName = this.toCamelCase(image.name.replace(/\.[^/.]+$/, ""));
const relativePath = image.relativePath;
switch (codeType) {
case "import":
return {
import: `import ${importName} from './${relativePath}';`,
usage: `<img src={${importName}} alt="${image.name}" />`,
};
case "component":
if (image.extension.toLowerCase() === "svg") {
return {
import: `import { ReactComponent as ${importName}Icon } from './${relativePath}';`,
usage: `<${importName}Icon />`,
component: `const ${importName}Icon = () => <${importName}Icon />;`,
};
}
return this.generateReactCode(image, "import");
case "inline":
if (image.extension.toLowerCase() === "svg") {
return {
usage: `// SVG content would be inlined here`,
note: "SVG content should be copied directly into JSX",
};
}
return this.generateReactCode(image, "import");
default:
return this.generateReactCode(image, "import");
}
}
generateVueCode(image, codeType) {
const relativePath = image.relativePath;
switch (codeType) {
case "import":
return {
import: `import imageUrl from './${relativePath}';`,
usage: `<img :src="imageUrl" alt="${image.name}" />`,
};
case "component":
if (image.extension.toLowerCase() === "svg") {
const componentName = this.toPascalCase(image.name.replace(/\.[^/.]+$/, ""));
return {
import: `import ${componentName} from './${relativePath}';`,
usage: `<${componentName} />`,
component: `<template>\n <${componentName} />\n</template>`,
};
}
return this.generateVueCode(image, "import");
case "inline":
return {
usage: `<img src="./${relativePath}" alt="${image.name}" />`,
};
default:
return this.generateVueCode(image, "import");
}
}
generateHtmlCode(image, codeType) {
const relativePath = image.relativePath;
return {
usage: `<img src="./${relativePath}" alt="${image.name}" />`,
inline: image.extension.toLowerCase() === "svg"
? `<!-- SVG content would be inlined here -->`
: `<img src="./${relativePath}" alt="${image.name}" />`,
};
}
generateAngularCode(image, codeType) {
const relativePath = image.relativePath;
switch (codeType) {
case "import":
return {
usage: `<img src="assets/${relativePath}" alt="${image.name}" />`,
note: "Place image in src/assets/ directory",
};
case "component":
if (image.extension.toLowerCase() === "svg") {
return {
usage: `<img src="assets/${relativePath}" alt="${image.name}" />`,
note: "For SVG icons, consider using Angular Material Icons or custom SVG components",
};
}
return this.generateAngularCode(image, "import");
default:
return this.generateAngularCode(image, "import");
}
}
generateSvelteCode(image, codeType) {
const relativePath = image.relativePath;
switch (codeType) {
case "import":
return {
import: `import imageUrl from './${relativePath}';`,
usage: `<img src={imageUrl} alt="${image.name}" />`,
};
case "inline":
return {
usage: `<img src="./${relativePath}" alt="${image.name}" />`,
};
default:
return this.generateSvelteCode(image, "import");
}
}
toCamelCase(str) {
return str.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""));
}
toPascalCase(str) {
const camelCase = this.toCamelCase(str);
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
}
async start(port, data) {
//ε¦ζη«―ε£θ’«ε η¨, εζζεθΏη¨
this.port = await this.findAvailablePort(port);
this.data = data;
// Set up static file serving for the project images
if (data.config && data.config.projectPath) {
// Add static file serving for project assets
this.app.use("/project-assets", express_1.default.static(data.config.projectPath, {
setHeaders: (res, filePath) => {
// Set appropriate headers for image files
if (filePath.match(/\.(jpg|jpeg|png|gif|svg|webp|ico)$/i)) {
res.setHeader("Cache-Control", "public, max-age=3600");
res.setHeader("Access-Control-Allow-Origin", "*");
// Set correct MIME type for SVG files
if (filePath.match(/\.svg$/i)) {
res.setHeader("Content-Type", "image/svg+xml");
}
}
},
}));
}
// Set up frontend routes (must be last to avoid conflicts)
this.setupFrontendRoutes();
return new Promise((resolve, reject) => {
try {
this.server = (0, http_1.createServer)(this.app);
this.server.listen(this.port, () => {
this.setupWebSocket();
resolve();
});
this.server.on("error", (error) => {
if (error.code === "EADDRINUSE") {
reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Port ${this.port} is already in use`, { port: this.port }, true));
}
else {
reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Failed to start server: ${error.message}`, error, false));
}
});
}
catch (error) {
reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Failed to create server: ${error instanceof Error ? error.message : "Unknown error"}`, error, false));
}
});
}
async stop() {
return new Promise((resolve) => {
// Close WebSocket connections
if (this.wss) {
this.clients.forEach((client) => {
if (client.readyState === ws_1.WebSocket.OPEN) {
client.close();
}
});
this.wss.close();
this.wss = null;
}
// Close HTTP server
if (this.server) {
this.server.close(() => {
this.server = null;
resolve();
});
}
else {
resolve();
}
});
}
setupWebSocket() {
if (!this.server)
return;
this.wss = new ws_1.WebSocketServer({ server: this.server });
this.wss.on("connection", (ws) => {
this.clients.add(ws);
// Send initial data
if (this.data) {
ws.send(JSON.stringify({
type: "initial-data",
data: {
stats: this.data.stats,
imageCount: this.data.images.length,
},
}));
}
ws.on("close", () => {
this.clients.delete(ws);
});
ws.on("error", (error) => {
console.error("π‘ WebSocket error:", error);
this.clients.delete(ws);
});
});
}
broadcast(event, data) {
if (!this.wss)
return;
const message = JSON.stringify({ type: event, data });
this.clients.forEach((client) => {
if (client.readyState === ws_1.WebSocket.OPEN) {
try {
client.send(message);
}
catch (error) {
console.error("π‘ Failed to send WebSocket message:", error);
this.clients.delete(client);
}
}
});
}
async findAvailablePort(startPort) {
const net = await Promise.resolve().then(() => __importStar(require("net")));
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(startPort, () => {
const port = server.address()?.port || startPort;
server.close(() => resolve(port));
});
server.on("error", async (error) => {
if (error.code === "EADDRINUSE") {
console.log(`π Port ${startPort} is in use, attempting to kill existing process...`);
try {
// Find process using the port
const lsofOutput = execSync(`lsof -ti :${startPort}`, {
encoding: "utf8",
}).trim();
if (lsofOutput) {
const pids = lsofOutput.split("\n").filter((pid) => pid.trim());
for (const pid of pids) {
console.log(`π Killing process ${pid} using port ${startPort}`);
execSync(`kill -9 ${pid}`);
}
// Wait a moment for the process to be killed
await new Promise((resolve) => setTimeout(resolve, 1000));
// Try to use the port again
const retryServer = net.createServer();
retryServer.listen(startPort, () => {
const port = retryServer.address()?.port || startPort;
retryServer.close(() => {
console.log(`β
Successfully freed and using port ${startPort}`);
resolve(port);
});
});
retryServer.on("error", (retryError) => {
// If still can't use the port after killing, reject with error
console.log(`β Port ${startPort} still in use after killing process`);
reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Port ${startPort} is still in use after attempting to kill existing process`, { port: startPort }, true));
});
}
else {
// No process found but port still in use
reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Port ${startPort} is in use but no process found`, { port: startPort }, true));
}
}
catch (killError) {
console.log(`β οΈ Could not kill process on port ${startPort}:`, killError);
reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Failed to kill process using port ${startPort}: ${killError instanceof Error
? killError.message
: "Unknown error"}`, { port: startPort, killError }, true));
}
}
else {
reject(error);
}
});
});
}
// Getter for current port (useful for testing)
getPort() {
return this.port;
}
// Getter for server status
isRunning() {
return this.server !== null && this.server.listening;
}
// Update server data (for real-time updates)
updateData(data) {
this.data = data;
this.broadcast("data-updated", {
stats: data.stats,
imageCount: data.images.length,
});
}
}
exports.WebServer = WebServer;
//# sourceMappingURL=WebServer.js.map