Last active
March 16, 2023 20:04
-
-
Save a-type/e8eaf3a9818667a5f466750ef0b49df9 to your computer and use it in GitHub Desktop.
Figma icon script
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
const axios = require('axios'); | |
const fs = require('fs-extra'); | |
const path = require('path'); | |
const prettier = require('prettier'); | |
const { camelCase } = require('change-case'); | |
const figmaClient = axios.create({ | |
baseURL: 'https://api.figma.com/v1', | |
headers: { | |
'Content-Type': 'application/json', | |
'X-Figma-Token': process.env.FIGMA_TOKEN, | |
}, | |
}); | |
const FILE_ID = '<< GET FROM THE URL >>'; | |
const PAGE = 'Page 1'; // change these to your structure | |
const FRAME = 'glyph'; | |
const baseDir = path.join(__dirname, '../web/components/icons/generated'); | |
// const nameMatch = /^Name=(.+?),/; | |
const nameMatch = /^(.*)$/; // at the moment icons are just named, no metadata | |
function getIconName(iconNode) { | |
return nameMatch.exec(iconNode.name)[1]; | |
} | |
function renderIconDef(icon) { | |
return `<symbol id="icon-${icon.name}" viewBox="0 0 ${icon.width} ${icon.height}">${icon.svg}</symbol>`; | |
} | |
function stripSvg(svgString) { | |
return svgString.replaceAll(/<svg[^>]*>/g, '').replace(/<\/svg>/g, ''); | |
} | |
// any svg with a fill of #484440 will be replaced with 'currentColor | |
// this allows for secondary colors to exist in the svg | |
function fillCurrentColor(svgString) { | |
return svgString.replaceAll(/fill="#484440"/g, `fill="currentColor"`); | |
} | |
function strokeCurrentColor(svgString) { | |
return svgString.replaceAll(/stroke="#484440"/g, `stroke="currentColor"`); | |
} | |
function reactifyAttributes(svgString) { | |
return svgString.replaceAll( | |
/(\w+?(?:-\w+?)+)=/g, | |
(match, p1) => `${camelCase(p1)}=`, | |
); | |
} | |
function processIconSvg(svgString) { | |
return reactifyAttributes( | |
fillCurrentColor(strokeCurrentColor(stripSvg(svgString))), | |
); | |
} | |
async function downloadImage(url) { | |
const response = await axios.get(url, { responseType: 'arrayBuffer' }); | |
return Buffer.from(response.data, 'binary').toString('utf-8'); | |
} | |
async function genIcons() { | |
const prettierConfig = await prettier.resolveConfig(__dirname); | |
prettierConfig.parser = 'babel-ts'; | |
const figmaFile = await figmaClient.get(`/files/${FILE_ID}`); | |
const figmaPage = figmaFile.data.document.children.find( | |
(page) => page.name === PAGE, | |
); | |
const figmaFrame = figmaPage.children.find((frame) => frame.name === FRAME); | |
const iconIds = figmaFrame.children.map((icon) => icon.id); | |
const iconImagesResponse = await figmaClient.get( | |
`/images/${FILE_ID}?format=svg&ids=${iconIds.join(',')}`, | |
); | |
const iconImages = iconImagesResponse.data.images; | |
// match up images to icon metadata | |
const icons = await Promise.all( | |
figmaFrame.children.map(async (iconNode) => { | |
const iconName = getIconName(iconNode); | |
const iconImage = iconImages[iconNode.id]; | |
return { | |
name: iconName, | |
width: iconNode.absoluteBoundingBox.width, | |
height: iconNode.absoluteBoundingBox.height, | |
svg: processIconSvg(await downloadImage(iconImage)), | |
}; | |
}), | |
); | |
// compile icons into a spritesheet | |
const spritesheetSvg = ` | |
<svg xmlns="http://www.w3.org/2000/svg" style={{ display: 'none' }} {...props}> | |
<defs> | |
${icons.map(renderIconDef).join('\n')} | |
</defs> | |
</svg> | |
`; | |
if (!fs.existsSync(baseDir)) { | |
fs.mkdirSync(baseDir); | |
} | |
fs.writeFileSync( | |
path.join(baseDir, 'IconSpritesheet.tsx'), | |
prettier.format( | |
`// WARNING: generated file! See 'yarn gen:icons'. Do not modify! | |
export const IconSpritesheet = (props: any) => ( | |
${spritesheetSvg} | |
); | |
`, | |
prettierConfig, | |
), | |
); | |
fs.writeFileSync( | |
path.join(baseDir, 'iconNames.ts'), | |
prettier.format( | |
`// WARNING: generated file! See 'yarn gen:icons'. Do not modify! | |
export const iconNames = [${icons | |
.map((i) => `'${i.name}'`) | |
.join(', ')}] as const; | |
export type IconName = typeof iconNames[number]; | |
`, | |
prettierConfig, | |
), | |
); | |
} | |
genIcons(); |
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 { ComponentProps, forwardRef, SVGAttributes } from 'react'; | |
import { styled } from 'stitches.config'; | |
import { IconName } from './generated/iconNames'; | |
export interface IconProps extends ComponentProps<typeof StyledSvg> { | |
name: IconName; | |
} | |
export const Icon = forwardRef<SVGSVGElement, IconProps>(function Icon( | |
{ name, ...rest }, | |
ref, | |
) { | |
return ( | |
<StyledSvg ref={ref} width={24} height={24} {...rest}> | |
<use xlinkHref={`#icon-${name}`} /> | |
</StyledSvg> | |
); | |
}); | |
const StyledSvg = styled('svg', { | |
flexShrink: 0, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment