#!/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'); /** * 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 = [ // Brand/Logo-Icons ohne Lucide-Pendant (Editors picken sie als social.icon) 'google', 'whatsapp', 'mastodon', 'telegram', // Sprach-Logos (CodeBlock: dynamische language->icon-Map, kein literal icon="") 'language-javascript', 'language-python', 'language-rust', ]; /** * Lucide-Icons aus CMS-Daten (Header social.icon, FilesBlock file types, …) + * dynamische Bindings, die der statische Scan nicht findet. */ const EXTRA_LUCIDE = [ // Nav / Header social.icon (CMS) 'calendar-1', 'contact-round', 'search', 'x', 'cloud', 'mail', 'facebook', 'youtube', 'map', 'video', 'instagram', 'twitter', 'linkedin', 'github', 'phone', // PostActions / Links 'share-2', 'link', 'external-link', 'arrow-up-right', // Kontakt/Verzeichnis 'id-card', 'qr-code', 'arrow-down-a-z', 'users', // File-type icons (FilesBlock: dynamic f.iconName) 'file', 'file-text', 'file-spreadsheet', 'file-image', 'file-video', 'file-music', 'file-down', 'presentation', // Tag/flag/info/format fallbacks 'tag', 'flag', 'info', 'type', 'image', // Windkarte / Map / Generator 'fan', 'mouse-pointer-click', 'sliders-horizontal', 'badge-check', ]; 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(prefix) { const pattern = new RegExp(`icon="${prefix}:([a-z0-9-]+)"`, 'g'); const found = new Set(); for (const file of walk(SRC_DIR)) { const content = fs.readFileSync(file, 'utf8'); let m; while ((m = pattern.exec(content)) !== null) { found.add(m[1]); } } return found; } /** Build a tree-shaken subset for one Iconify collection. */ function buildSubset({ prefix, pkg, outFile, extra }) { const iconsJson = path.join(ROOT, 'node_modules', '@iconify-json', pkg, 'icons.json'); if (!fs.existsSync(iconsJson)) { console.error(`[generate-icons] missing ${iconsJson} — run \`npm install\` first.`); process.exit(1); } const collection = JSON.parse(fs.readFileSync(iconsJson, 'utf8')); const wanted = new Set([...scanStatic(prefix), ...extra]); 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(outFile, JSON.stringify(subset, null, 2) + '\n'); const sizeAfter = fs.statSync(outFile).size; console.log( `[generate-icons] ${prefix}: ${Object.keys(icons).length} icons + ${Object.keys(aliases).length} aliases → ${path.relative(ROOT, outFile)} (${(sizeAfter / 1024).toFixed(1)} KB)`, ); if (missing.length) { console.warn(`[generate-icons] ${prefix}: ${missing.length} not found: ${missing.join(', ')}`); } } function main() { buildSubset({ prefix: 'mdi', pkg: 'mdi', outFile: path.join(ROOT, 'src', 'lib', 'iconify-mdi-subset.generated.json'), extra: EXTRA_ICONS, }); buildSubset({ prefix: 'lucide', pkg: 'lucide', outFile: path.join(ROOT, 'src', 'lib', 'iconify-lucide-subset.generated.json'), extra: EXTRA_LUCIDE, }); } main();