Skip to content

Instantly share code, notes, and snippets.

@csandman
Created July 25, 2024 22:55
Show Gist options
  • Save csandman/eda60b90fbbeaf01f02ebed5cb5000ec to your computer and use it in GitHub Desktop.
Save csandman/eda60b90fbbeaf01f02ebed5cb5000ec to your computer and use it in GitHub Desktop.
import { transform } from "@svgr/core";
import { readdir, readFile, writeFile, mkdir } from "fs/promises";
import path from "path";
import { rimraf } from "rimraf";
import prettier from "prettier";
const defaultProps = `/** These can be modified to change the default props for all icons */
const defaultProps: IconProps = {
stroke: "currentColor",
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 1.5,
};`;
const defaultPropsFileContents = `import type { IconProps } from "@chakra-ui/react";
${defaultProps}
export default defaultProps;
`;
const templateFileHeader = `import React from "react";
import { createIcon } from "@chakra-ui/react";
import defaultProps from "../default-props";`;
const templateFileContents = `/** ORIGINAL_NAME */
export const ICON_NAME = createIcon({
displayName: "ICON_NAME",
defaultProps,
path: (
PATH_CODE
),
});
`;
const groupFileHeader = `import React from "react";
import { createIcon, type IconProps } from "@chakra-ui/react";`;
const ICONS_DIR = "./nbs-icons";
const getComponentName = (filename) => {
let [name] = filename.split(".");
name = name.replace(/centre/g, "center");
const nameParts = name.split("-");
const capitalizedParts = nameParts.map((part) => {
const isNumber = /^\d+$/.test(part);
return isNumber
? Number(part)
: part.charAt(0).toUpperCase() + part.slice(1);
});
return capitalizedParts.join("");
};
const getFileName = (filename) => {
let [name] = filename.split(".");
// For some reason, the `centre` spelling is used in the file names for some icons
name = name.replace(/centre/g, "center");
const nameParts = name.split("-");
const cleanedParts = nameParts.map((part) => {
const isNumber = /^\d+$/.test(part);
return isNumber ? Number(part) : part;
});
return cleanedParts.join("-");
};
const getIconCode = async (fileName) => {
const originalIconPath = path.join(ICONS_DIR, fileName);
const componentName = getComponentName(fileName);
const svgCode = await readFile(originalIconPath, "utf8");
// Remove all extra path attributes
let cleanedSvgCode = svgCode.replaceAll('stroke="black"', "");
cleanedSvgCode = cleanedSvgCode.replaceAll('stroke-linecap="round"', "");
cleanedSvgCode = cleanedSvgCode.replaceAll('stroke-linejoin="round"', "");
cleanedSvgCode = cleanedSvgCode.replaceAll('stroke-width="2"', "");
const jsCode = await transform(
cleanedSvgCode,
{
plugins: ["@svgr/plugin-svgo", "@svgr/plugin-jsx"],
typescript: true,
icon: true,
typescript: true,
},
{
componentName,
filePath: "./output/alert-circle.tsx",
}
);
let contents = jsCode.match(/<svg[\s\S]+?>([\s\S]+)<\/svg>/)[1];
contents = contents.replaceAll("d=", 'fill="none" d=');
// circles don't use `d` attribute, so we need to add `fill="none"` on the circle itself
contents = contents.replaceAll("<circle", '<circle fill="none"');
const matchCount = contents.match(/</g).length;
if (matchCount > 1) {
contents = `<>${contents}</>`;
}
let iconFileContents = templateFileContents.replace("PATH_CODE", contents);
iconFileContents = iconFileContents.replaceAll("ICON_NAME", componentName);
iconFileContents = iconFileContents.replaceAll(
"ORIGINAL_NAME",
fileName.split(".")[0]
);
const fullIconFileContents = `${templateFileHeader}\n\n${iconFileContents}`;
const formatted = prettier.format(fullIconFileContents, { parser: "babel" });
const newFileName = getFileName(fileName);
await writeFile(path.join("./output/icons", newFileName + ".tsx"), formatted);
return [newFileName, componentName, iconFileContents];
};
const generateIcons = async () => {
await rimraf("./output");
await mkdir("./output/icons", { recursive: true });
const files = await readdir(ICONS_DIR);
const iconFileNames = files.filter((file) => file.includes(".svg"));
const iconParts = await Promise.all(
iconFileNames.map((filename) => getIconCode(filename))
);
const indexFile = iconParts
.map(
([newFileName, componentName]) =>
`export { ${componentName} } from "./icons/${newFileName}";`
)
.join("\n");
const formatttedIndexFile = prettier.format(indexFile, { parser: "babel" });
await writeFile(path.join("./output", "index.ts"), formatttedIndexFile);
await writeFile(
path.join("./output", "default-props.ts"),
prettier.format(defaultPropsFileContents, { parser: "babel" })
);
const iconFileContents = iconParts.map(
([, , iconFileContents]) => iconFileContents
);
const joinedFileContents = [
groupFileHeader,
defaultProps,
...iconFileContents,
].join("\n\n");
const formattedJoinedFileContents = prettier.format(joinedFileContents, {
parser: "babel",
});
await writeFile(
path.join("./output", "icons.tsx"),
formattedJoinedFileContents
);
};
generateIcons();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment