Wrap material icon in a empty suspense boundary * Wrap material icon in a empty suspense boundary * skip lazy loading icons * remove suspense boundary * Don't import lazy from react when generating icon file Approved-by: Linus Flood
380 lines
7.3 KiB
TypeScript
380 lines
7.3 KiB
TypeScript
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(
|
|
/<svg[^>]*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<SVGSVGElement>) => (
|
|
<svg viewBox="${viewBox}" {...props}>
|
|
${inner.trim()}
|
|
</svg>
|
|
)
|
|
|
|
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<SVGProps<SVGSVGElement>>
|
|
|
|
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)
|