WIP on master
This commit is contained in:
parent
b6ab1ff50e
commit
80aaab46f2
25 changed files with 6291 additions and 927 deletions
424
analyze-css.js
Normal file
424
analyze-css.js
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
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/**/*.ts',
|
||||
'./wwwroot/**/*.html'
|
||||
],
|
||||
css: [
|
||||
'./wwwroot/css/*.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/calendar-base-css.css',
|
||||
'./wwwroot/css/calendar-components-css.css',
|
||||
'./wwwroot/css/calendar-events-css.css',
|
||||
'./wwwroot/css/calendar-layout-css.css',
|
||||
'./wwwroot/css/calendar-month-css.css',
|
||||
'./wwwroot/css/calendar-popup-css.css',
|
||||
'./wwwroot/css/calendar-sliding-animation.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);
|
||||
}
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue