diff --git a/package-lock.json b/package-lock.json index 61074ce..46b1e9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "eslint-config-prettier": "8.8.0", "eslint-plugin-prettier": "4.2.1", "figma-api": "^1.11.0", - "got": "11.8.6", + "got": "^11.8.6", "html-webpack-plugin": "^5.5.3", "identity-obj-proxy": "3.0.0", "jest": "29.6.1", diff --git a/package.json b/package.json index 0438193..a07455a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "eslint-config-prettier": "8.8.0", "eslint-plugin-prettier": "4.2.1", "figma-api": "^1.11.0", - "got": "11.8.6", + "got": "^11.8.6", "html-webpack-plugin": "^5.5.3", "identity-obj-proxy": "3.0.0", "jest": "29.6.1", diff --git a/tools/figma/fetch-icons.ts b/tools/figma/fetch-icons.ts index cebac51..f43a67d 100644 --- a/tools/figma/fetch-icons.ts +++ b/tools/figma/fetch-icons.ts @@ -1,13 +1,13 @@ import {Api as FigmaApi} from 'figma-api'; import {getComponents} from './get-components'; import {getIconDescriptions} from './get-icon-descriptions'; -import {getImageLinks} from './get-image-links'; +import {IconDescriptionWithLink, getImageLinks} from './get-image-links'; /** The name of the canvas in the file from which the icons will be loaded */ const CANVAS_NAME = 'glyphs'; const AVAILABLE_SIZES = [14, 24]; -export async function fetchFigmaIcons() { +export async function fetchFigmaIcons(): Promise { const personalAccessToken: string | undefined = process.env.FIGMA_API_TOKEN; const fileId: string | undefined = process.env.FIGMA_FILE_ID; @@ -19,15 +19,8 @@ export async function fetchFigmaIcons() { } const api = new FigmaApi({personalAccessToken}); - const components = await getComponents({fileId, canvasName: CANVAS_NAME}, api); - console.info('Success get components'); - const iconDescriptions = getIconDescriptions(components, AVAILABLE_SIZES); - console.info('Success get icon descriptions'); - const imageLinks = await getImageLinks(iconDescriptions, fileId, api); - console.info('Success load icon images'); - return imageLinks; } diff --git a/tools/figma/get-image-files.ts b/tools/figma/get-image-files.ts new file mode 100644 index 0000000..f95022d --- /dev/null +++ b/tools/figma/get-image-files.ts @@ -0,0 +1,46 @@ +import got from 'got'; +import {IconDescription} from './get-icon-descriptions'; +import {IconDescriptionWithLink} from './get-image-links'; + +const MAX_RETRIES = 20; +/** Our designer marks these icons with this color that do not need to be worked on yet. */ +const ERROR_COLOR_REGEXP = /fill="#C90D0D"/; +/** Default icon color from Figma */ +const FILL_COLOR_REGEXP = /fill="black"/g; + +export type IconDescriptionWithData = IconDescription & { + data: Buffer; +}; + +export const downloadAndTransform = async (icons: IconDescriptionWithLink[]): Promise => { + const iconsWithData = await getImageFiles(icons); + return iconsWithData + .filter((icon) => (icon.exportFormat !== 'svg' ? true : !icon.data.toString().match(ERROR_COLOR_REGEXP))) + .map((icon) => + icon.exportFormat !== 'svg' + ? icon + : {...icon, data: Buffer.from(icon.data.toString().replace(FILL_COLOR_REGEXP, 'fill="currentColor"'))} + ); +}; + +const getImageFiles = async (icons: IconDescriptionWithLink[]): Promise => { + return Promise.all( + icons.map(async (icon) => { + try { + const file = await fetchFile(icon.link); + return {...file, name: icon.name, exportFormat: icon.exportFormat, componentId: icon.componentId}; + } catch (e) { + e.message = `${icon.name}: ${e.message}`; + throw e; + } + }) + ); +}; + +const fetchFile = async (url: string) => { + const response = await got(url, {timeout: 60 * 1000, retry: MAX_RETRIES}); + if (!response.body) { + throw new Error('No response body.'); + } + return {data: response.body}; +}; diff --git a/tools/figma/get-image-links.ts b/tools/figma/get-image-links.ts index 3cc2fdd..cd7373a 100644 --- a/tools/figma/get-image-links.ts +++ b/tools/figma/get-image-links.ts @@ -7,6 +7,7 @@ const IMAGE_SCALE = 1; const ICONS_PER_CHUNK = 100; export type IconDescriptionWithLink = IconDescription & { + componentId: string; link: string; }; type SeparatedIcons = { diff --git a/tools/figma/local.ts b/tools/figma/local.ts new file mode 100644 index 0000000..24ff972 --- /dev/null +++ b/tools/figma/local.ts @@ -0,0 +1,45 @@ +import fs from 'fs/promises'; +import path from 'path'; +import {ExportFormat} from './get-icon-descriptions'; +import {IconDescriptionWithData} from './get-image-files'; + +const BASE_DIR = path.join(__dirname, '../../'); +const ICONS_PATH = path.join(BASE_DIR, 'static/icons'); +const TYPES_PATH = path.join(BASE_DIR, 'src/icons/types'); + +export type FileDescription = { + name: string; + data: Buffer; + exportFormat: ExportFormat; +}; + +export const getFiles = async (): Promise => { + console.info('Getting static resources dir'); + const currentFilenames = await fs.readdir(ICONS_PATH); + const descriptions: FileDescription[] = await Promise.all( + currentFilenames.map(async (filename) => { + const fileExtension = path.parse(filename).ext.slice(1); + if (fileExtension !== 'svg' && fileExtension !== 'png') { + throw new Error('Unknown file extension.'); + } + const cleanFilename = path.parse(filename).name; + const data = await fs.readFile(path.join(ICONS_PATH, filename)); + return { + name: cleanFilename, + data, + exportFormat: fileExtension + }; + }) + ); + console.info(`${descriptions.length} current icons files fetched`); + return descriptions; +}; + +export const updateLocalFiles = async (icons: IconDescriptionWithData[]) => { + await Promise.all( + icons.map((icon) => { + const filePath = path.join(ICONS_PATH, `${icon.name}.${icon.exportFormat}`); + return fs.writeFile(filePath, icon.data, icon.exportFormat === 'svg' ? 'utf-8' : null); + }) + ); +}; diff --git a/tools/figma/update-files.ts b/tools/figma/update-files.ts new file mode 100644 index 0000000..ec2a7ce --- /dev/null +++ b/tools/figma/update-files.ts @@ -0,0 +1,17 @@ +import {differenceBy, intersectionBy} from 'lodash'; +import {fetchFigmaIcons} from '../figma/fetch-icons'; +import {getFiles, updateLocalFiles} from '../figma/local'; +import {downloadAndTransform} from './get-image-files'; + +export const updateFiles = async () => { + const localIcons = await getFiles(); + + const figmaIcons = await fetchFigmaIcons(); + + const existingLocalIcons = intersectionBy(localIcons, figmaIcons, (d) => d.name); + const iconsToDelete = differenceBy(localIcons, existingLocalIcons, (d) => d.name); + + const iconsWithData = await downloadAndTransform(figmaIcons); + + await updateLocalFiles(iconsWithData); +}; diff --git a/tools/scripts/sync-icons.ts b/tools/scripts/sync-icons.ts index ef9174b..65e038e 100644 --- a/tools/scripts/sync-icons.ts +++ b/tools/scripts/sync-icons.ts @@ -1,10 +1,8 @@ -import {fetchFigmaIcons} from '../figma/fetch-icons'; +import {updateFiles} from '../figma/update-files'; async function main() { try { - console.info('Start update figma icons'); - await fetchFigmaIcons(); - console.info('Update figma icons succeed!'); + await updateFiles(); } catch (error) { console.error(error.message || error.toString()); }