#!/usr/bin/env node /** * Tree-shake @iconify-json/mdi: scan src/ for `icon="mdi:..."` references and * emit a minimal IconifyJSON containing only those icons. Saves ~3 MB in the * client bundle vs. shipping the full 7000+ icon collection. * * Dynamic icons (from CMS data, conditional names) cannot be detected * statically — list them under EXTRA_ICONS below. Anything missing falls * through to the iconify API at runtime, which adds a network roundtrip but * does not break. * * Run via `npm run generate:icons`. Hooked into `prebuild`. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const SRC_DIR = path.join(ROOT, 'src'); const ICONS_JSON = path.join(ROOT, 'node_modules', '@iconify-json', 'mdi', 'icons.json'); const OUT_FILE = path.join(ROOT, 'src', 'lib', 'iconify-mdi-subset.generated.json'); /** * Icons that appear via dynamic bindings (CMS data, conditional expressions) * and would otherwise miss the static scan. Add new ones here when editors * pick icons that appear blank — better than shipping the full collection. */ const EXTRA_ICONS = [ // PostActions: dynamic share/copy state 'share-variant-outline', 'link-variant', // Search: file-type fallback 'file-outline', // Common social icons editors pick (Header.svelte: social.icon) 'facebook', 'instagram', 'twitter', 'youtube', 'linkedin', 'mastodon', 'github', 'email', 'email-outline', 'phone', 'whatsapp', 'telegram', 'import-contacts', 'arrow-top-right', 'open-in-new', 'card-account-details-outline', 'qrcode', 'sort-alphabetical-ascending', 'account-group-outline', // Common file-type icons (FilesBlock: f.iconName) 'file-pdf-box', 'file-word-box', 'file-excel-box', 'file-image', 'file-video', 'file-music', // Tag/link/banner fallbacks (LinkListBlock, Tags, DeadlineBannerBlock) 'link', 'tag-outline', 'flag-outline', 'information-outline', // OrganisationsMapBlock: activate-overlay 'cursor-default-click-outline', // CalendarBlock: social-image modal button 'image-outline', // StellingnahmeGenerator: KI-Regeln-Accordion + Detail-Anpassen 'tune-variant', 'tune', 'check-decagram', ]; const STATIC_PATTERN = /icon="mdi:([a-z0-9-]+)"/g; function walk(dir) { const out = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) { out.push(...walk(full)); } else if (/\.(svelte|ts|tsx|js|mjs)$/.test(entry.name)) { out.push(full); } } return out; } function scanStatic() { const found = new Set(); for (const file of walk(SRC_DIR)) { const content = fs.readFileSync(file, 'utf8'); let m; while ((m = STATIC_PATTERN.exec(content)) !== null) { found.add(m[1]); } } return found; } function main() { if (!fs.existsSync(ICONS_JSON)) { console.error( `[generate-icons] missing ${ICONS_JSON} — run \`npm install\` first.`, ); process.exit(1); } const collection = JSON.parse(fs.readFileSync(ICONS_JSON, 'utf8')); const wanted = new Set([...scanStatic(), ...EXTRA_ICONS]); const icons = {}; const aliases = {}; const missing = []; for (const name of wanted) { if (collection.icons[name]) { icons[name] = collection.icons[name]; } else if (collection.aliases?.[name]) { aliases[name] = collection.aliases[name]; const parent = collection.aliases[name].parent; if (parent && collection.icons[parent] && !icons[parent]) { icons[parent] = collection.icons[parent]; } } else { missing.push(name); } } const subset = { prefix: collection.prefix, width: collection.width, height: collection.height, icons, ...(Object.keys(aliases).length ? { aliases } : {}), }; fs.writeFileSync(OUT_FILE, JSON.stringify(subset, null, 2) + '\n'); const sizeBefore = fs.statSync(ICONS_JSON).size; const sizeAfter = fs.statSync(OUT_FILE).size; console.log( `[generate-icons] ${Object.keys(icons).length} icons + ${Object.keys(aliases).length} aliases → ${path.relative(ROOT, OUT_FILE)}`, ); console.log( `[generate-icons] ${(sizeBefore / 1024 / 1024).toFixed(2)} MB → ${(sizeAfter / 1024).toFixed(1)} KB (${((1 - sizeAfter / sizeBefore) * 100).toFixed(1)}% smaller)`, ); if (missing.length) { console.warn( `[generate-icons] ${missing.length} icon(s) not found in MDI: ${missing.join(', ')}`, ); } } main();