From ae48d934542181457159a38867a9a2c39fa377e2 Mon Sep 17 00:00:00 2001 From: Julian Skinner Date: Thu, 22 Aug 2024 10:42:42 -0500 Subject: [PATCH 1/6] feat: add batching to figma export --- figma-icon-config.json | 3 +- scripts/figma_icons/figma-icon-export.ts | 563 ++++++----------------- scripts/figma_icons/finalize-export.ts | 460 ++++++++++++++++++ scripts/figma_icons/types.ts | 6 + 4 files changed, 608 insertions(+), 424 deletions(-) create mode 100644 scripts/figma_icons/finalize-export.ts diff --git a/figma-icon-config.json b/figma-icon-config.json index 9f64baa..e8c795b 100644 --- a/figma-icon-config.json +++ b/figma-icon-config.json @@ -1,4 +1,5 @@ { "pageName": "Icons", - "downloadPath": "tmp/svg" + "downloadPath": "tmp/svg", + "batchSize": 200 } diff --git a/scripts/figma_icons/figma-icon-export.ts b/scripts/figma_icons/figma-icon-export.ts index b586b21..a4e9cc2 100644 --- a/scripts/figma_icons/figma-icon-export.ts +++ b/scripts/figma_icons/figma-icon-export.ts @@ -1,78 +1,43 @@ import * as dotenv from 'dotenv'; -import { run as optimizeSvgs } from '../optimize-svgs' -import { collectionCopy } from '../collection-copy'; - - /* eslint-disable @typescript-eslint/no-var-requires */ const axios = require('axios'); import chalk from 'chalk'; // Terminal string styling done right import path from 'path'; import fs from 'fs-extra'; import mkdirp from 'mkdirp'; -import cliui from 'cliui'; -import { simpleGit, SimpleGitOptions, StatusResult } from 'simple-git'; -import { FigmaIcon, FigmaIconConfig, IconFileDetail, SvgDiffResult } from './types'; +import { run as finalizeExport } from './finalize-export'; +import { run as optimize } from '../optimize-svgs' + +import { FigmaIcon, FigmaIconConfig } from './types'; if (process.env.NODE_ENV !== 'prod') { dotenv.config({ path: `${process.cwd()}/.env` }); } - const info = chalk.white; const error = chalk.red.bold; const detail = chalk.yellowBright; const log = console.log; -const baseDir = process.cwd(); -const scriptsDir = path.join(baseDir, 'scripts'); -const srcDir = path.join(baseDir, 'src'); -const srcSvgBasePath = path.join(srcDir, 'svg'); - -const date = new Date(); -const strDate = [date.getFullYear().toString(), (date.getMonth() + 1).toString().padStart(2,'0'), date.getDate().toString().padStart(2, '0')].join('-'); +const defaultBatchSize = 500; let figmaClient; +let config: FigmaIconConfig; -const run = async (rootDir: string) => { +const run = async (rootDir: string) : Promise => { try { - const config: FigmaIconConfig = await loadFigmaIconConfig(rootDir); + config = await loadFigmaIconConfig(rootDir); figmaClient = client(config.figmaAccessToken); - const data = await processData(rootDir, config); - - await optimizeSvgs(rootDir, true); + const results = await processData(rootDir, config); - const git = gitClient(); - await git.add(srcSvgBasePath); - - const statusResults: StatusResult = await git.status([srcSvgBasePath]); - const filesChanged = statusResults.files; - - if (filesChanged.length <= 0) { - log(detail('No changes were found. Exiting the process!!!!')) - - removeTmpDirectory(config); - - return; - } + await optimize(rootDir, true); - log('Copying collections...'); - await collectionCopy(rootDir); + await finalizeExport(config, results, rootDir); - log(`${makeResultsTable(data.downloaded)}\n`); - - console.log('Icon Count: ', data.icons.length, 'Result Count: ', data.downloaded.length); - createJsonIconList(data.icons, srcDir); - - await createChangelogHTML(statusResults); - - removeTmpDirectory(config) - - // restore staged files in case of failure - const { exec } = require('node:child_process') - exec(`git restore --staged ${srcSvgBasePath}`) + return results; } catch (e) { log(error(e)); @@ -80,102 +45,6 @@ const run = async (rootDir: string) => { } } -/** - * Reads the file contents to read the Svg File and stores - * into a property to be used later - * - * @param status - staus indicator, this could be D - deleted, M - modified, or empty - new - * @param filePath - relattive path to the file - * @returns - SvgDiffResult - */ -const buildBeforeAndAfterSvg = async (status: string, filePath: string, previousFilePath: string = null): Promise => { - const filename = path.basename(filePath); - const previousFileName = previousFilePath && path.basename(previousFilePath); - - let beforeSvg = null; - let afterSvg = null; - - switch(status) { - case 'D': - beforeSvg = await getFileContentsFromGit(filePath); - break; - - case 'M': - beforeSvg = await getFileContentsFromGit(filePath); - afterSvg = await getFileContentsFromDisk(filename) - break; - - case 'R': - beforeSvg = await getFileContentsFromGit(previousFilePath); - afterSvg = await getFileContentsFromDisk(filename) - break; - - case '': - afterSvg = await getFileContentsFromDisk(filename); - break; - } - - - return { - previousFileName, - filename, - status, - before: beforeSvg, - after: afterSvg, - } -} - -/** - * Generates a Header, table, and description - * - * @param sectionName - name used for the Header - * @param data - an array of {@link SVGDiffResult} - * @param description - text to display under the table as a description - * @returns The string of html used to represent a section e.g Added in the Changelog - */ -const buildHTMLSection = (sectionName: string, data: Array, description: string) => { - const content = `

${sectionName}

`; - const table: string = buildHTMLTable(data, sectionName == 'Renamed'); - const desc = `

${description}

