Skip to content

Instantly share code, notes, and snippets.

@forgo
Created June 17, 2025 16:07
Show Gist options
  • Select an option

  • Save forgo/3836f3373b6f219cc14acc0b9b64497b to your computer and use it in GitHub Desktop.

Select an option

Save forgo/3836f3373b6f219cc14acc0b9b64497b to your computer and use it in GitHub Desktop.
Generate React Icons from SVG Directory
// 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