Calendar/analyze-css.js

421 lines
14 KiB
JavaScript
Raw Permalink Normal View History

2025-11-03 14:54:57 +01:00
import { PurgeCSS } from 'purgecss';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Create reports directory if it doesn't exist
const reportsDir = './reports';
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir);
}
console.log('🔍 Starting CSS Analysis...\n');
// 1. Run PurgeCSS to find unused CSS
console.log('📊 Running PurgeCSS analysis...');
async function runPurgeCSS() {
const purgeCSSResults = await new PurgeCSS().purge({
content: [
'./src/v2/**/*.ts',
'./wwwroot/v2.html'
2025-11-03 14:54:57 +01:00
],
css: [
'./wwwroot/css/v2/*.css'
2025-11-03 14:54:57 +01:00
],
rejected: true,
rejectedCss: true,
safelist: {
standard: [
/^swp-/,
/^cols-[1-4]$/,
/^stack-level-[0-4]$/,
'dragging',
'hover',
'highlight',
'transitioning',
'filter-active',
'swp--resizing',
'max-event-indicator',
'max-event-overflow-hide',
'max-event-overflow-show',
'allday-chevron',
'collapsed',
'expanded',
/^month-/,
/^week-/,
'today',
'weekend',
'other-month',
'hidden',
'invisible',
'transparent',
'calendar-wrapper'
]
}
});
// Calculate statistics
let totalOriginalSize = 0;
let totalPurgedSize = 0;
let totalRejected = 0;
const rejectedByFile = {};
purgeCSSResults.forEach(result => {
const fileName = path.basename(result.file);
const originalSize = result.css.length + (result.rejected ? result.rejected.join('').length : 0);
const purgedSize = result.css.length;
const rejectedSize = result.rejected ? result.rejected.length : 0;
totalOriginalSize += originalSize;
totalPurgedSize += purgedSize;
totalRejected += rejectedSize;
rejectedByFile[fileName] = {
originalSize,
purgedSize,
rejectedCount: rejectedSize,
rejected: result.rejected || []
};
});
const report = {
summary: {
totalFiles: purgeCSSResults.length,
totalOriginalSize,
totalPurgedSize,
totalRejected,
percentageRemoved: ((totalRejected / (totalOriginalSize || 1)) * 100).toFixed(2) + '%',
potentialSavings: totalOriginalSize - totalPurgedSize
},
fileDetails: rejectedByFile
};
fs.writeFileSync(
path.join(reportsDir, 'purgecss-report.json'),
JSON.stringify(report, null, 2)
);
console.log('✅ PurgeCSS analysis complete');
console.log(` - Total CSS rules analyzed: ${totalOriginalSize}`);
console.log(` - Unused CSS rules found: ${totalRejected}`);
console.log(` - Potential removal: ${report.summary.percentageRemoved}`);
return report;
}
// 2. Analyze CSS with basic stats
console.log('\n📊 Running CSS Stats analysis...');
function runCSSStats() {
const cssFiles = [
'./wwwroot/css/v2/calendar-v2.css',
'./wwwroot/css/v2/calendar-v2-base.css',
'./wwwroot/css/v2/calendar-v2-layout.css',
'./wwwroot/css/v2/calendar-v2-events.css'
2025-11-03 14:54:57 +01:00
];
const stats = {};
cssFiles.forEach(file => {
if (fs.existsSync(file)) {
const fileName = path.basename(file);
const content = fs.readFileSync(file, 'utf8');
// Basic statistics
const lines = content.split('\n').length;
const size = Buffer.byteLength(content, 'utf8');
const rules = (content.match(/\{[^}]*\}/g) || []).length;
const selectors = (content.match(/[^{]+(?=\{)/g) || []).length;
const properties = (content.match(/[^:]+:[^;]+;/g) || []).length;
const colors = [...new Set(content.match(/#[0-9a-fA-F]{3,6}|rgba?\([^)]+\)|hsla?\([^)]+\)/g) || [])];
const mediaQueries = (content.match(/@media[^{]+/g) || []).length;
stats[fileName] = {
lines,
size: `${(size / 1024).toFixed(2)} KB`,
sizeBytes: size,
rules,
selectors,
properties,
uniqueColors: colors.length,
colors: colors.slice(0, 10), // First 10 colors
mediaQueries
};
}
});
fs.writeFileSync(
path.join(reportsDir, 'css-stats.json'),
JSON.stringify(stats, null, 2)
);
console.log('✅ CSS Stats analysis complete');
console.log(` - Files analyzed: ${Object.keys(stats).length}`);
return stats;
}
// 3. Generate HTML report
function generateHTMLReport(purgeReport, statsReport) {
const totalSize = Object.values(statsReport).reduce((sum, stat) => sum + stat.sizeBytes, 0);
const totalSizeKB = (totalSize / 1024).toFixed(2);
const html = `
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS Analysis Report - Calendar Plantempus</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #2196f3;
margin-bottom: 10px;
font-size: 2.5em;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 1.1em;
}
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.stat-card.warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card.success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-value {
font-size: 2.5em;
font-weight: bold;
margin: 10px 0;
}
.stat-label {
font-size: 0.9em;
opacity: 0.9;
}
section {
margin-bottom: 40px;
}
h2 {
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #2196f3;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #555;
}
tr:hover {
background: #f8f9fa;
}
.file-detail {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
.rejected-list {
max-height: 200px;
overflow-y: auto;
background: white;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
}
.badge-danger { background: #ffebee; color: #c62828; }
.badge-warning { background: #fff3e0; color: #ef6c00; }
.badge-success { background: #e8f5e9; color: #2e7d32; }
.timestamp {
color: #999;
font-size: 0.9em;
margin-top: 30px;
text-align: center;
}
.color-palette {
display: flex;
gap: 5px;
flex-wrap: wrap;
margin-top: 10px;
}
.color-swatch {
width: 30px;
height: 30px;
border-radius: 4px;
border: 1px solid #ddd;
}
</style>
</head>
<body>
<div class="container">
<h1>📊 CSS Analysis Report</h1>
<p class="subtitle">Calendar Plantempus - Production CSS Analysis</p>
<div class="summary">
<div class="stat-card">
<div class="stat-label">Total CSS Size</div>
<div class="stat-value">${totalSizeKB} KB</div>
</div>
<div class="stat-card">
<div class="stat-label">CSS Files</div>
<div class="stat-value">${purgeReport.summary.totalFiles}</div>
</div>
<div class="stat-card warning">
<div class="stat-label">Unused CSS Rules</div>
<div class="stat-value">${purgeReport.summary.totalRejected}</div>
</div>
<div class="stat-card success">
<div class="stat-label">Potential Removal</div>
<div class="stat-value">${purgeReport.summary.percentageRemoved}</div>
</div>
</div>
<section>
<h2>📈 CSS Statistics by File</h2>
<table>
<thead>
<tr>
<th>File</th>
<th>Size</th>
<th>Lines</th>
<th>Rules</th>
<th>Selectors</th>
<th>Properties</th>
<th>Colors</th>
</tr>
</thead>
<tbody>
${Object.entries(statsReport).map(([file, stats]) => `
<tr>
<td><strong>${file}</strong></td>
<td>${stats.size}</td>
<td>${stats.lines}</td>
<td>${stats.rules}</td>
<td>${stats.selectors}</td>
<td>${stats.properties}</td>
<td>${stats.uniqueColors}</td>
</tr>
`).join('')}
</tbody>
</table>
</section>
<section>
<h2>🗑 Unused CSS by File</h2>
${Object.entries(purgeReport.fileDetails).map(([file, details]) => `
<div class="file-detail">
<h3>${file}</h3>
<p>
<span class="badge ${details.rejectedCount > 50 ? 'badge-danger' : details.rejectedCount > 20 ? 'badge-warning' : 'badge-success'}">
${details.rejectedCount} unused rules
</span>
<span style="margin-left: 10px; color: #666;">
Original: ${details.originalSize} | After purge: ${details.purgedSize}
</span>
</p>
${details.rejectedCount > 0 ? `
<details>
<summary style="cursor: pointer; margin-top: 10px;">Show unused selectors</summary>
<div class="rejected-list">
${details.rejected.slice(0, 50).join('<br>')}
${details.rejected.length > 50 ? `<br><em>... and ${details.rejected.length - 50} more</em>` : ''}
</div>
</details>
` : '<p style="color: #2e7d32; margin-top: 10px;">✅ No unused CSS found!</p>'}
</div>
`).join('')}
</section>
<section>
<h2>💡 Recommendations</h2>
<ul style="line-height: 2;">
${purgeReport.summary.totalRejected > 100 ?
'<li>⚠️ <strong>High number of unused CSS rules detected.</strong> Consider removing unused styles to improve performance.</li>' :
'<li>✅ CSS usage is relatively clean.</li>'}
${Object.values(purgeReport.fileDetails).some(d => d.rejectedCount > 50) ?
'<li>⚠️ Some files have significant unused CSS. Review these files for optimization opportunities.</li>' : ''}
<li>📦 Consider consolidating similar styles to reduce duplication.</li>
<li>🎨 Review color palette - found ${Object.values(statsReport).reduce((sum, s) => sum + s.uniqueColors, 0)} unique colors across all files.</li>
<li>🔄 Implement a build process to automatically remove unused CSS in production.</li>
</ul>
</section>
<p class="timestamp">Report generated: ${new Date().toLocaleString('da-DK')}</p>
</div>
</body>
</html>
`;
fs.writeFileSync(path.join(reportsDir, 'css-analysis-report.html'), html);
console.log('\n✅ HTML report generated: reports/css-analysis-report.html');
}
// Run all analyses
(async () => {
try {
const purgeReport = await runPurgeCSS();
const statsReport = runCSSStats();
generateHTMLReport(purgeReport, statsReport);
console.log('\n🎉 CSS Analysis Complete!');
console.log('📄 Reports generated in ./reports/ directory');
console.log(' - purgecss-report.json (detailed unused CSS data)');
console.log(' - css-stats.json (CSS statistics)');
console.log(' - css-analysis-report.html (visual report)');
console.log('\n💡 Open reports/css-analysis-report.html in your browser to view the full report');
} catch (error) {
console.error('❌ Error during analysis:', error);
process.exit(1);
}
})();