`; - - return [content, table, desc].join('\n'); -} - - -/** - * Generates a HTML Table - * - * @param data an array of {@link SvgDiffResult} - * @returns - The html table markup - */ -const buildHTMLTable = (data: Array, isRenamed = false) => { - const tableRows = data.map((diff) => ` - ${ isRenamed ? `${diff.previousFileName}` : ''} - ${diff.filename} - ${diff.before || ''} - ${diff.after || ''} -`); - - const tableBody = ` - ${tableRows.join('')} - ` - - const table = ` - - - ${ isRenamed ? `` : ''} - ` : 'FileName'} - - - - - ${tableBody} -
Previous Filename${ isRenamed ? `New FilenameBeforeAfter
` - - return table; -} - - /** * Creates the axios client with the appropriate * headers and url. @@ -202,125 +71,6 @@ const client = (apiToken) => { return instance; }; -/** - * Creates the Changelog.html file based on the - * latest data pulled from Figma - * - * @returns The results from SimpleGit.status() - */ -const createChangelogHTML = async (statusResults: StatusResult) => { - const { modified, created, deleted, renamed } = await processStatusResults(statusResults); - - // Adding or Deleting will be Major version bump - // Modifying will be a MINOR version bump - - const html = fs.readFileSync(path.join(scriptsDir, 'figma_icons', 'changelog-template.html'), 'utf8') - .replace(/{{date}}/g, strDate) - .replace(/{{modified}}/g, statusResults.modified.length.toString()) - .replace(/{{deleted}}/g, statusResults.deleted.length.toString()) - .replace(/{{created}}/g, statusResults.created.length.toString()) - .replace(/{{renamed}}/g, statusResults.renamed.length.toString()) - .replace(/{{content}}/g, [created, modified, deleted, renamed].join('\n')); - - const changelogFilename = `${strDate}-changelog.html` - const changelogPath = path.join(baseDir, 'changelogs'); - const fullChangelogFilename = path.join(changelogPath, changelogFilename) - - // Write file to changelogs directory - fs.writeFileSync(fullChangelogFilename, html); - - const arrChangelogs = fs.readdirSync(changelogPath); - - const changelogRecords = []; - let numberOfChangelogs = 0; - - arrChangelogs.reverse().forEach((filename, idx) => { - if ( path.extname(filename) === '.html') { - if (idx < 10 ) { - changelogRecords.push(`${path.basename(filename)}`) - } - numberOfChangelogs++ - } - }) - log('Number of Changelog files found: ', detail(numberOfChangelogs)); - - const indexHtml = fs.readFileSync(path.join(baseDir, 'src','index-template.html'), 'utf8') - .replace(/{{changelogs}}/g, changelogRecords.join('
')); - - // Copy index.html file to www worker folder - fs.writeFileSync(path.join(baseDir, 'src', 'index.html'), indexHtml); - - if ( fs.ensureDir(path.join(baseDir, 'www')) ) { - const wwwChangelogPath = path.join(baseDir, 'www', 'changelogs'); - const wwwChangelogFile = path.join(wwwChangelogPath, changelogFilename); - - // Create Changelogs folder in `www` worker folder - fs.mkdirSync(wwwChangelogPath, { recursive: true }) - fs.copyFileSync(fullChangelogFilename, wwwChangelogFile) - } - - return statusResults; -} - -/** - * Creates JSON data file that contains additional - * metadata - * - * @example - * ``` - * { - * "category": "features", - * "name": "access-key", - * "tags": [ - * "access", - * "key", - * "license", - * "object", - * "password", - * "secure" - * ] - * } - * ``` - * - * @param icons - array of FigmaIcons - * @param outputDir - output directory to save the JSON data - */ -const createJsonIconList = (icons: Array, outputDir: string) => { - try { - icons = icons.sort((a, b) => { - if (a.name < b.name) return -1; - if (a.name > b.name) return 1; - return 0; - }); - - const outputObj = { - icons: icons.map((icon) => { - let tags; - - if (icon.tags) { - tags = icon.tags?.split(',').map((tag) => (tag.trim())).sort(); - } - else { - tags = icon.name.split('-'); - } - - return { - name: icon.name, - category: icon.frame || null, - tags: tags.sort(), - } - } - )}; - - const srcJsonStr = JSON.stringify(outputObj, null, 2) + '\n'; - fs.writeFileSync(path.join(outputDir, 'icon-data.json'), srcJsonStr); - - } - catch (e) { - logErrorMessage('createJsonIconList', e); - } -} - /** * Creates the directory that will * contain the SVGs @@ -352,46 +102,51 @@ const createOutputDirectory = async (outputDir: string) => { * @param outputDir - The directory that the svg will be downloaded * @returns a object with the name and size of the svg */ -const downloadImage = (icon: FigmaIcon, outputDir: string) => { - const nameClean = icon.name.toLowerCase(); - const directory = outputDir; +const downloadImages = (icons: FigmaIcon[], outputDir: string) => { + // Ensure the output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } - const imagePath = path.resolve(directory, `${nameClean}.svg`); - const writer = fs.createWriteStream(imagePath); + // Map each icon to a promise that resolves when the download and write is complete + const downloadPromises = icons.map((icon) => { + const nameClean = icon.name.toLowerCase(); + const imagePath = path.resolve(outputDir, `${nameClean}.svg`); + + return axios.get(icon.url, { responseType: 'arraybuffer' }) + .then((res) => { + fs.writeFileSync(imagePath, res.data); + icon.filesize = fs.statSync(imagePath).size; + log(info('Successfully downloaded and saved:'), detail(`${icon.name}.svg`)); - // log('Image url: ', info(icon.url), 'Frame: ', info(icon.frame)); - // log('Image Path: ', info(imagePath)); + return { + name: `${icon.name}.svg`, + size: icon.filesize + }; + }) + .catch((err) => { + logErrorMessage(`Failed to download ${icon.name} from ${icon.url}:`, err.message); + throw err; // Re-throw to handle in Promise.all + }); + }); - axios.get(icon.url, { responseType: 'stream' }) - .then((res) => { - res.data.pipe(writer) + return Promise.all(downloadPromises) + .then((results) => { + info('All downloads completed successfully.'); + return results; // Array of all icon file details }) .catch((err) => { - if (err.message === 'Socket connection timeout') { - log(detail('Create new Figma access Token'),) - } - - log('Icon name: ', detail(icon.name)) - log('Error details ', '\nMessage: ', error(err.message), '\nUrl: ', detail(err.config.url)) - log(chalk.red.bold('Something went wrong fetching the image from S3, please try again'),) - log(chalk.red.bold(err),) + logErrorMessage('An error occurred while downloading images:', err); + throw err; }); +}; - return new Promise((resolve, reject) => { - writer.on('finish', () => { - // log(info(`Saved ${name}.svg`, fs.statSync(imagePath).size)) - icon.filesize = fs.statSync(imagePath).size; - resolve({ - name: `${icon.name}.svg`, - size: fs.statSync(imagePath).size - }) - }); +const isNotIgnored = (value) => { + return !value.name.startsWith('_'); +} - writer.on('error', (err) => { - console.log('error writting file', err) - reject(err) - }) - }) +const isNotText = (value) => { + return value.type.toLowerCase() !== 'text'; } /** @@ -403,25 +158,27 @@ const downloadImage = (icon: FigmaIcon, outputDir: string) => { * @returns An array of FigmaIcons */ const extractIcons = (pageData, ignoreFrames: string[], componentMetadata) => { - const iconFrames = pageData.children; + const iconFrames = pageData.children.filter(isNotIgnored); + + log(info('Frames to be processed: ', detail(iconFrames.length))); const iconLibrary: Array = []; iconFrames.forEach((frame) => { - if ( ['COMPONENT_SET', 'FRAME'].includes(frame.type) && (!frame.name.startsWith('_') && !ignoreFrames?.includes(frame.name)) ) { - const components = frame.children; + if ( ['COMPONENT_SET', 'FRAME'].includes(frame.type) && !ignoreFrames?.includes(frame.name)) { + const components = frame.children.filter(isNotText); - components.forEach( (component) => { - // if ( componentMetadata[component.id] === undefined) { - // console.log('Frame: ', frame.name, 'Component Id: ', component.id, 'Metadata: ', componentMetadata[component.id]) - // } + log(info('---- Frame:', detail(frame.name), ':', detail(components.length).trim(), 'icons')); + const componentSuffix = config.pageName === pageData.name ? '' : `-${pageData.name}`; + components.forEach( (component) => { const icon = { id: component.id, frame: frame.name.toLowerCase().replaceAll(' ', '-'), - name: !isNaN(component.name) ? `number-${component.name}`: component.name + name: !isNaN(component.name) ? `number-${component.name}${componentSuffix}`: `${component.name}${componentSuffix}` }; + if (componentMetadata[component.id] !== undefined ) { icon["tags"] = componentMetadata[component.id].description } @@ -437,27 +194,24 @@ const extractIcons = (pageData, ignoreFrames: string[], componentMetadata) => { /** * Process that finds and downloads SVGs * - * @param page - Page object in figma + * @param fileId - The unique Id for the Figma File + * @param config - The configuration object + * @param iconLibrary - The array of FigmaIcons + * @returns - An object with the icons and downloaded SVGs */ -const fetchAndDownloadIcons = async (page, fileId: string, config: FigmaIconConfig, componentMetadata) => { +const fetchAndDownloadIcons = async (fileId: string, config: FigmaIconConfig, iconLibrary) => { try { - const iconLibrary = extractIcons(page, config.ignoreFrames, componentMetadata); - - log(chalk.yellowBright(iconLibrary.length), info('icons have been extracted')) - const icons: Array = await fetchImageUrls(fileId, iconLibrary) - await createOutputDirectory(config.downloadPath); - fs.emptyDirSync(config.downloadPath); - - const allIcons = icons.map((icon) => downloadImage(icon, config.downloadPath)); + const outputDirectory = config.downloadPath; - const results = await Promise.all(allIcons).then((res) => { return res; }); + const allIcons = await downloadImages(icons, outputDirectory); + console.log('allIcons: ', allIcons.length) return { icons: iconLibrary, - downloaded: results, + downloaded: allIcons, } } catch (e) { @@ -535,35 +289,6 @@ const findPage = (document, pageName: string,) => { return iconPage }; -/** - * Reads the file contents using git cat-file - * See {@link https://git-scm.com/docs/git-cat-file} for more details - * - * @param filePath - the relative path to the file on disk - * @returns string - */ -const getFileContentsFromGit = async (filePath: string) => { - return await gitClient().catFile(['blob', `HEAD:${filePath}`]); -} - -/** - * Reads the file contents located on disk - * @param filename - the name of the file - * @returns string - */ -const getFileContentsFromDisk = async (filename: string) => { - return await fs.readFileSync(path.join(srcSvgBasePath, filename), 'utf8'); -} - -/** - * Creates an instance of the SimpleGit object - * @param options - list of SimpleGitOptions - * @returns SimpleGit client - */ -const gitClient = (options: Partial = { baseDir: srcSvgBasePath, binary: 'git' } ) => { - return simpleGit(options); - } - /** * Loads the figma-icon-config * @@ -582,6 +307,7 @@ const loadFigmaIconConfig = async (rootDir: string) => { let hasError = false; + hasError ||= setFigmaBatchSize(config); hasError ||= setFigmaAccessToken(config); hasError ||= setFigmaFileId(config); @@ -616,7 +342,6 @@ const processData = async (rootDir: string, config: FigmaIconConfig) => { const branch = figmaData.branches.find(b => b.name === config.branchName) if (!branch) { - // throw error(`No branch found with the name "${chalk.white.bgRed(config.branchName)}"`); log(error('No branch found with the name'), chalk.white.bgRed(config.branchName)); process.exit(1); } @@ -627,55 +352,48 @@ const processData = async (rootDir: string, config: FigmaIconConfig) => { figmaData = await fetchFigmaData(figmaFileId); } + // TODO: Changes need to be made here to iterate multiple pages which will need to include the page name to name icons const page = findPage(figmaData.document, config.pageName); + const iconsArray = extractIcons(page, config.ignoreFrames, figmaData.components); + const batches = splitIntoBatches(iconsArray, config.batchSize); + + log(chalk.yellowBright(iconsArray.length), info('icons have been extracted')) + const response = await fs.emptyDir(config.downloadPath) .then(() => { - return fetchAndDownloadIcons(page, figmaFileId, config, figmaData.components); - }) + let output = { icons: [], downloaded: [] }; - return response; - } catch (e) { - logErrorMessage('processData', e); - } -} + const outputDirectory = config.downloadPath; //.concat(`-${batchNo}`) -/** - * Processes the SimpleGit status results and builds - * the HTML Section data - * - * @param results - list of results from SimpleGit.status - * @returns object - string html data for modifed, created, deleted - */ -const processStatusResults = async (results: StatusResult) => { - const { modified: m, created: n, deleted: d, renamed: r } = results; + createOutputDirectory(outputDirectory); + fs.emptyDirSync(outputDirectory); - let created, deleted, modified, renamed; + const iconResults = Promise.all(batches.map(async (batch, idx) => { + log("Processing batch", chalk.yellowBright(idx+1), " of ", chalk.yellowBright(batches.length, " with ", chalk.yellowBright(batch.length), " icons")); - if (n.length > 0) { - created = await Promise.all(n.map((path) => { return buildBeforeAndAfterSvg('', path)})); - created = buildHTMLSection('Added', created, 'New icons introduced in this version. You will not see them in the "before" column because they did not exist in the previous version.'); - } + const downloaded = await fetchAndDownloadIcons(figmaFileId, config, batch); - if (d.length > 0) { - deleted = await Promise.all(d.map((path) => ( buildBeforeAndAfterSvg('D', path)))); - deleted = buildHTMLSection('Deleted', deleted, 'Present in the previous version but have been removed in this one. You will not see them in the "after" column because they are no longer available.'); - } + return downloaded; + })).then((results) => { + output = results.reduce((acc, iconResult) => { + acc.icons = acc.icons.concat(iconResult.icons); + acc.downloaded = acc.downloaded.concat(iconResult.downloaded); + return acc; + }); - if (m.length > 0) { - modified = await Promise.all(m.map((path) => ( buildBeforeAndAfterSvg('M', path)))); - modified = buildHTMLSection('Modified', modified, 'Changed since the previous version. The change could be visual or in the code behind the icon. If the change is visual, you will see the difference between the "before" and "after" columns. If the change is only in the code, the appearance might remain the same, but it will still be listed as "modified."'); - } + return output; + }); - if (r.length > 0) { - renamed = await Promise.all(r.map((path) => ( buildBeforeAndAfterSvg('R', path.to, path.from)))); - renamed = buildHTMLSection('Renamed', renamed, 'Present in the previous version but have been renamed in this one. You will see both the "Previous" and "New" filename columns. There will not be any visual changes in the "before" or "after" columns.'); - } + return iconResults; + }) - return { created, deleted, modified, renamed }; + return response; + } catch (e) { + logErrorMessage('processData', e); + } } - /***************************/ /* Kicks off the Process */ /***************************/ @@ -689,17 +407,6 @@ run(path.join(__dirname, '../..')); * */ -/** - * Generates a string that represents the number - * of KiB - * - * @param size - number - - * @returns string - */ -const formatSize = (size) => { - return (size / 1024).toFixed(2) + ' KiB' -} - /** * Logs an error message * @param methodName - the name of the method the error occurred in @@ -710,44 +417,36 @@ const logErrorMessage = (methodName: string, err) => { } /** - * Outputs a table of Name and Filesize for each - * file downloaded + * Reads the batchSize if set in the + * configuration file or environment variable. + * If one is not found, it will use the default of 500 * - * @param results - Collection of name and filesize + * @param config - FigmaIconConfig object + * @returns boolean - hasError occurred */ -const makeResultsTable = (results) => { - const ui = cliui({width: 80}); - - ui.div( - makeRow( - chalk.cyan.bold(`File`), - chalk.cyan.bold(`Size`), - ) + `\n\n` + - results.map(asset => makeRow( - asset.name.includes('-duplicate-name') - ? chalk.red.bold(asset.name) - : chalk.green(asset.name), - formatSize(asset.size) - )).join(`\n`) - ) - return ui.toString() -} -const makeRow = (a, b) => { - return ` ${a}\t ${b}\t` -} +const setFigmaBatchSize = (config: FigmaIconConfig) => { + let hasError = false; -/** - * Removes the tmp directory created during - * the download process from the FigmaAPI - * - * @param config - FigmaIconConfig object - */ -const removeTmpDirectory = (config: FigmaIconConfig) => { - log('Removing tmp directory') - const tmpDir = path.join(config.downloadPath, '..'); - fs.rmSync(tmpDir, { force: true, recursive: true }); + switch(true) { + case (!!process.env.BATCH_SIZE == true): + log(info('Using Batch Size in ', detail('ENVIRONMENT variable'))); + config.batchSize = parseInt(process.env.BATCH_SIZE); + break; + + case (!!config.batchSize == true): + log(info('Using Batch Size in ', detail('CONFIGURATION file'))); + break; + + case (!config.batchSize && !process.env.BATCH_SIZE) == true: + config.batchSize = defaultBatchSize + log(info('Using default Batch Size of ', detail(defaultBatchSize))); + break; + } + + return hasError; } + /** * Reads and Sets the Figma access token * @@ -813,3 +512,21 @@ const setFigmaFileId = (config: FigmaIconConfig) => { return hasError; } + +/** + * Splits the result set of FigmaIcons into + * smaller processible batches for Figma API + * + * @param array - an array of FigmaIcon objects + * @param batchSize - size of the batch + * @returns array - an array of arrays of FigmaIcon objects + */ +const splitIntoBatches = ( array: Array, batchSize: number) => { + let batches = []; + for (let i = 0; i < array.length; i += batchSize) { + let batch = array.slice(i, i + batchSize); + batches.push(batch); + } + return batches; +} + diff --git a/scripts/figma_icons/finalize-export.ts b/scripts/figma_icons/finalize-export.ts new file mode 100644 index 0000000..f0320d5 --- /dev/null +++ b/scripts/figma_icons/finalize-export.ts @@ -0,0 +1,460 @@ +import { collectionCopy } from '../collection-copy'; + +import chalk from 'chalk'; // Terminal string styling done right +import cliui from 'cliui'; +import path from 'path'; +import fs from 'fs-extra'; +import { simpleGit, SimpleGitOptions, StatusResult } from 'simple-git'; +import { FigmaIcon, FigmaIconConfig, SvgDiffResult } from './types'; + +const info = chalk.white; +const error = chalk.red.bold; +const detail = chalk.yellowBright; +const log = console.log; + +const baseDir = process.cwd(); +const scriptsDir = path.join(baseDir, 'scripts'); +const srcDir = path.join(baseDir, 'src'); +const srcSvgBasePath = path.join(srcDir, 'svg'); + +const date = new Date(); +const strDate = [date.getFullYear().toString(), (date.getMonth() + 1).toString().padStart(2,'0'), date.getDate().toString().padStart(2, '0')].join('-'); + + +export const run = async(config, data, rootDir: string) => { + const git = gitClient(); + await git.add(srcSvgBasePath); + + + console.log('SVG Base path: ', srcSvgBasePath); + const statusResults: StatusResult = await git.status([srcSvgBasePath]); + const filesChanged = statusResults.files; + + if (filesChanged.length <= 0) { + log(detail('No changes were found. Exiting the process!!!!')) + + removeTmpDirectory(config); + + return; + } + + log('Copying collections...'); + await collectionCopy(rootDir); + + log(`${makeResultsTable(data.downloaded)}\n`); + + createJsonIconList(data.icons, srcDir); + + await createChangelogHTML(statusResults); + + // removeTmpDirectory(config) + + // restore staged files in case of failure + const { exec } = require('node:child_process') + exec(`git restore --staged ${srcSvgBasePath}`) +} + +/** + * + * Helper Methods + * + * + */ + +/** + * Reads the file contents to read the Svg File and stores + * into a property to be used later + * + * @param status - staus indicator, this could be D - deleted, M - modified, or empty - new + * @param filePath - relattive path to the file + * @returns - SvgDiffResult + */ +const buildBeforeAndAfterSvg = async (status: string, filePath: string, previousFilePath: string = null): Promise => { + const filename = path.basename(filePath); + const previousFileName = previousFilePath && path.basename(previousFilePath); + + let beforeSvg = null; + let afterSvg = null; + + switch(status) { + case 'D': + beforeSvg = await getFileContentsFromGit(filePath); + break; + + case 'M': + beforeSvg = await getFileContentsFromGit(filePath); + afterSvg = await getFileContentsFromDisk(filename) + break; + + case 'R': + beforeSvg = await getFileContentsFromGit(previousFilePath); + afterSvg = await getFileContentsFromDisk(filename) + break; + + case '': + afterSvg = await getFileContentsFromDisk(filename); + break; + } + + + return { + previousFileName, + filename, + status, + before: beforeSvg, + after: afterSvg, + } +} + +/** + * Generates a Header, table, and description + * + * @param sectionName - name used for the Header + * @param data - an array of {@link SVGDiffResult} + * @param description - text to display under the table as a description + * @returns The string of html used to represent a section e.g Added in the Changelog + */ +const buildHTMLSection = (sectionName: string, data: Array, description: string) => { + const content = `

