import { writeFile, readFile, mkdir, rmdir, readdir, rm, } from "node:fs/promises" import { resolve, join } from "node:path" import { existsSync } from "node:fs" // Your full list of Material Symbol icon names const ICONS = [ "accessibility", "accessible", "acute", "add_circle", "add", "air_purifier_gen", "air", "airline_seat_recline_normal", "airplane_ticket", "apartment", "apparel", "arrow_back_ios", "arrow_back", "arrow_forward_ios", "arrow_forward", "arrow_right", "arrow_upward", "assistant_navigation", "asterisk", "attractions", "award_star", "bakery_dining", "balcony", "bathroom", "bathtub", "beach_access", "bed", "bedroom_parent", "box", "brunch_dining", "business_center", "calendar_add_on", "calendar_clock", "calendar_month", "calendar_today", "call_quality", "call", "camera", "cancel", "chair", "mobile_charge", "check_box", "check_circle", "check", "checked_bag", "checkroom", "chevron_left", "chevron_right", "close", "coffee_maker", "coffee", "compare_arrows", "computer", "concierge", "confirmation_number", "connected_tv", "content_copy", "contract", "cool_to_dry", "countertops", "credit_card_heart", "credit_card", "credit_score", "curtains_closed", "curtains", "dangerous", "deck", "delete", "desk", "device_thermostat", "diamond", "dining", "directions_run", "directions_subway", "directions", "downhill_skiing", "download", "dresser", "edit_calendar", "edit_square", "edit", "electric_bike", "electric_car", "elevator", "emoji_transportation", "error", "exercise", "family_restroom", "fastfood", "favorite", "fax", "featured_seasonal_and_gifts", "festival", "filter_alt", "filter", "format_list_bulleted", "floor_lamp", "forest", "garage", "globe", "golf_course", "groups", "health_and_beauty", "heat", "hiking", "home", "hot_tub", "houseboat", "hvac", "id_card", "imagesmode", "info", "iron", "kayaking", "kettle", "keyboard_arrow_down", "keyboard_arrow_right", "keyboard_arrow_up", "king_bed", "kitchen", "landscape", "laundry", "link", "liquor", "live_tv", "local_bar", "local_cafe", "local_convenience_store", "local_drink", "local_laundry_service", "local_parking", "location_city", "location_on", "lock", "lock_clock", "loyalty", "luggage", "mail", "map", "meeting_room", "microwave", "mode_fan", "museum", "music_cast", "music_note", "nature", "night_shelter", "nightlife", "open_in_new", "pan_zoom", "panorama", "pause", "pedal_bike", "person", "pets", "phone_enabled", "photo_camera", "play_arrow", "pool", "print", "radio", "recommend", "redeem", "refresh", "remove", "restaurant", "room_service", "router", "sailing", "sauna", "scene", "search", "sell", "shopping_bag", "shower", "single_bed", "skateboarding", "smoke_free", "smoking_rooms", "spa", "sports_esports", "sports_golf", "sports_handball", "sports_tennis", "stairs", "star", "sticky_note_2", "straighten", "styler", "support_agent", "swipe", "sync_saved_locally", "table_bar", "theater_comedy", "things_to_do", "train", "tram", "transit_ticket", "travel_luggage_and_bags", "travel", "trophy", "tv_guide", "tv_remote", "upload", "visibility_off", "visibility", "volume_off", "volume_up", "ward", "warning", "water_full", "wifi", "yard", ].sort() const STYLES = ["outlined"] as const const OUT = resolve( __dirname, "../packages/design-system/lib/components/Icons/MaterialIcon/generated.tsx" ) const REACT_OUT = resolve( __dirname, "../packages/design-system/lib/components/Icons/MaterialIcon/generated" ) const PACKAGE_BASE = resolve( __dirname, "../node_modules/@material-symbols/svg-400" ) function camelCase(str: string) { return str.replace(/[-_](\w)/g, (_, c: string) => c.toUpperCase()) } function pascalCase(str: string) { const camel = camelCase(str) return camel.charAt(0).toUpperCase() + camel.slice(1) } async function main() { await rm(REACT_OUT, { recursive: true, force: true }) await mkdir(REACT_OUT, { recursive: true }) const imports: string[] = [] const registryEntries: string[] = [] const missing: string[] = [] for (const icon of ICONS) { const styleEntries: string[] = [] for (const style of STYLES) { const parts: string[] = [] const variants = [ { suffix: "", varNameSuffix: "Outlined", path: resolve(PACKAGE_BASE, style, `${icon}.svg`), key: "outlined", }, { suffix: "-fill", varNameSuffix: "Filled", path: resolve(PACKAGE_BASE, style, `${icon}-fill.svg`), key: "filled", }, ] for (const variant of variants) { if (existsSync(variant.path)) { const content = await readFile(variant.path, "utf8") const svgMatch = content.match( /]*viewBox="([^"]*)"[^>]*>([\s\S]*?)<\/svg>/ ) if (svgMatch) { const viewBox = svgMatch[1] const inner = svgMatch[2] const componentName = `${pascalCase(icon)}${variant.varNameSuffix}` const fileName = `${componentName}.tsx` const filePath = join(REACT_OUT, fileName) const componentContent = ` /* AUTO-GENERATED — DO NOT EDIT */ import type { SVGProps } from "react" const ${componentName} = (props: SVGProps) => ( ${inner.trim()} ) export default ${componentName} `.trim() + "\n" await writeFile(filePath, componentContent, "utf8") imports.push( `import ${componentName} from "./generated/${componentName}"` ) parts.push(`${variant.key}: ${componentName}`) } else { console.warn(`Could not parse SVG for ${variant.path}`) } } else { if (variant.key === "outlined") { missing.push(`${style}/${icon}.svg`) } } } if (parts.length) { styleEntries.push(`${style}: { ${parts.join(", ")} }`) } } // ALWAYS emit the icon key (even if partial) if (styleEntries.length) { registryEntries.push(`"${icon}": { ${styleEntries.join(", ")} },`) } else { missing.push(`❌ no variants for "${icon}"`) } } const content = ` /* AUTO-GENERATED — DO NOT EDIT */ import type { FunctionComponent, SVGProps } from "react" ${imports.join("\n")} type SvgIcon = FunctionComponent> export const materialIcons: Record< string, Partial<{ outlined: { outlined: SvgIcon; filled?: SvgIcon } rounded: { outlined: SvgIcon; filled?: SvgIcon } sharp: { outlined: SvgIcon; filled?: SvgIcon } }> > = { ${registryEntries.join("\n")} } export type MaterialIconName = keyof typeof materialIcons `.trim() + "\n" await writeFile(OUT, content, "utf8") console.log(`✅ Generated ${registryEntries.length} icons`) if (missing.length) { console.warn("⚠️ Missing SVGs:") missing.slice(0, 20).forEach((m) => console.warn(" ", m)) if (missing.length > 20) { console.warn(` …and ${missing.length - 20} more`) } } } main().catch(console.error)