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: [
|
2025-12-11 00:15:13 +01:00
|
|
|
|
'./src/v2/**/*.ts',
|
|
|
|
|
|
'./wwwroot/v2.html'
|
2025-11-03 14:54:57 +01:00
|
|
|
|
],
|
|
|
|
|
|
css: [
|
2025-12-11 00:15:13 +01:00
|
|
|
|
'./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 = [
|
2025-12-11 00:15:13 +01:00
|
|
|
|
'./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);
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|