#!/usr/bin/env node const { icons } = require('@iconify-json/material-symbols'); const { getIcons } = require('@iconify/utils'); const fs = require('fs'); const path = require('path'); // Check for verbose flag const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v'); // Logging functions const info = (message) => console.log(message); const debug = (message) => { if (isVerbose) { console.log(message); } }; // Function to scan codebase for LocalIcon usage function scanForUsedIcons() { const usedIcons = new Set(); const srcDir = path.join(__dirname, '..', 'src'); info('🔍 Scanning codebase for LocalIcon usage...'); if (!fs.existsSync(srcDir)) { console.error('❌ Source directory not found:', srcDir); process.exit(1); } // Recursively scan all .tsx and .ts files function scanDirectory(dir) { const files = fs.readdirSync(dir); files.forEach(file => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { scanDirectory(filePath); } else if (file.endsWith('.tsx') || file.endsWith('.ts')) { const content = fs.readFileSync(filePath, 'utf8'); // Match LocalIcon usage: const localIconMatches = content.match(/]*icon="([^"]+)"/g); if (localIconMatches) { localIconMatches.forEach(match => { const iconMatch = match.match(/icon="([^"]+)"/); if (iconMatch) { usedIcons.add(iconMatch[1]); debug(` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); } }); } // Match old material-symbols-rounded spans: icon-name const spanMatches = content.match(/]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g); if (spanMatches) { spanMatches.forEach(match => { const iconMatch = match.match(/>([^<]+)<\/span>/); if (iconMatch && iconMatch[1].trim()) { const iconName = iconMatch[1].trim(); usedIcons.add(iconName); debug(` Found (legacy): ${iconName} in ${path.relative(srcDir, filePath)}`); } }); } // Match Icon component usage: const iconMatches = content.match(/]*icon="material-symbols:([^"]+)"/g); if (iconMatches) { iconMatches.forEach(match => { const iconMatch = match.match(/icon="material-symbols:([^"]+)"/); if (iconMatch) { usedIcons.add(iconMatch[1]); debug(` Found (Icon): ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); } }); } } }); } scanDirectory(srcDir); const iconArray = Array.from(usedIcons).sort(); info(`📋 Found ${iconArray.length} unique icons across codebase`); return iconArray; } // Auto-detect used icons const usedIcons = scanForUsedIcons(); // Check if we need to regenerate (compare with existing) const outputPath = path.join(__dirname, '..', 'src', 'assets', 'material-symbols-icons.json'); let needsRegeneration = true; if (fs.existsSync(outputPath)) { try { const existingSet = JSON.parse(fs.readFileSync(outputPath, 'utf8')); const existingIcons = Object.keys(existingSet.icons || {}).sort(); const currentIcons = [...usedIcons].sort(); if (JSON.stringify(existingIcons) === JSON.stringify(currentIcons)) { needsRegeneration = false; info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`); } } catch (error) { // If we can't parse existing file, regenerate needsRegeneration = true; } } if (!needsRegeneration) { info('🎉 No regeneration needed!'); process.exit(0); } info(`🔍 Extracting ${usedIcons.length} icons from Material Symbols...`); // Extract only our used icons from the full set const extractedIcons = getIcons(icons, usedIcons); if (!extractedIcons) { console.error('❌ Failed to extract icons'); process.exit(1); } // Check for missing icons const extractedIconNames = Object.keys(extractedIcons.icons || {}); const missingIcons = usedIcons.filter(icon => !extractedIconNames.includes(icon)); if (missingIcons.length > 0) { info(`⚠️ Missing icons (${missingIcons.length}): ${missingIcons.join(', ')}`); info('💡 These icons don\'t exist in Material Symbols. Please use available alternatives.'); } // Create output directory const outputDir = path.join(__dirname, '..', 'src', 'assets'); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Write the extracted icon set to a file (outputPath already defined above) fs.writeFileSync(outputPath, JSON.stringify(extractedIcons, null, 2)); info(`✅ Successfully extracted ${Object.keys(extractedIcons.icons || {}).length} icons`); info(`📦 Bundle size: ${Math.round(JSON.stringify(extractedIcons).length / 1024)}KB`); info(`💾 Saved to: ${outputPath}`); // Generate TypeScript types const typesContent = `// Auto-generated icon types // This file is automatically generated by scripts/generate-icons.js // Do not edit manually - changes will be overwritten export type MaterialSymbolIcon = ${usedIcons.map(icon => `'${icon}'`).join(' | ')}; export interface IconSet { prefix: string; icons: Record; width?: number; height?: number; } // Re-export the icon set as the default export with proper typing declare const iconSet: IconSet; export default iconSet; `; const typesPath = path.join(outputDir, 'material-symbols-icons.d.ts'); fs.writeFileSync(typesPath, typesContent); info(`📝 Generated types: ${typesPath}`); info(`🎉 Icon extraction complete!`);