#!/usr/bin/env node /** * Audit @mana/shared-icons usage to inform the tree-shaking migration. * * The shared-icons package re-exports phosphor-svelte via a barrel * (`export * from 'phosphor-svelte'`). Each phosphor-svelte component * inlines ALL SIX weight variants (thin / light / regular / bold / * fill / duotone) because the weight is a runtime prop. So importing * `` still ships the other five paths. * * Result: the prod bundle has ~466 KB of icon path data across two * chunks (`chunks/*.js` with `` bodies). * * This audit reports: * - Which icons are actually used, sorted by frequency (top N). * - Which weights each icon uses (to gauge how much of the 6x * weight-per-icon cost is actually exercised at runtime). * - Files with the most icon imports (worth splitting first if they * end up in the shared chunk). * * Usage: * node scripts/audit-icon-usage.mjs * node scripts/audit-icon-usage.mjs --top 30 */ import { readdirSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(__dirname, '..'); const WEB_SRC = join(REPO_ROOT, 'apps/mana/apps/web/src'); const args = process.argv.slice(2); const TOP_IDX = args.indexOf('--top'); const TOP_N = TOP_IDX >= 0 ? Number(args[TOP_IDX + 1] || 30) : 30; const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.svelte-kit']); function walk(dir, collect) { for (const ent of readdirSync(dir, { withFileTypes: true })) { if (SKIP_DIRS.has(ent.name)) continue; const p = join(dir, ent.name); if (ent.isDirectory()) walk(p, collect); else if (ent.isFile() && /\.(svelte|ts|tsx|js)$/.test(ent.name)) collect(p); } } function audit() { const files = []; walk(WEB_SRC, (p) => files.push(p)); const iconFrequency = new Map(); // name → count const perFile = new Map(); // file → Set const weightsUsed = new Map(); // name → Set const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@mana\/shared-icons['"]/g; const weightRe = /<\s*([A-Z]\w+)[^>]*\bweight\s*=\s*["']?(\w+)["']?/g; for (const file of files) { const src = readFileSync(file, 'utf8'); let m; importRe.lastIndex = 0; while ((m = importRe.exec(src)) !== null) { const names = m[1] .split(',') .map((n) => n.trim().split(/\s+as\s+/)[0]) .filter(Boolean); for (const n of names) { iconFrequency.set(n, (iconFrequency.get(n) ?? 0) + 1); const rel = file.slice(REPO_ROOT.length + 1); if (!perFile.has(rel)) perFile.set(rel, new Set()); perFile.get(rel).add(n); } } weightRe.lastIndex = 0; while ((m = weightRe.exec(src)) !== null) { const [, name, weight] = m; if (!weightsUsed.has(name)) weightsUsed.set(name, new Set()); weightsUsed.get(name).add(weight); } } const ranked = [...iconFrequency.entries()].sort((a, b) => b[1] - a[1]); console.log(`\n── Icon usage audit ───────────────────────────────────\n`); console.log(`Distinct icons imported: ${iconFrequency.size}`); console.log(`Files importing icons: ${perFile.size}`); console.log( `Bundle cost (estimated): ${(iconFrequency.size * 6 * 0.7).toFixed(0)} KB ` + `(${iconFrequency.size} icons × 6 weights × ~0.7 KB each path)` ); console.log(''); console.log(`Top ${Math.min(TOP_N, ranked.length)} icons by import count:\n`); for (const [name, count] of ranked.slice(0, TOP_N)) { const weights = weightsUsed.get(name); const wStr = weights && weights.size > 0 ? `weights: ${[...weights].sort().join(', ')}` : '—'; console.log(` ${String(count).padStart(4)}× ${name.padEnd(28)} ${wStr}`); } console.log(''); // Weight distribution (how many icons use each weight at all) const weightCounts = new Map(); for (const [, weights] of weightsUsed) { for (const w of weights) weightCounts.set(w, (weightCounts.get(w) ?? 0) + 1); } console.log(`Weights actually used across the codebase:\n`); for (const [weight, count] of [...weightCounts.entries()].sort((a, b) => b[1] - a[1])) { console.log(` ${weight.padEnd(10)} ${count} icon(s)`); } const defaultWeight = iconFrequency.size - (weightCounts.get('regular') ?? 0); console.log(` (unset/default) ~${defaultWeight} icon(s) use the default "regular" weight`); console.log(''); const topFiles = [...perFile.entries()].sort((a, b) => b[1].size - a[1].size).slice(0, 10); console.log(`Top 10 files by distinct-icon count:\n`); for (const [file, icons] of topFiles) { console.log(` ${String(icons.size).padStart(3)} icons ${file}`); } console.log(''); console.log( `Migration path to reduce the 466 KB icon chunks:\n\n` + ` 1. Change @mana/shared-icons/src/index.ts to drop \`export * from\n` + ` 'phosphor-svelte'\` — require per-icon re-exports of only the\n` + ` ${iconFrequency.size} icons actually used.\n\n` + ` 2. OR migrate callers to import directly from phosphor-svelte's\n` + ` per-icon paths: \`import House from 'phosphor-svelte/House'\`.\n\n` + ` 3. Longest-term: build a custom icon set that only ships the\n` + ` weights actually used (most icons only use "regular" or "bold").\n` ); } audit();