import { readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "fs"; import { join, relative } from "path"; const ROOT = join(import.meta.dirname, ".."); const SRC = join(ROOT, "src"); const OUT_DIR = join(SRC, "generated"); const OUT_FILE = join(OUT_DIR, "icon-inventory.json"); const OUT_INCLUDE = join(OUT_DIR, "icon-include.mjs"); /** @type {Record} */ const ICON_ALIASES = { "eos-icons:loading": "material-symbols:progress-activity", }; const KNOWN_PREFIXES = new Set([ "material-symbols", "fa6-brands", "fa6-regular", "fa6-solid", "mdi", "logos", "devicon", "hugeicons", "mingcute", "skill-icons", "vscode-icons", "material-icon-theme", "game-icons", "iconoir", ]); const ICON_PATTERN = /(?:name|icon)=["'{]([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9._-]*)/gi; const JSON_ICON_PATTERN = /"icon"\s*:\s*"([^"]+)"/g; const YAML_ICON_PATTERN = /\bicon:\s*["']([^"']+)["']/g; const TS_ICON_PATTERN = /\bicon\s*:\s*["']([^"']+)["']/g; const RETURN_ICON_PATTERN = /return\s+["']([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9._-]*)["']/g; function walk(dir, files = []) { for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (statSync(full).isDirectory()) { walk(full, files); } else { files.push(full); } } return files; } function normalizeIcon(name) { const trimmed = name.trim(); return ICON_ALIASES[trimmed] ?? trimmed; } function addIcon(icons, raw) { const name = normalizeIcon(raw); if (!name.includes(":")) return; const [prefix, iconName] = name.split(":"); if (!prefix || !iconName || !KNOWN_PREFIXES.has(prefix)) return; if (!icons[prefix]) icons[prefix] = new Set(); icons[prefix].add(iconName); } function scanFile(path, icons) { const content = readFileSync(path, "utf8"); const rel = relative(ROOT, path).replace(/\\/g, "/"); if (rel.endsWith(".json")) { for (const match of content.matchAll(JSON_ICON_PATTERN)) { addIcon(icons, match[1]); } return; } if (rel.endsWith(".yaml") || rel.endsWith(".yml")) { for (const match of content.matchAll(YAML_ICON_PATTERN)) { addIcon(icons, match[1]); } return; } if (!/\.(astro|svelte|ts|tsx|js|mjs)$/.test(rel)) return; for (const match of content.matchAll(ICON_PATTERN)) { addIcon(icons, match[1]); } for (const match of content.matchAll(TS_ICON_PATTERN)) { addIcon(icons, match[1]); } for (const match of content.matchAll(RETURN_ICON_PATTERN)) { addIcon(icons, match[1]); } } function main() { /** @type {Record>} */ const icons = {}; const files = walk(SRC); for (const file of files) { if (file.includes("generated")) continue; scanFile(file, icons); } const extraConfigFiles = [join(ROOT, "twilight.config.yaml")]; for (const file of extraConfigFiles) { try { scanFile(file, icons); } catch { /* optional config */ } } /** @type {Record} */ const inventory = {}; for (const [prefix, names] of Object.entries(icons)) { inventory[prefix] = [...names].sort(); } mkdirSync(OUT_DIR, { recursive: true }); writeFileSync(OUT_FILE, `${JSON.stringify(inventory, null, 2)}\n`, "utf8"); writeFileSync( OUT_INCLUDE, `// AUTO-GENERATED by scripts/collect-icons.mjs — do not edit\nexport default ${JSON.stringify(inventory, null, 4)};\n`, "utf8", ); const total = Object.values(inventory).reduce((n, arr) => n + arr.length, 0); console.log(`Collected ${total} icons in ${Object.keys(inventory).length} sets -> ${relative(ROOT, OUT_FILE)}`); } main();