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 for PlanTempus...\n'); // 1. Run PurgeCSS to find unused CSS console.log('šŸ“Š Running PurgeCSS analysis...'); async function runPurgeCSS() { const purgeCSSResults = await new PurgeCSS().purge({ content: [ './Features/**/*.cshtml', './wwwroot/ts/**/*.ts' ], css: [ './wwwroot/css/*.css' ], rejected: true, rejectedCss: true, safelist: { standard: [ /^swp-/, // All custom web components /^ph-/, // Phosphor icons 'active', // Tab states 'checked', // Checkbox states 'collapsed', 'expanded', 'hidden', 'has-demo-banner', /^owner$/, // Role badges /^admin$/, /^leader$/, /^employee$/, /^purple$/, // Avatar colors /^blue$/, /^amber$/, /^teal$/, /^master$/, // Employee tags /^senior$/, /^junior$/, /^cert$/, /^draft$/, // Status badges /^approved$/, /^invited$/, /^danger$/, // Button variants /^primary$/, /^secondary$/ ] } }); // 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 cssDir = './wwwroot/css'; const cssFiles = fs.readdirSync(cssDir) .filter(file => file.endsWith('.css')) .map(file => path.join(cssDir, file)); 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?\([^)]+\)|var\(--color-[^)]+\)/g) || [])]; const mediaQueries = (content.match(/@media[^{]+/g) || []).length; const cssVariables = [...new Set(content.match(/var\(--[^)]+\)/g) || [])]; 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, cssVariables: cssVariables.length }; } }); 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 totalLines = Object.values(statsReport).reduce((sum, stat) => sum + stat.lines, 0); const html = ` CSS Analysis Report - PlanTempus

šŸ“Š CSS Analysis Report

PlanTempus - Production CSS Analysis

Total CSS Size
${totalSizeKB} KB
CSS Files
${purgeReport.summary.totalFiles}
Total Lines
${totalLines.toLocaleString()}
Unused CSS Rules
${purgeReport.summary.totalRejected}

šŸ“ˆ CSS Statistics by File

${Object.entries(statsReport) .sort((a, b) => b[1].sizeBytes - a[1].sizeBytes) .map(([file, stats]) => ` `).join('')}
File Size Lines Rules Selectors Properties CSS Vars
${file} ${stats.size} ${stats.lines} ${stats.rules} ${stats.selectors} ${stats.properties} ${stats.cssVariables}

šŸ—‘ļø Unused CSS by File

${Object.entries(purgeReport.fileDetails) .sort((a, b) => b[1].rejectedCount - a[1].rejectedCount) .map(([file, details]) => `

${file}

${details.rejectedCount} unused rules Original: ${details.originalSize} | After purge: ${details.purgedSize}

${details.rejectedCount > 0 ? `
Show unused selectors
${details.rejected.slice(0, 50).join('
')} ${details.rejected.length > 50 ? `
... and ${details.rejected.length - 50} more` : ''}
` : '

āœ… No unused CSS found!

'}
`).join('')}

šŸ’” Recommendations

Report generated: ${new Date().toLocaleString('da-DK')}

`; 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); } })();