Created
June 17, 2025 16:07
-
-
Save forgo/3836f3373b6f219cc14acc0b9b64497b to your computer and use it in GitHub Desktop.
Generate React Icons from SVG Directory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // generate-icons.ts | |
| /** | |
| * generate-icons.ts | |
| * | |
| * Run with: | |
| * deno run --allow-read --allow-write generate-icons.ts | |
| * | |
| * This version: | |
| * - Uses @svgr/core@6.5.1?bundle so that Deno pulls in the full SVGR runtime | |
| * - After SVGR transforms each SVG into TSX, runs Prettier (TypeScript parser) | |
| * to format the output before writing to disk. | |
| */ | |
| import { ensureDir } from "https://deno.land/std@0.182.0/fs/mod.ts"; | |
| import { | |
| basename, | |
| extname, | |
| join, | |
| } from "https://deno.land/std@0.182.0/path/mod.ts"; | |
| import { Config, transform } from "https://esm.sh/@svgr/core@6.5.1?bundle"; | |
| // Import Prettier standalone + the TypeScript parser plugin | |
| import parserTs from "https://esm.sh/prettier@2.8.8/parser-typescript"; | |
| import prettier from "https://esm.sh/prettier@2.8.8/standalone"; | |
| /** | |
| * Convert a slug (kebab-case or snake_case) into PascalCase. | |
| * E.g. "airplay-to-tv" → "AirplayToTv" | |
| */ | |
| function toPascalCase(str: string): string { | |
| return str | |
| .split(/[\s\-_]+/) // split on space, dash, underscore | |
| .filter(Boolean) | |
| .map((s) => s[0].toUpperCase() + s.slice(1).toLowerCase()) | |
| .join(""); | |
| } | |
| async function main() { | |
| // 1) Ensure ./icons directory exists | |
| const outDir = "./icons"; | |
| await ensureDir(outDir); | |
| // 2) Loop through every SVG file in ./svg | |
| for await (const entry of Deno.readDir("./svg")) { | |
| if (!entry.isFile) continue; | |
| if (extname(entry.name).toLowerCase() !== ".svg") continue; | |
| // 3) Build the component name | |
| const baseName = basename(entry.name, ".svg"); // e.g. "airplay-to-tv" | |
| const pascal = toPascalCase(baseName); // e.g. "AirplayToTv" | |
| const componentName = `Icon${pascal}`; // e.g. "IconAirplayToTv" | |
| // 4) Read the raw SVG text | |
| const rawSvg = await Deno.readTextFile(join("./svg", entry.name)); | |
| // 5) Define SVGR options, then cast to Config | |
| const svgrOptions: Config = { | |
| typescript: true, | |
| icon: false, | |
| expandProps: false, | |
| replaceAttrValues: { currentColor: "{color}" }, | |
| svgProps: { | |
| className: "{className}", | |
| style: "{style}", | |
| }, | |
| // Use the (variables, context) signature instead of (api, opts, state) | |
| template: (variables, context) => { | |
| // variables contains: { componentName, jsx, … } | |
| // context contains: { tpl, … } (tpl is SVGR’s template builder) | |
| const { componentName, jsx } = variables; | |
| const { tpl } = context; | |
| return tpl` | |
| import { IconBaseProps } from "@lib/components/Icon"; | |
| export function ${componentName}({ | |
| color = "currentColor", | |
| className = "", | |
| style, | |
| }: IconBaseProps) { | |
| return ${jsx}; | |
| } | |
| `; | |
| }, | |
| }; | |
| // 6) Transform the SVG into a TSX string | |
| const tsxCode = await transform(rawSvg, svgrOptions, { componentName }); | |
| // 7) Prettify with Prettier (TypeScript parser) | |
| const formatted = prettier.format(tsxCode, { | |
| parser: "typescript", | |
| plugins: [parserTs], | |
| singleQuote: false, | |
| trailingComma: "all", | |
| printWidth: 80, | |
| }); | |
| // 8) Debug: print the first 60 characters of the result | |
| console.log( | |
| `>>> ${componentName}.tsx (first 60 chars):\n${formatted.slice(0, 60)}\n`, | |
| ); | |
| // 9) Write the prettified code to ./icons/Icon<PascalCasedName>.tsx | |
| const outPath = join(outDir, `${componentName}.tsx`); | |
| await Deno.writeTextFile(outPath, formatted); | |
| console.log(`✅ wrote icons/${componentName}.tsx`); | |
| } | |
| console.log("✔️ All svg processed."); | |
| } | |
| main().catch((err) => { | |
| console.error("❌ Error in generate-icons.ts:", err); | |
| Deno.exit(1); | |
| }); | |
| // generate-icons-index.ts | |
| /** | |
| * generate-icon-index.ts | |
| * | |
| * Usage: | |
| * deno run --allow-read --allow-write generate-icon-index.ts | |
| * | |
| * What it does: | |
| * - Reads the `./icons/` directory. | |
| * - Finds every file matching `Icon*.tsx`. | |
| * - Strips off the `.tsx` extension to get component names (e.g. "IconAirplayToTv"). | |
| * - Emits an `index.ts` in `./icons/` that: | |
| * • Imports each component as a named import: | |
| * import { IconAirplayToTv } from "./IconAirplayToTv"; | |
| * • Builds and exports a single `Icons` object that maps `"AirplayToTv"` → `IconAirplayToTv` | |
| * • Exports a `type IconName = keyof typeof Icons`. | |
| * • Exports `const IconNames: IconName[] = Object.keys(Icons) as IconName[];` | |
| */ | |
| import { ensureDir } from "https://deno.land/std@0.182.0/fs/mod.ts"; | |
| import { | |
| basename, | |
| extname, | |
| join, | |
| } from "https://deno.land/std@0.182.0/path/mod.ts"; | |
| async function main() { | |
| const iconsDir = "./icons"; | |
| // 1) Ensure ./icons exists (in case you run this before generating icons) | |
| await ensureDir(iconsDir); | |
| // 2) Read all directory entries in ./icons | |
| const entries: string[] = []; | |
| for await (const e of Deno.readDir(iconsDir)) { | |
| if (!e.isFile) continue; | |
| if (extname(e.name).toLowerCase() !== ".tsx") continue; | |
| const nameNoExt = basename(e.name, ".tsx"); | |
| if (!nameNoExt.startsWith("Icon")) continue; | |
| entries.push(e.name); | |
| } | |
| // 3) Build a list of component names (strip off .tsx, sort alphabetically) | |
| // e.g. ["IconAdd.tsx", "IconAirplayToTv.tsx", ...] → ["IconAdd", "IconAirplayToTv", ...] | |
| const componentNames = entries | |
| .map((file) => basename(file, ".tsx")) | |
| .sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" })); | |
| // 4) Construct import lines AND the Icons object lines | |
| const importLines: string[] = []; | |
| const iconsObjectLines: string[] = []; | |
| for (const comp of componentNames) { | |
| // Named import form: | |
| // import { IconAirplayToTv } from "./IconAirplayToTv"; | |
| importLines.push(`import { ${comp} } from "./${comp}";`); | |
| // Strip the "Icon" prefix to get the key, e.g. "AirplayToTv" | |
| const key = comp.replace(/^Icon/, ""); | |
| iconsObjectLines.push(` ${key}: ${comp},`); | |
| } | |
| // 5) Assemble index.ts contents | |
| const indexLines = [ | |
| // → IMPORTS | |
| ...importLines, | |
| "", | |
| // → EXPORT: Icons object | |
| "export const Icons = {", | |
| ...iconsObjectLines, | |
| "};", | |
| "", | |
| // → EXPORT: IconName type | |
| "export type IconName = keyof typeof Icons;", | |
| "", | |
| // → EXPORT: IconNames array | |
| "export const IconNames: IconName[] = Object.keys(Icons) as IconName[];", | |
| "", | |
| ]; | |
| const indexContents = indexLines.join("\n"); | |
| // 6) Write ./icons/index.ts | |
| const outPath = join(iconsDir, "index.ts"); | |
| await Deno.writeTextFile(outPath, indexContents); | |
| console.log(`✅ wrote ${outPath}`); | |
| } | |
| main().catch((err) => { | |
| console.error("❌ Error generating index.ts:", err); | |
| Deno.exit(1); | |
| }); | |
| // deno.json | |
| { | |
| "tasks": { | |
| "generate": "deno run --allow-env --allow-sys --allow-read --allow-write generate-icons.ts", | |
| "generate-index": "deno run --allow-env --allow-sys --allow-read --allow-write generate-icons-index.ts" | |
| }, | |
| "imports": { | |
| "@std/assert": "jsr:@std/assert@1" | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment