Created
July 25, 2024 22:55
-
-
Save csandman/eda60b90fbbeaf01f02ebed5cb5000ec to your computer and use it in GitHub Desktop.
This file contains 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
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