UNPKG

css-unused-cleaner

Version:

Detect and remove unused CSS selectors with intuitive browser UI. Analyze HTML/CSS files and safely clean up unused styles with real-time preview.

324 lines (269 loc) 9.16 kB
const express = require('express'); const cors = require('cors'); const path = require('path'); const fs = require('fs-extra'); const debug = require('debug')('css-cleaner:server'); let currentAnalysis = null; let currentProjectRoot = null; /** * Start the Express server * @param {number} port - Port to listen on * @param {Object} analysisResult - Analysis result from analyzer * @param {string} projectRoot - Project root directory * @returns {Object} Express server instance */ async function startServer(port, analysisResult, projectRoot) { const app = express(); // Store analysis data currentAnalysis = analysisResult; currentProjectRoot = projectRoot; // Middleware app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'dist'))); // Logging middleware app.use((req, res, next) => { debug(`${req.method} ${req.path}`); next(); }); // API Routes /** * GET /api/selectors - Get all selectors with their states */ app.get('/api/selectors', (req, res) => { try { res.json({ selectors: currentAnalysis.selectors, stats: calculateCurrentStats(), files: currentAnalysis.files, projectRoot: currentProjectRoot }); } catch (error) { debug('Error getting selectors:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/toggle - Toggle selector active state */ app.post('/api/toggle', (req, res) => { try { const { selector, active } = req.body; if (!selector || typeof active !== 'boolean') { return res.status(400).json({ error: 'Missing selector or active parameter' }); } if (!currentAnalysis.selectors[selector]) { return res.status(404).json({ error: 'Selector not found' }); } currentAnalysis.selectors[selector].active = active; debug(`Toggled selector ${selector} to ${active}`); const newStats = calculateCurrentStats(); res.json({ success: true, stats: newStats, selector: currentAnalysis.selectors[selector] }); } catch (error) { debug('Error toggling selector:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/toggle-all - Toggle all selectors */ app.post('/api/toggle-all', (req, res) => { try { const { active } = req.body; if (typeof active !== 'boolean') { return res.status(400).json({ error: 'Missing active parameter' }); } Object.keys(currentAnalysis.selectors).forEach(selector => { currentAnalysis.selectors[selector].active = active; }); debug(`Toggled all selectors to ${active}`); const newStats = calculateCurrentStats(); res.json({ success: true, stats: newStats }); } catch (error) { debug('Error toggling all selectors:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/restore - Restore selector states */ app.post('/api/restore', (req, res) => { try { const { type } = req.body; if (type === 'original') { // Restore to original analysis state Object.keys(currentAnalysis.selectors).forEach(selector => { const selectorData = currentAnalysis.selectors[selector]; selectorData.active = !selectorData.unused; }); debug('Restored to original state'); } else if (type === 'all') { // Enable all selectors Object.keys(currentAnalysis.selectors).forEach(selector => { currentAnalysis.selectors[selector].active = true; }); debug('Restored all selectors to active'); } const newStats = calculateCurrentStats(); res.json({ success: true, stats: newStats }); } catch (error) { debug('Error restoring selectors:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/save - Save cleaned CSS */ app.post('/api/save', async (req, res) => { try { const { filename = 'cleaned.css', overwrite = false } = req.body; // Generate cleaned CSS content const cleanedCSS = generateCleanedCSS(); let outputPath; let savedFiles = []; if (overwrite) { // Overwrite original CSS files for (const cssFile of currentAnalysis.files.css) { const originalPath = path.join(currentProjectRoot, cssFile); // Create backup first const backupDir = path.join(currentProjectRoot, 'backup'); await fs.ensureDir(backupDir); const backupPath = path.join(backupDir, `${cssFile}.backup.${Date.now()}`); await fs.copy(originalPath, backupPath); // Overwrite original await fs.writeFile(originalPath, cleanedCSS, 'utf8'); savedFiles.push(cssFile); debug(`Overwritten ${originalPath}, backup saved to ${backupPath}`); } outputPath = 'Original CSS files'; } else { // Save as new file outputPath = path.join(currentProjectRoot, filename); await fs.writeFile(outputPath, cleanedCSS, 'utf8'); savedFiles.push(filename); debug(`Saved cleaned CSS to ${outputPath}`); } // Save session const sessionPath = path.join(currentProjectRoot, 'session.json'); await fs.writeJson(sessionPath, currentAnalysis, { spaces: 2 }); const stats = calculateCurrentStats(); res.json({ success: true, outputPath: overwrite ? outputPath : path.relative(currentProjectRoot, outputPath), sessionPath: path.relative(currentProjectRoot, sessionPath), overwritten: overwrite, savedFiles: savedFiles, stats, size: cleanedCSS.length }); } catch (error) { debug('Error saving CSS:', error); res.status(500).json({ error: error.message }); } }); /** * GET /api/css - Get current CSS based on active selectors */ app.get('/api/css', (req, res) => { try { const cleanedCSS = generateCleanedCSS(); res.setHeader('Content-Type', 'text/css'); res.send(cleanedCSS); } catch (error) { debug('Error generating CSS:', error); res.status(500).json({ error: error.message }); } }); /** * GET /api/preview/:file - Serve HTML files with modified CSS */ app.get('/api/preview/:file(*)', async (req, res) => { try { const requestedFile = req.params.file; const filePath = path.join(currentProjectRoot, requestedFile); // Security check - ensure file is within project root const resolvedPath = path.resolve(filePath); const resolvedRoot = path.resolve(currentProjectRoot); if (!resolvedPath.startsWith(resolvedRoot)) { return res.status(403).json({ error: 'Access denied' }); } if (!await fs.pathExists(filePath)) { return res.status(404).json({ error: 'File not found' }); } const htmlContent = await fs.readFile(filePath, 'utf8'); // Replace CSS links with our dynamic CSS endpoint const modifiedHtml = htmlContent.replace( /<link\s+[^>]*rel=["']stylesheet["'][^>]*>/gi, '<link rel="stylesheet" href="/api/css">' ); res.setHeader('Content-Type', 'text/html'); res.send(modifiedHtml); } catch (error) { debug('Error serving preview:', error); res.status(500).json({ error: error.message }); } }); // Serve the main UI app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); }); // Error handling middleware app.use((error, req, res, next) => { debug('Server error:', error); res.status(500).json({ error: 'Internal server error' }); }); // Start server const server = app.listen(port, () => { debug(`Server listening on port ${port}`); }); return server; } /** * Generate cleaned CSS from active selectors * @returns {string} Cleaned CSS content */ function generateCleanedCSS() { let cleanedCSS = ''; Object.keys(currentAnalysis.selectors).forEach(selector => { const selectorData = currentAnalysis.selectors[selector]; if (selectorData.active) { cleanedCSS += selectorData.css; } }); return cleanedCSS; } /** * Calculate current statistics based on selector states * @returns {Object} Current statistics */ function calculateCurrentStats() { const total = Object.keys(currentAnalysis.selectors).length; let unused = 0; let active = 0; let disabled = 0; Object.values(currentAnalysis.selectors).forEach(selectorData => { if (selectorData.unused) unused++; if (selectorData.active) active++; if (!selectorData.active) disabled++; }); return { total, unused, used: total - unused, active, disabled }; } module.exports = { startServer };