cloudflare/kumo
Publicmirrored fromhttps://github.com/cloudflare/kumoAvailable
packages/kumo-figma/src/build-phosphor-icons.ts
170lines · modecode
| 1 | #!/usr/bin/env npx tsx |
| 2 | /** |
| 3 | * Build Phosphor Icons Data |
| 4 | * |
| 5 | * Extracts specific Phosphor icons needed by Figma component generators. |
| 6 | * This runs at BUILD TIME before bundling the plugin. |
| 7 | * |
| 8 | * Usage: |
| 9 | * pnpm --filter @cloudflare/kumo-figma build:data |
| 10 | * |
| 11 | * Output: |
| 12 | * packages/kumo-figma/src/generated/phosphor-icons.json |
| 13 | */ |
| 14 | |
| 15 | import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; |
| 16 | import { join, dirname } from "node:path"; |
| 17 | import { fileURLToPath } from "node:url"; |
| 18 | |
| 19 | const __filename = fileURLToPath(import.meta.url); |
| 20 | const __dirname = dirname(__filename); |
| 21 | |
| 22 | /** |
| 23 | * Icons used by Figma component generators. |
| 24 | * Maps our icon IDs (ph-*) to Phosphor icon names. |
| 25 | */ |
| 26 | const REQUIRED_ICONS: Record<string, string> = { |
| 27 | // DEFAULT_ICONS from icon-utils.ts |
| 28 | "ph-plus": "plus", |
| 29 | "ph-arrows-clockwise": "arrows-clockwise", |
| 30 | "ph-check": "check", |
| 31 | "ph-minus": "minus", |
| 32 | "ph-arrow-right": "arrow-right", |
| 33 | |
| 34 | // Navigation icons |
| 35 | "ph-caret-down": "caret-down", |
| 36 | "ph-caret-up": "caret-up", |
| 37 | "ph-caret-left": "caret-left", |
| 38 | "ph-caret-right": "caret-right", |
| 39 | "ph-caret-double-left": "caret-double-left", |
| 40 | "ph-caret-double-right": "caret-double-right", |
| 41 | "ph-caret-up-down": "caret-up-down", |
| 42 | |
| 43 | // UI icons |
| 44 | "ph-x": "x", |
| 45 | "ph-eye": "eye", |
| 46 | "ph-eye-slash": "eye-slash", |
| 47 | "ph-clipboard": "clipboard", |
| 48 | "ph-copy": "copy", |
| 49 | "ph-trash": "trash", |
| 50 | "ph-pencil": "pencil", |
| 51 | "ph-download": "download", |
| 52 | "ph-share": "share", |
| 53 | "ph-sign-out": "sign-out", |
| 54 | "ph-user": "user", |
| 55 | "ph-gear": "gear", |
| 56 | "ph-info": "info", |
| 57 | "ph-warning": "warning", |
| 58 | "ph-magnifying-glass": "magnifying-glass", |
| 59 | "ph-house": "house", |
| 60 | "ph-bell": "bell", |
| 61 | "ph-database": "database", |
| 62 | "ph-globe-hemisphere-west": "globe-hemisphere-west", |
| 63 | }; |
| 64 | |
| 65 | /** |
| 66 | * Icon data structure for Figma generator |
| 67 | */ |
| 68 | type IconData = { |
| 69 | id: string; |
| 70 | viewBox: string; |
| 71 | content: string; |
| 72 | }; |
| 73 | |
| 74 | /** |
| 75 | * Find the @phosphor-icons/core package in node_modules |
| 76 | */ |
| 77 | function findPhosphorCore(): string { |
| 78 | // Try common locations |
| 79 | const possiblePaths = [ |
| 80 | // pnpm structure |
| 81 | join( |
| 82 | __dirname, |
| 83 | "../../../node_modules/.pnpm/@phosphor-icons+core@2.1.1/node_modules/@phosphor-icons/core", |
| 84 | ), |
| 85 | // Standard node_modules |
| 86 | join(__dirname, "../../node_modules/@phosphor-icons/core"), |
| 87 | join(__dirname, "../node_modules/@phosphor-icons/core"), |
| 88 | ]; |
| 89 | |
| 90 | for (const path of possiblePaths) { |
| 91 | if (existsSync(path)) { |
| 92 | return path; |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | throw new Error( |
| 97 | "@phosphor-icons/core not found. Run: pnpm add -D @phosphor-icons/core --filter @cloudflare/kumo-figma", |
| 98 | ); |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Parse SVG content to extract viewBox and inner content |
| 103 | */ |
| 104 | function parseSvg( |
| 105 | svgContent: string, |
| 106 | ): { viewBox: string; content: string } | null { |
| 107 | const viewBoxMatch = svgContent.match(/viewBox="([^"]+)"/); |
| 108 | const contentMatch = svgContent.match(/<svg[^>]*>([\s\S]*?)<\/svg>/); |
| 109 | |
| 110 | if (!viewBoxMatch || !contentMatch) { |
| 111 | return null; |
| 112 | } |
| 113 | |
| 114 | return { |
| 115 | viewBox: viewBoxMatch[1], |
| 116 | content: contentMatch[1].trim(), |
| 117 | }; |
| 118 | } |
| 119 | |
| 120 | // Main execution |
| 121 | console.log("📖 Building Phosphor icons data..."); |
| 122 | |
| 123 | const phosphorPath = findPhosphorCore(); |
| 124 | const assetsPath = join(phosphorPath, "assets/regular"); |
| 125 | |
| 126 | console.log(`✅ Found @phosphor-icons/core at ${phosphorPath}`); |
| 127 | |
| 128 | const icons: IconData[] = []; |
| 129 | const missingIcons: string[] = []; |
| 130 | |
| 131 | for (const [iconId, phosphorName] of Object.entries(REQUIRED_ICONS)) { |
| 132 | const svgPath = join(assetsPath, `${phosphorName}.svg`); |
| 133 | |
| 134 | if (!existsSync(svgPath)) { |
| 135 | console.warn(`⚠️ Missing icon: ${phosphorName} (${iconId})`); |
| 136 | missingIcons.push(iconId); |
| 137 | continue; |
| 138 | } |
| 139 | |
| 140 | const svgContent = readFileSync(svgPath, "utf-8"); |
| 141 | const parsed = parseSvg(svgContent); |
| 142 | |
| 143 | if (!parsed) { |
| 144 | console.warn(`⚠️ Failed to parse: ${phosphorName}`); |
| 145 | missingIcons.push(iconId); |
| 146 | continue; |
| 147 | } |
| 148 | |
| 149 | icons.push({ |
| 150 | id: iconId, |
| 151 | viewBox: parsed.viewBox, |
| 152 | content: parsed.content, |
| 153 | }); |
| 154 | } |
| 155 | |
| 156 | console.log(`✅ Extracted ${icons.length} icons`); |
| 157 | |
| 158 | if (missingIcons.length > 0) { |
| 159 | console.warn(`⚠️ Missing icons: ${missingIcons.join(", ")}`); |
| 160 | } |
| 161 | |
| 162 | // Ensure generated directory exists |
| 163 | const generatedDir = join(__dirname, "generated"); |
| 164 | mkdirSync(generatedDir, { recursive: true }); |
| 165 | |
| 166 | // Write icon data as JSON |
| 167 | const outputPath = join(generatedDir, "phosphor-icons.json"); |
| 168 | writeFileSync(outputPath, JSON.stringify(icons, null, 2)); |
| 169 | |
| 170 | console.log(`✅ Wrote ${outputPath}`); |
| 171 | |