${sectionName}

`; + const table: string = buildHTMLTable(data, sectionName == 'Renamed'); + const desc = `

${description}

`; + + return [content, table, desc].join('\n'); +} + + +/** + * Generates a HTML Table + * + * @param data an array of {@link SvgDiffResult} + * @returns - The html table markup + */ +const buildHTMLTable = (data: Array, isRenamed = false) => { + const tableRows = data.map((diff) => ` + ${ isRenamed ? `${diff.previousFileName}` : ''} + ${diff.filename} + ${diff.before || ''} + ${diff.after || ''} +`); + + const tableBody = ` + ${tableRows.join('')} + ` + + const table = ` + + + ${ isRenamed ? `` : ''} + ` : 'FileName'} + + + + + ${tableBody} +
Previous Filename${ isRenamed ? `New FilenameBeforeAfter
` + + return table; +} + +/** + * Creates the Changelog.html file based on the + * latest data pulled from Figma + * + * @returns The results from SimpleGit.status() + */ +const createChangelogHTML = async (statusResults: StatusResult) => { + const { modified, created, deleted, renamed } = await processStatusResults(statusResults); + + // Adding or Deleting will be Major version bump + // Modifying will be a MINOR version bump + + const html = fs.readFileSync(path.join(scriptsDir, 'figma_icons', 'changelog-template.html'), 'utf8') + .replace(/{{date}}/g, strDate) + .replace(/{{modified}}/g, statusResults.modified.length.toString()) + .replace(/{{deleted}}/g, statusResults.deleted.length.toString()) + .replace(/{{created}}/g, statusResults.created.length.toString()) + .replace(/{{renamed}}/g, statusResults.renamed.length.toString()) + .replace(/{{content}}/g, [created, modified, deleted, renamed].join('\n')); + + const changelogFilename = `${strDate}-changelog.html` + const changelogPath = path.join(baseDir, 'changelogs'); + const fullChangelogFilename = path.join(changelogPath, changelogFilename) + + // Write file to changelogs directory + fs.writeFileSync(fullChangelogFilename, html); + + const arrChangelogs = fs.readdirSync(changelogPath); + + const changelogRecords = []; + let numberOfChangelogs = 0; + + arrChangelogs.reverse().forEach((filename, idx) => { + if ( path.extname(filename) === '.html') { + if (idx < 10 ) { + changelogRecords.push(`${path.basename(filename)}`) + } + numberOfChangelogs++ + } + }) + log('Number of Changelog files found: ', detail(numberOfChangelogs)); + + const indexHtml = fs.readFileSync(path.join(baseDir, 'src','index-template.html'), 'utf8') + .replace(/{{changelogs}}/g, changelogRecords.join('
')); + + // Copy index.html file to www worker folder + fs.writeFileSync(path.join(baseDir, 'src', 'index.html'), indexHtml); + + if ( fs.ensureDir(path.join(baseDir, 'www')) ) { + const wwwChangelogPath = path.join(baseDir, 'www', 'changelogs'); + const wwwChangelogFile = path.join(wwwChangelogPath, changelogFilename); + + // Create Changelogs folder in `www` worker folder + fs.mkdirSync(wwwChangelogPath, { recursive: true }) + fs.copyFileSync(fullChangelogFilename, wwwChangelogFile) + } + + return statusResults; +} + +/** + * Creates JSON data file that contains additional + * metadata + * + * @example + * ``` + * { + * "category": "features", + * "name": "access-key", + * "tags": [ + * "access", + * "key", + * "license", + * "object", + * "password", + * "secure" + * ] + * } + * ``` + * + * @param icons - array of FigmaIcons + * @param outputDir - output directory to save the JSON data + */ +const createJsonIconList = (icons: Array, outputDir: string) => { + console.log('Icon count: ', icons.length); + try { + icons = icons.sort((a, b) => { + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + + const outputObj = { + icons: icons.map((icon) => { + let tags; + + if (icon.tags) { + tags = icon.tags?.split(',').map((tag) => (tag.trim())).sort(); + } + else { + tags = icon.name.split('-'); + } + + return { + name: icon.name, + category: icon.frame || null, + tags: tags.sort(), + } + } + )}; + + const srcJsonStr = JSON.stringify(outputObj, null, 2) + '\n'; + fs.writeFileSync(path.join(outputDir, 'icon-data.json'), srcJsonStr); + + } + catch (e) { + logErrorMessage('createJsonIconList', e); + } +} + +/** + * Generates a string that represents the number + * of KiB + * + * @param size - number - + * @returns string + */ +const formatSize = (size) => { + return (size / 1024).toFixed(2) + ' KiB' +} + +/** + * Reads the file contents located on disk + * @param filename - the name of the file + * @returns string + */ +const getFileContentsFromDisk = async (filename: string) => { + return await fs.readFileSync(path.join(srcSvgBasePath, filename), 'utf8'); +} + +/** + * Reads the file contents using git cat-file + * See {@link https://git-scm.com/docs/git-cat-file} for more details + * + * @param filePath - the relative path to the file on disk + * @returns string + */ +const getFileContentsFromGit = async (filePath: string) => { + return await gitClient().catFile(['blob', `HEAD:${filePath}`]); +} + +/** + * Creates an instance of the SimpleGit object + * @param options - list of SimpleGitOptions + * @returns SimpleGit client + */ +const gitClient = (options: Partial = { baseDir: srcSvgBasePath, binary: 'git' } ) => { + return simpleGit(options); + } + + /** + * Loads the figma-icon-config + * + * @params rootDir - The source directory + * @returns FigmaIconConfig object + */ +const loadFigmaIconConfig = async (rootDir: string) => { + try { + const configFile = path.resolve(path.join(rootDir, 'figma-icon-config.json')); + + if (fs.existsSync(configFile)) { + log(info('Config file located at: ', detail(configFile))); + + const strConfig = await fs.readFile(configFile, 'utf-8'); + const config = JSON.parse(strConfig) as FigmaIconConfig; + + let hasError = false; + + hasError ||= setDownloadPath(config); + + + if (hasError) { + logErrorMessage('loadFigmaIconConfig', null); + process.exit(1); + } + + return config; + } + } + catch (e) { + logErrorMessage('loadFigmaIconConfig', e); + process.exit(1); + } +} +/** + * Logs an error message + * @param methodName - the name of the method the error occurred in + * @param err - the error + */ +const logErrorMessage = (methodName: string, err) => { + log('Error in ' , detail(methodName), '\n Message: ', error(err)); +} + +/** + * Outputs a table of Name and Filesize for each + * file downloaded + * + * @param results - Collection of name and filesize + */ +const makeResultsTable = (results) => { + const ui = cliui({width: 80}); + + console.log('Results: ', results.length); + ui.div( + makeRow( + chalk.cyan.bold(`File`), + chalk.cyan.bold(`Size`), + ) + `\n\n` + + results.map(asset => makeRow( + asset.name.includes('-duplicate-name') + ? chalk.red.bold(asset.name) + : chalk.green(asset.name), + formatSize(asset.size) + )).join(`\n`) + ) + return ui.toString() +} + +const makeRow = (a, b) => { + return ` ${a}\t ${b}\t` +} + +/** + * Processes the SimpleGit status results and builds + * the HTML Section data + * + * @param results - list of results from SimpleGit.status + * @returns object - string html data for modifed, created, deleted + */ +const processStatusResults = async (results: StatusResult) => { + const { modified: m, created: n, deleted: d, renamed: r } = results; + + let created, deleted, modified, renamed; + + if (n.length > 0) { + created = await Promise.all(n.map((path) => { return buildBeforeAndAfterSvg('', path)})); + created = buildHTMLSection('Added', created, 'New icons introduced in this version. You will not see them in the "before" column because they did not exist in the previous version.'); + } + + if (d.length > 0) { + deleted = await Promise.all(d.map((path) => ( buildBeforeAndAfterSvg('D', path)))); + deleted = buildHTMLSection('Deleted', deleted, 'Present in the previous version but have been removed in this one. You will not see them in the "after" column because they are no longer available.'); + } + + if (m.length > 0) { + modified = await Promise.all(m.map((path) => ( buildBeforeAndAfterSvg('M', path)))); + modified = buildHTMLSection('Modified', modified, 'Changed since the previous version. The change could be visual or in the code behind the icon. If the change is visual, you will see the difference between the "before" and "after" columns. If the change is only in the code, the appearance might remain the same, but it will still be listed as "modified."'); + } + + if (r.length > 0) { + renamed = await Promise.all(r.map((path) => ( buildBeforeAndAfterSvg('R', path.to, path.from)))); + renamed = buildHTMLSection('Renamed', renamed, 'Present in the previous version but have been renamed in this one. You will see both the "Previous" and "New" filename columns. There will not be any visual changes in the "before" or "after" columns.'); + } + + return { created, deleted, modified, renamed }; +} + +/** + * Removes the tmp directory created during + * the download process from the FigmaAPI + * + * @param config - FigmaIconConfig object + */ +const removeTmpDirectory = (config: FigmaIconConfig) => { + log('Removing tmp directory') + const tmpDir = path.join(config.downloadPath, '..'); + fs.rmSync(tmpDir, { force: true, recursive: true }); +} + +/** + * Reads and Sets the Figma access token + * + * @params config - FigmaIconConfig object + * @returns boolean - hasError occurred + */ +const setDownloadPath = (config: FigmaIconConfig) => { + let hasError = false; + + // DownloadPath check + switch(true) { + case (!!config.downloadPath == true): + log(info('Using Download Path in ', detail('CONFIGURATION file'))); + break; + + case (!config.downloadPath) == true: + hasError ||= true; + log(error('No downloadPath has been provided, please set in figma-icon-config.json!!!!!')); + break; + } + + return hasError; +} diff --git a/scripts/figma_icons/types.ts b/scripts/figma_icons/types.ts index eebbabc..036ef28 100644 --- a/scripts/figma_icons/types.ts +++ b/scripts/figma_icons/types.ts @@ -1,4 +1,10 @@ export interface FigmaIconConfig { + /** + * The number of images to process at a time + * + */ + batchSize: number; + /** * Branch that is located in the * Figma file From 661089943d8ea8d813c51c027db51fa959f2c3bf Mon Sep 17 00:00:00 2001 From: Julian Skinner Date: Tue, 27 Aug 2024 11:37:12 -0500 Subject: [PATCH 2/6] chore: update figma icon export to handle multiple pages and sending request in batches --- figma-icon-config.json | 4 +- scripts/figma_icons/figma-icon-export.ts | 146 +++++++++++++---------- scripts/figma_icons/finalize-export.ts | 76 ++---------- scripts/figma_icons/types.ts | 2 +- scripts/optimize-svgs.ts | 1 - scripts/readme.md | 4 +- 6 files changed, 95 insertions(+), 138 deletions(-) diff --git a/figma-icon-config.json b/figma-icon-config.json index e8c795b..af38310 100644 --- a/figma-icon-config.json +++ b/figma-icon-config.json @@ -1,5 +1,5 @@ { - "pageName": "Icons", + "pageNames": ["Icons", "Filled", "Duotone"], "downloadPath": "tmp/svg", - "batchSize": 200 + "batchSize": 500 } diff --git a/scripts/figma_icons/figma-icon-export.ts b/scripts/figma_icons/figma-icon-export.ts index a4e9cc2..0322f85 100644 --- a/scripts/figma_icons/figma-icon-export.ts +++ b/scripts/figma_icons/figma-icon-export.ts @@ -8,7 +8,7 @@ import fs from 'fs-extra'; import mkdirp from 'mkdirp'; import { run as finalizeExport } from './finalize-export'; -import { run as optimize } from '../optimize-svgs' +import { run as optimizeSvgs} from '../optimize-svgs' import { FigmaIcon, FigmaIconConfig } from './types'; @@ -28,14 +28,45 @@ let config: FigmaIconConfig; const run = async (rootDir: string) : Promise => { try { + const optimizedOutputDir = path.join(rootDir, 'src'); + const optimizedOutputSvgDir = path.join(optimizedOutputDir, 'svg'); + config = await loadFigmaIconConfig(rootDir); figmaClient = client(config.figmaAccessToken); - const results = await processData(rootDir, config); + config.downloadPath = path.join(rootDir, config.downloadPath); + + await fs.emptyDir(config.downloadPath) + await fs.emptyDir(optimizedOutputSvgDir); + + const results = await config.pageNames.reduce(async (accPromise, pageName) => { + const acc = await accPromise; + + const result = await processData(rootDir, config, pageName); + + await optimizeSvgs(rootDir, true); + + log(info('Total results processed: ', detail(result.downloaded.length), 'for Page: ', detail(pageName))); + + acc.push(result); + + return acc; + }, Promise.resolve([])); // Initial value is a resolved promise with an empty array + - await optimize(rootDir, true); + const flattenedResults = results.reduce((acc, item) => { + // Combine icons + acc.icons = [...acc.icons, ...item.icons]; - await finalizeExport(config, results, rootDir); + // Combine downloaded + acc.downloaded = [...acc.downloaded, ...item.downloaded]; + + return acc; + }, { icons: [], downloaded: [] }); + + await finalizeExport(config, flattenedResults, rootDir); + + removeTmpDirectory(config); return results; } @@ -71,30 +102,6 @@ const client = (apiToken) => { return instance; }; -/** - * Creates the directory that will - * contain the SVGs - * - * @param outputDir - The directory name to create - */ -const createOutputDirectory = async (outputDir: string) => { - return new Promise((resolve) => { - const directory = path.resolve(outputDir); - - if(!fs.existsSync(directory)) { - log(info(`Directory ${outputDir} does not exist`)); - - if (mkdirp.sync(directory)) { - log(info(`Created directory ${outputDir}`)) - resolve(); - } - } - else { - resolve(); - } - }) -} - /** * Downloads the images * @@ -102,7 +109,7 @@ const createOutputDirectory = async (outputDir: string) => { * @param outputDir - The directory that the svg will be downloaded * @returns a object with the name and size of the svg */ -const downloadImages = (icons: FigmaIcon[], outputDir: string) => { +const downloadImages = (icons: FigmaIcon[], outputDir: string, pageName: string) => { // Ensure the output directory exists if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); @@ -111,16 +118,18 @@ const downloadImages = (icons: FigmaIcon[], outputDir: string) => { // Map each icon to a promise that resolves when the download and write is complete const downloadPromises = icons.map((icon) => { const nameClean = icon.name.toLowerCase(); - const imagePath = path.resolve(outputDir, `${nameClean}.svg`); + const iconNameSuffix = pageName === 'Icons' ? '' : `-${pageName.toLowerCase()}`; + const filename= `${nameClean}${iconNameSuffix}.svg`; + const imagePath = path.resolve(outputDir, filename); return axios.get(icon.url, { responseType: 'arraybuffer' }) .then((res) => { fs.writeFileSync(imagePath, res.data); icon.filesize = fs.statSync(imagePath).size; - log(info('Successfully downloaded and saved:'), detail(`${icon.name}.svg`)); + log(info('Successfully downloaded and saved:'), detail(filename)); return { - name: `${icon.name}.svg`, + name: filename, size: icon.filesize }; }) @@ -157,11 +166,11 @@ const isNotText = (value) => { * @param componentMetadata - an object containing additional details for the icon * @returns An array of FigmaIcons */ -const extractIcons = (pageData, ignoreFrames: string[], componentMetadata) => { +const extractIcons = (pageData, ignoreFrames: string[], componentMetadata, pageName) => { const iconFrames = pageData.children.filter(isNotIgnored); - log(info('Frames to be processed: ', detail(iconFrames.length))); + log(info('-- # of Frames to be processed: ', detail(iconFrames.length))); const iconLibrary: Array = []; iconFrames.forEach((frame) => { @@ -170,7 +179,7 @@ const extractIcons = (pageData, ignoreFrames: string[], componentMetadata) => { log(info('---- Frame:', detail(frame.name), ':', detail(components.length).trim(), 'icons')); - const componentSuffix = config.pageName === pageData.name ? '' : `-${pageData.name}`; + const componentSuffix = pageName === pageData.name ? '' : `-${pageData.name}`; components.forEach( (component) => { const icon = { id: component.id, @@ -200,14 +209,13 @@ const extractIcons = (pageData, ignoreFrames: string[], componentMetadata) => { * @param iconLibrary - The array of FigmaIcons * @returns - An object with the icons and downloaded SVGs */ -const fetchAndDownloadIcons = async (fileId: string, config: FigmaIconConfig, iconLibrary) => { +const fetchAndDownloadIcons = async (fileId: string, config: FigmaIconConfig, iconLibrary, pageName: string) => { try { const icons: Array = await fetchImageUrls(fileId, iconLibrary) const outputDirectory = config.downloadPath; - const allIcons = await downloadImages(icons, outputDirectory); - console.log('allIcons: ', allIcons.length) + const allIcons = await downloadImages(icons, outputDirectory, pageName); return { icons: iconLibrary, @@ -331,10 +339,8 @@ const loadFigmaIconConfig = async (rootDir: string) => { * @params rootDir - The initial starting directory * @params config - The config data */ -const processData = async (rootDir: string, config: FigmaIconConfig) => { +const processData = async (rootDir: string, config: FigmaIconConfig, pageName) => { try { - config.downloadPath = path.join(rootDir, config.downloadPath); - let figmaFileId = config.figmaFileId; let figmaData = await fetchFigmaData(figmaFileId); @@ -352,43 +358,39 @@ const processData = async (rootDir: string, config: FigmaIconConfig) => { figmaData = await fetchFigmaData(figmaFileId); } - // TODO: Changes need to be made here to iterate multiple pages which will need to include the page name to name icons - const page = findPage(figmaData.document, config.pageName); - - const iconsArray = extractIcons(page, config.ignoreFrames, figmaData.components); + log(info('Page to be processed: ', detail(pageName))); + const page = findPage(figmaData.document, pageName); + const iconsArray = extractIcons(page, config.ignoreFrames, figmaData.components, pageName); const batches = splitIntoBatches(iconsArray, config.batchSize); log(chalk.yellowBright(iconsArray.length), info('icons have been extracted')) - const response = await fs.emptyDir(config.downloadPath) - .then(() => { - let output = { icons: [], downloaded: [] }; - - const outputDirectory = config.downloadPath; //.concat(`-${batchNo}`) + let output = { icons: [], downloaded: [] }; - createOutputDirectory(outputDirectory); - fs.emptyDirSync(outputDirectory); + const outputDirectory = config.downloadPath; //.concat(`-${batchNo}`) - const iconResults = Promise.all(batches.map(async (batch, idx) => { - log("Processing batch", chalk.yellowBright(idx+1), " of ", chalk.yellowBright(batches.length, " with ", chalk.yellowBright(batch.length), " icons")); + const iconResults = Promise.all(batches.map(async (batch, idx) => { + log("Processing batch", chalk.yellowBright(idx+1), " of ", chalk.yellowBright(batches.length, " with ", chalk.yellowBright(batch.length), " icons")); - const downloaded = await fetchAndDownloadIcons(figmaFileId, config, batch); - - return downloaded; - })).then((results) => { - output = results.reduce((acc, iconResult) => { - acc.icons = acc.icons.concat(iconResult.icons); - acc.downloaded = acc.downloaded.concat(iconResult.downloaded); - return acc; - }); + const downloaded = await fetchAndDownloadIcons(figmaFileId, config, batch, pageName); + return downloaded; + })).then((results) => { + if (results.length === 0) { return output; + } + + output = results.reduce((acc, iconResult) => { + acc.icons = acc.icons.concat(iconResult.icons); + acc.downloaded = acc.downloaded.concat(iconResult.downloaded); + return acc; }); - return iconResults; - }) + return output; + }); + + return iconResults; - return response; } catch (e) { logErrorMessage('processData', e); } @@ -416,6 +418,18 @@ const logErrorMessage = (methodName: string, err) => { log('Error in ' , detail(methodName), '\n Message: ', error(err)); } +/** + * Removes the tmp directory created during + * the download process from the FigmaAPI + * + * @param config - FigmaIconConfig object + */ +const removeTmpDirectory = (config: FigmaIconConfig) => { + log('Removing tmp directory') + const tmpDir = path.join(config.downloadPath, '..'); + fs.rmSync(tmpDir, { force: true, recursive: true }); +} + /** * Reads the batchSize if set in the * configuration file or environment variable. diff --git a/scripts/figma_icons/finalize-export.ts b/scripts/figma_icons/finalize-export.ts index f0320d5..8b67f13 100644 --- a/scripts/figma_icons/finalize-export.ts +++ b/scripts/figma_icons/finalize-export.ts @@ -25,8 +25,6 @@ export const run = async(config, data, rootDir: string) => { const git = gitClient(); await git.add(srcSvgBasePath); - - console.log('SVG Base path: ', srcSvgBasePath); const statusResults: StatusResult = await git.status([srcSvgBasePath]); const filesChanged = statusResults.files; @@ -41,14 +39,14 @@ export const run = async(config, data, rootDir: string) => { log('Copying collections...'); await collectionCopy(rootDir); - log(`${makeResultsTable(data.downloaded)}\n`); + // log(`${makeResultsTable(data.downloaded)}\n`); + log(info('Creating JSON Icon List')); createJsonIconList(data.icons, srcDir); + log(info('Calling createChangelogHTML')); await createChangelogHTML(statusResults); - // removeTmpDirectory(config) - // restore staged files in case of failure const { exec } = require('node:child_process') exec(`git restore --staged ${srcSvgBasePath}`) @@ -240,7 +238,6 @@ const createChangelogHTML = async (statusResults: StatusResult) => { * @param outputDir - output directory to save the JSON data */ const createJsonIconList = (icons: Array, outputDir: string) => { - console.log('Icon count: ', icons.length); try { icons = icons.sort((a, b) => { if (a.name < b.name) return -1; @@ -314,42 +311,8 @@ const getFileContentsFromGit = async (filePath: string) => { */ const gitClient = (options: Partial = { baseDir: srcSvgBasePath, binary: 'git' } ) => { return simpleGit(options); - } - - /** - * Loads the figma-icon-config - * - * @params rootDir - The source directory - * @returns FigmaIconConfig object - */ -const loadFigmaIconConfig = async (rootDir: string) => { - try { - const configFile = path.resolve(path.join(rootDir, 'figma-icon-config.json')); - - if (fs.existsSync(configFile)) { - log(info('Config file located at: ', detail(configFile))); - - const strConfig = await fs.readFile(configFile, 'utf-8'); - const config = JSON.parse(strConfig) as FigmaIconConfig; - - let hasError = false; - - hasError ||= setDownloadPath(config); - - - if (hasError) { - logErrorMessage('loadFigmaIconConfig', null); - process.exit(1); - } - - return config; - } - } - catch (e) { - logErrorMessage('loadFigmaIconConfig', e); - process.exit(1); - } } + /** * Logs an error message * @param methodName - the name of the method the error occurred in @@ -362,7 +325,12 @@ const logErrorMessage = (methodName: string, err) => { /** * Outputs a table of Name and Filesize for each * file downloaded - * + * @example + * File Size + * ios-battery.svg 0.91 KiB + * ios-wifi.svg 1.23 KiB + * ios-data.svg 1.00 KiB + * klarna.svg 0.53 KiB * @param results - Collection of name and filesize */ const makeResultsTable = (results) => { @@ -434,27 +402,3 @@ const removeTmpDirectory = (config: FigmaIconConfig) => { const tmpDir = path.join(config.downloadPath, '..'); fs.rmSync(tmpDir, { force: true, recursive: true }); } - -/** - * Reads and Sets the Figma access token - * - * @params config - FigmaIconConfig object - * @returns boolean - hasError occurred - */ -const setDownloadPath = (config: FigmaIconConfig) => { - let hasError = false; - - // DownloadPath check - switch(true) { - case (!!config.downloadPath == true): - log(info('Using Download Path in ', detail('CONFIGURATION file'))); - break; - - case (!config.downloadPath) == true: - hasError ||= true; - log(error('No downloadPath has been provided, please set in figma-icon-config.json!!!!!')); - break; - } - - return hasError; -} diff --git a/scripts/figma_icons/types.ts b/scripts/figma_icons/types.ts index 036ef28..5e172c3 100644 --- a/scripts/figma_icons/types.ts +++ b/scripts/figma_icons/types.ts @@ -51,7 +51,7 @@ export interface FigmaIconConfig { /** * Page that contains the icons */ - pageName: string, + pageNames: string[], } export type FigmaIcon = { diff --git a/scripts/optimize-svgs.ts b/scripts/optimize-svgs.ts index 8cd29bc..c3ad658 100644 --- a/scripts/optimize-svgs.ts +++ b/scripts/optimize-svgs.ts @@ -52,7 +52,6 @@ export const run = async(rootDir: string, optimizeFiles = false) => { if (optimizeFiles) { log('Optimized OuputSvgDir: ', optimizedOutputSvgDir) - await fs.emptyDir(optimizedOutputSvgDir); log('Optimizing SVGs...'); await optimizeSvgs(srcSvgData); diff --git a/scripts/readme.md b/scripts/readme.md index ea3442f..a7ba922 100644 --- a/scripts/readme.md +++ b/scripts/readme.md @@ -25,7 +25,7 @@ figma-icon-config.json "figmaAccessToken": "some-access-token", "figmaFileId": "figma-file-id", "branchName": "reorg", - "pageName": "Icons", + "pageNames": ["Icons"], "outputPath": "src/svg", "ignoreFrames": ["_Countries", "Docs"] } @@ -37,7 +37,7 @@ figma-icon-config.json |figmaAccessToken|string|true|FIGMA_ACCESS_TOKEN|The Personal Access token that you have created in your account. See [above](#creating-a-figma-access-token). You can either set in the config or as environment variable, but one needs to be set.| |figmaFileId|string|true|FIGMA_FILE_ID|The unique id for the file. This can be found in the url, when viewing the Figma file. e.g `https://www.figma.com/file/some-unique-id`. In the example this would be `some-unique-id`| |branchName|string|false||Name of the branch| -|pageName|string|true||Name of the page the icons can be found| +|pageNames|string[]|true||A list of page names the icons can be found| |ouputPath|string|true||Location which the raw svgs should be saved| |ignoreFrames|array|false||Name of the frame(s) that may be located in the page you wish to ignore. Frames that are prefixed with `_` are automatically ignored. If there is only one frame, please add it in array format e.g ['Frame to Ignore'] From 1ac91ccdf44a1f469ce697347bfec134df7d932a Mon Sep 17 00:00:00 2001 From: Julian Skinner Date: Tue, 27 Aug 2024 12:56:47 -0500 Subject: [PATCH 3/6] refactor(icon-export): make branchName lookup to be case-insensitive --- scripts/figma_icons/figma-icon-export.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/figma_icons/figma-icon-export.ts b/scripts/figma_icons/figma-icon-export.ts index 0322f85..5823c4f 100644 --- a/scripts/figma_icons/figma-icon-export.ts +++ b/scripts/figma_icons/figma-icon-export.ts @@ -345,7 +345,7 @@ const processData = async (rootDir: string, config: FigmaIconConfig, pageName) = let figmaData = await fetchFigmaData(figmaFileId); if ( config.branchName && figmaData.branches.length > 0) { - const branch = figmaData.branches.find(b => b.name === config.branchName) + const branch = figmaData.branches.find(b => b.name.toLowerCase() === config.branchName.toLowerCase()) if (!branch) { log(error('No branch found with the name'), chalk.white.bgRed(config.branchName)); From fed801baf6e70a6b978365a671b27c88b3f8a1d0 Mon Sep 17 00:00:00 2001 From: Julian Skinner Date: Tue, 27 Aug 2024 13:02:36 -0500 Subject: [PATCH 4/6] test: updates specs to include '--color-icon-fill: currentColor;' --- src/components/pds-icon/test/pds-icon.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/pds-icon/test/pds-icon.spec.ts b/src/components/pds-icon/test/pds-icon.spec.ts index f3082f1..531e634 100644 --- a/src/components/pds-icon/test/pds-icon.spec.ts +++ b/src/components/pds-icon/test/pds-icon.spec.ts @@ -8,7 +8,7 @@ describe('pds-icon', () => { html: '', }); expect(root).toEqualHtml(` - +
@@ -22,7 +22,7 @@ describe('pds-icon', () => { html: '', }); expect(root).toEqualHtml(` - +
@@ -36,7 +36,7 @@ describe('pds-icon', () => { html: '', }); expect(root).toEqualHtml(` - +
@@ -50,7 +50,7 @@ describe('pds-icon', () => { html: '', }); expect(root).toEqualHtml(` - +
@@ -79,7 +79,7 @@ describe('pds-icon', () => { }); expect(root).toEqualHtml(` - +
@@ -96,7 +96,7 @@ describe('pds-icon', () => { const icon = page.root; expect(icon).toEqualHtml(` - +
@@ -109,7 +109,7 @@ describe('pds-icon', () => { await page.waitForChanges(); expect(icon).toEqualHtml(` - +
From 1f8127af27510379571246fd28ad8bc20862c4cc Mon Sep 17 00:00:00 2001 From: Julian Skinner Date: Thu, 29 Aug 2024 14:06:16 -0500 Subject: [PATCH 5/6] ci: update the slack message to exclude the date specific changelog --- .github/workflows/slack_payloads/release-info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slack_payloads/release-info.json b/.github/workflows/slack_payloads/release-info.json index 96a4df9..3d5265a 100644 --- a/.github/workflows/slack_payloads/release-info.json +++ b/.github/workflows/slack_payloads/release-info.json @@ -37,7 +37,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "*Commit Message*: ${{ env.COMMIT_MESSAGE }}\n\n *Release*: ${{ env.RELEASE_URL }}\n\n *Changelog*: ${{ env.CHANGELOG_URL }}" + "text": "*Commit Message*: ${{ env.COMMIT_MESSAGE }}\n\n *Release*: ${{ env.RELEASE_URL }}\n\n *Changelog*: https://pine-icons.netlify.app/" } } ] From 9023f1fe33f07ae845262ac2dab07eb712120275 Mon Sep 17 00:00:00 2001 From: Julian Skinner Date: Thu, 19 Sep 2024 16:48:21 -0500 Subject: [PATCH 6/6] chore: change the pageNames to only look at the Icons page --- figma-icon-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/figma-icon-config.json b/figma-icon-config.json index af38310..047c06c 100644 --- a/figma-icon-config.json +++ b/figma-icon-config.json @@ -1,5 +1,5 @@ { - "pageNames": ["Icons", "Filled", "Duotone"], + "pageNames": ["Icons"], "downloadPath": "tmp/svg", "batchSize": 500 }