Consolidates and optimizes CSS files for calendar v2 Reduces CSS files from 8 to 4, improving project structure Minimizes unused CSS rules and improves overall CSS efficiency Simplifies PurgeCSS configuration and content targeting
421 lines
No EOL
14 KiB
JavaScript
421 lines
No EOL
14 KiB
JavaScript
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'
|
|
],
|
|
css: [
|
|
'./wwwroot/css/v2/*.css'
|
|
],
|
|
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'
|
|
];
|
|
|
|
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);
|
|
}
|
|
})(); |