import { writeFile } from "node:fs/promises" import { resolve } 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", "rounded", "sharp"] as const const OUT = resolve( __dirname, "../packages/design-system/lib/components/Icons/MaterialIcon/generated.tsx" ) const PACKAGE_BASE = resolve( __dirname, "../node_modules/@material-symbols/svg-400" ) function camelCase(str: string) { return str.replace(/[-_](\w)/g, (_, c: string) => c.toUpperCase()) } async function main() { 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 fill0Path = resolve(PACKAGE_BASE, style, `${icon}.svg`) const fill1Path = resolve(PACKAGE_BASE, style, `${icon}-fill.svg`) let outlinedVar: string | undefined let filledVar: string | undefined if (existsSync(fill0Path)) { outlinedVar = `${camelCase(icon)}${camelCase(style)}Outlined` imports.push( `import ${outlinedVar} from "@material-symbols/svg-400/${style}/${icon}.svg"` ) parts.push(`outlined: ${outlinedVar}`) } else { missing.push(`${style}/${icon}.svg`) } if (existsSync(fill1Path)) { filledVar = `${camelCase(icon)}${camelCase(style)}Filled` imports.push( `import ${filledVar} from "@material-symbols/svg-400/${style}/${icon}-fill.svg"` ) parts.push(`filled: ${filledVar}`) } 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)