// @ts-check 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'; // Defines where the font lives const DESIGN_SYSTEM_FONT_DIR = `./packages/design-system/public/_static/fonts/material-symbols`; const WEB_FONT_DIR = `./apps/scandic-web/public/_static/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', 'add_circle', 'air', 'air_purifier_gen', 'airline_seat_recline_normal', 'airplane_ticket', 'apartment', 'apparel', 'arrow_back', 'arrow_forward', 'arrow_right', 'arrow_upward', 'assistant_navigation', 'asterisk', 'attractions', 'award_star', 'bakery_dining', 'balcony', 'bathroom', 'bathtub', 'beach_access', 'bed', 'bedroom_parent', 'box', 'business_center', 'calendar_add_on', 'calendar_clock', 'calendar_month', 'calendar_today', 'call', 'call_quality', 'camera', 'cancel', 'chair', 'charging_station', 'check', 'check_box', 'check_circle', 'checked_bag', 'checkroom', 'chevron_left', 'chevron_right', 'close', 'coffee', 'coffee_maker', 'compare_arrows', 'computer', 'concierge', 'confirmation_number', 'connected_tv', 'content_copy', 'contract', 'cool_to_dry', 'countertops', 'credit_card', 'credit_card_heart', 'curtains', 'curtains_closed', 'deck', 'delete', 'desk', 'device_thermostat', 'diamond', 'dining', 'directions', 'directions_run', 'directions_subway', 'downhill_skiing', 'download', 'dresser', 'edit', 'edit_calendar', 'edit_square', 'electric_bike', 'electric_car', 'elevator', 'emoji_transportation', 'error', 'error_circle_rounded', 'exercise', 'family_restroom', 'fastfood', 'favorite', 'fax', 'featured_seasonal_and_gifts', 'festival', 'filter', 'filter_alt', 'floor_lamp', 'forest', 'garage', 'globe', 'golf_course', 'groups', 'health_and_beauty', 'heat', 'hiking', 'home', 'hot_tub', 'houseboat', 'hvac', 'imagesmode', 'info', 'iron', 'kayaking', 'kettle', 'keyboard_arrow_down', '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', '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', 'travel_luggage_and_bags', 'tv_guide', 'tv_remote', 'upload', 'visibility', 'visibility_off', 'ward', 'warning', 'water_full', 'wifi', 'yard', ].sort(); function createHash(value) { 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) { 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, destFolder) { 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(folderPath) { await rm(DESIGN_SYSTEM_FONT_DIR, { recursive: true, force: true }); await mkdir(DESIGN_SYSTEM_FONT_DIR, { recursive: true }); await rm(WEB_FONT_DIR, { recursive: true, force: true }); await mkdir(WEB_FONT_DIR, { recursive: true }); } async function updateFontCSS() { const file = './packages/design-system/lib/fonts.css'; const css = await readFile(file, { encoding: 'utf-8', }); await writeFile( file, css.replace( /url\(\/_static\/fonts\/material-symbols\/rounded[^)]+\)/, `url(/_static/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, DESIGN_SYSTEM_FONT_DIR); await download(iconUrl, WEB_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();