import crypto from "node:crypto"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { createWriteStream } from "node:fs"; import { resolve, join } 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", "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", "loyalty", "luggage", "mail", "map", "meeting_room", "microwave", "mode_fan", "museum", "music_cast", "music_note", "nature", "night_shelter", "nightlife", "open_in_new", "pan_zoom", "panorama", "pedal_bike", "person", "pets", "phone", "photo_camera", "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", "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", "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();