Created
August 8, 2025 12:44
-
-
Save frankhale/543f53dbf0508d60f2b448a407620cef to your computer and use it in GitHub Desktop.
Finds unused CSS classes in Angular components and SCSS files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fs from 'fs'; | |
import path from 'path'; | |
import { fileURLToPath } from 'url'; | |
const __filename = fileURLToPath(import.meta.url); | |
const __dirname = path.dirname(__filename); | |
// Function to extract CSS class names from SCSS file | |
function extractCssClasses(scssContent) { | |
const classRegex = /\.([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/g; | |
const classes = new Set(); | |
let match; | |
while ((match = classRegex.exec(scssContent)) !== null) { | |
classes.add(match[1]); | |
} | |
return Array.from(classes); | |
} | |
// Function to extract used classes from HTML file | |
function extractUsedClasses(htmlContent) { | |
const usedClasses = new Set(); | |
// Extract classes from standard class attributes | |
const classRegex = /class\s*=\s*["']([^"']*)["']/g; | |
let match; | |
while ((match = classRegex.exec(htmlContent)) !== null) { | |
const classes = match[1].split(/\s+/); | |
classes.forEach(cls => { | |
if (cls.trim()) { | |
usedClasses.add(cls.trim()); | |
} | |
}); | |
} | |
// Extract classes from Angular property binding syntax [class.className] | |
const angularClassRegex = /\[class\.([a-zA-Z][a-zA-Z0-9_-]*)\]/g; | |
while ((match = angularClassRegex.exec(htmlContent)) !== null) { | |
usedClasses.add(match[1]); | |
} | |
return Array.from(usedClasses); | |
} | |
// Function to analyze a component | |
function analyzeComponent(componentPath) { | |
// Get actual file names | |
const scssFiles = fs.readdirSync(componentPath).filter(f => f.endsWith('.component.scss')); | |
const htmlFiles = fs.readdirSync(componentPath).filter(f => f.endsWith('.component.html')); | |
if (scssFiles.length === 0 || htmlFiles.length === 0) { | |
return null; | |
} | |
const scssContent = fs.readFileSync(path.join(componentPath, scssFiles[0]), 'utf8'); | |
const htmlContent = fs.readFileSync(path.join(componentPath, htmlFiles[0]), 'utf8'); | |
const definedClasses = extractCssClasses(scssContent); | |
const usedClasses = extractUsedClasses(htmlContent); | |
const unusedClasses = definedClasses.filter(cls => !usedClasses.includes(cls)); | |
return { | |
component: path.basename(componentPath), | |
scssFile: scssFiles[0], | |
htmlFile: htmlFiles[0], | |
definedClasses, | |
usedClasses, | |
unusedClasses | |
}; | |
} | |
// Main function to scan all components | |
function scanAllComponents() { | |
const srcPath = path.join(__dirname, 'src/app'); | |
const results = []; | |
function scanDirectory(dir) { | |
const items = fs.readdirSync(dir); | |
for (const item of items) { | |
const fullPath = path.join(dir, item); | |
const stat = fs.statSync(fullPath); | |
if (stat.isDirectory()) { | |
// Check if this directory contains component files | |
const hasComponentFiles = fs.readdirSync(fullPath).some(f => | |
f.endsWith('.component.scss') || f.endsWith('.component.html') | |
); | |
if (hasComponentFiles) { | |
const result = analyzeComponent(fullPath); | |
if (result) { | |
results.push(result); | |
} | |
} | |
// Recursively scan subdirectories | |
scanDirectory(fullPath); | |
} | |
} | |
} | |
scanDirectory(srcPath); | |
return results; | |
} | |
// Run the analysis | |
const allResults = scanAllComponents(); | |
const unusedCssResults = allResults.filter(result => result.unusedClasses.length > 0); | |
console.log('=== UNUSED CSS CLASSES ANALYSIS ===\n'); | |
if (unusedCssResults.length === 0) { | |
console.log('✅ No unused CSS classes found!'); | |
} else { | |
console.log(`Found ${unusedCssResults.length} components with unused CSS classes:\n`); | |
unusedCssResults.forEach(result => { | |
console.log(`📁 ${result.component}:`); | |
console.log(` Unused classes (${result.unusedClasses.length}):`); | |
result.unusedClasses.forEach(cls => console.log(` - ${cls}`)); | |
console.log(` Total defined: ${result.definedClasses.length}, Total used: ${result.usedClasses.length}`); | |
console.log(''); | |
}); | |
// Summary | |
const totalUnused = unusedCssResults.reduce((sum, result) => sum + result.unusedClasses.length, 0); | |
const totalDefined = allResults.reduce((sum, result) => sum + result.definedClasses.length, 0); | |
const totalUsed = allResults.reduce((sum, result) => sum + result.usedClasses.length, 0); | |
console.log('=== SUMMARY ==='); | |
console.log(`Total components analyzed: ${allResults.length}`); | |
console.log(`Components with unused classes: ${unusedCssResults.length}`); | |
console.log(`Total unused classes: ${totalUnused}`); | |
console.log(`Total defined classes: ${totalDefined}`); | |
console.log(`Total used classes: ${totalUsed}`); | |
console.log(`Usage rate: ${((totalUsed / totalDefined) * 100).toFixed(1)}%`); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment