import crypto from "node:crypto" import { createWriteStream } from "node:fs" import { mkdir, readFile, rm, writeFile } from "node:fs/promises" import { join, resolve } from "node:path" import { Readable } from "node:stream" import { pipeline } from "node:stream/promises" import stringify from "json-stable-stringify-without-jsonify" import { fileURLToPath } from "node:url" const __filename = fileURLToPath(import.meta.url) const __dirname = resolve(__filename, "..") // Defines where the font lives const FONT_DIR = resolve(__dirname, "../shared/fonts/material-symbols") // Defines the settings for the font const FONT_BASE_URL = `https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0..1,0` // Defines the subset of icons for the font 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", "charging_station", "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", "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_circle_rounded", "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", "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() function createHash(value: unknown) { const stringified = stringify(value) const hash = crypto.createHash("sha256") hash.update(stringified) return hash.digest("hex") } const hash = createHash(icons).substring(0, 8) async function fetchIconUrl(url: string) { const response = await fetch(url, { headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", }, }) if (!response.ok) { console.error(`Unable to fetch woff2 for ${url}`) process.exit(1) } const text = await response.text() const isWoff2 = /format\('woff2'\)/.test(text) if (!isWoff2) { console.error(`Unable to identify woff2 font in response`) process.exit(1) } const srcUrl = text.match(/src: url\(([^)]+)\)/) if (srcUrl && srcUrl[1]) { return srcUrl[1] } return null } async function download(url: string, destFolder: string) { const dest = resolve(join(destFolder, `/rounded-${hash}.woff2`)) try { const response = await fetch(url) if (!response.ok) { console.error(`Unable to fetch ${url}`) process.exit(1) } if (!response.body) { console.error(`Bad response from ${url}`) process.exit(1) } const fileStream = createWriteStream(dest) // @ts-expect-error: type mismatch const readableNodeStream = Readable.fromWeb(response.body) await pipeline(readableNodeStream, fileStream) } catch (error) { console.error(`Error downloading file from ${url}:`, error) process.exit(1) } } async function cleanFontDirs() { await rm(FONT_DIR, { recursive: true, force: true }) await mkdir(FONT_DIR, { recursive: true }) await writeFile( join(FONT_DIR, ".auto-generated"), `Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.\nhash=${hash}\ncreated=${new Date().toISOString()}\n`, { encoding: "utf-8" } ) } async function updateFontCSS() { const file = resolve(__dirname, "../packages/design-system/lib/fonts.css") const css = await readFile(file, { encoding: "utf-8", }) await writeFile( file, css.replace( /url\(\/_static\/shared\/fonts\/material-symbols\/rounded[^)]+\)/, `url(/_static/shared/fonts/material-symbols/rounded-${hash}.woff2)` ), { encoding: "utf-8", } ) } async function main() { const fontUrl = `${FONT_BASE_URL}&icon_names=${icons.join(",")}&display=block` const iconUrl = await fetchIconUrl(fontUrl) if (iconUrl) { await cleanFontDirs() await download(iconUrl, FONT_DIR) await updateFontCSS() console.log("Successfully updated icons!") process.exit(0) } else { console.error( `Unable to find the icon font src URL in CSS response from Google Fonts at ${fontUrl}` ) } } main()