From aeb0c201af40d262fb6b341e2f51aa970a3de4fb Mon Sep 17 00:00:00 2001 From: Marco Montalbano Date: Wed, 22 Nov 2023 21:59:50 +0100 Subject: [PATCH] chore: clean-up code avoiding duplication --- .figmaexportrc.example.local.ts | 2 +- .../core/src/lib/_mocks_/figma.files.json | 2 +- .../core/src/lib/export-components.test.ts | 55 +++++- packages/core/src/lib/export-components.ts | 24 ++- packages/core/src/lib/export-styles.test.ts | 18 +- packages/core/src/lib/export-styles.ts | 22 ++- packages/core/src/lib/figma.test.ts | 44 ++--- packages/core/src/lib/figma.ts | 168 ++++++++++++++---- .../core/src/lib/figmaStyles/effectStyle.ts | 4 +- .../core/src/lib/figmaStyles/index.test.ts | 27 ++- packages/core/src/lib/figmaStyles/index.ts | 16 +- .../core/src/lib/figmaStyles/paintStyle.ts | 4 +- packages/core/src/lib/utils.test.ts | 36 ++++ packages/core/src/lib/utils.ts | 31 +++- packages/website/.figmaexportrc.js | 2 +- packages/website/src/GitHubLink.jsx | 2 +- packages/website/src/SvgOcticons.jsx | 2 +- .../ComponentsAsSvgr_default.jsx | 6 +- 18 files changed, 340 insertions(+), 125 deletions(-) diff --git a/.figmaexportrc.example.local.ts b/.figmaexportrc.example.local.ts index 0b9c2ca7..d517f3f3 100755 --- a/.figmaexportrc.example.local.ts +++ b/.figmaexportrc.example.local.ts @@ -39,7 +39,7 @@ const styleOptions: StylesCommandOptions = { const componentOptions: ComponentsCommandOptions = { fileId: 'fzYhvQpqwhZDUImRz431Qo', - onlyFromPages: ['icons', 'unit-test', 'octicons-by-github'], + onlyFromPages: ['icons', 'unit-test', 'icons/octicons-by-github'], // concurrency: 30, transformers: [ transformSvgWithSvgo({ diff --git a/packages/core/src/lib/_mocks_/figma.files.json b/packages/core/src/lib/_mocks_/figma.files.json index 7acdb301..638ad1c1 100644 --- a/packages/core/src/lib/_mocks_/figma.files.json +++ b/packages/core/src/lib/_mocks_/figma.files.json @@ -42232,7 +42232,7 @@ }, { "id": "254:0", - "name": "octicons-by-github", + "name": "icons/octicons-by-github", "type": "CANVAS", "children": [ { diff --git a/packages/core/src/lib/export-components.test.ts b/packages/core/src/lib/export-components.test.ts index 564c6e8a..6155c7d3 100644 --- a/packages/core/src/lib/export-components.test.ts +++ b/packages/core/src/lib/export-components.test.ts @@ -88,7 +88,53 @@ describe('export-component', () => { ids: ['10:8', '8:1', '9:1'], svg_include_id: true, }); - expect(clientFile).to.have.been.calledOnceWithExactly('fileABCD', { version: 'versionABCD' }); + + expect(clientFile).to.have.been.calledOnce; + expect(clientFile.firstCall).to.have.been.calledWith('fileABCD', { + version: 'versionABCD', depth: undefined, ids: undefined, + }); + + expect(logger).to.have.been.callCount(6); + expect(logger.getCall(0)).to.have.been.calledWith('fetching document'); + expect(logger.getCall(1)).to.have.been.calledWith('preparing components'); + expect(logger.getCall(2)).to.have.been.calledWith('fetching components 1/3'); + expect(logger.getCall(3)).to.have.been.calledWith('fetching components 2/3'); + expect(logger.getCall(4)).to.have.been.calledWith('fetching components 3/3'); + expect(logger.getCall(5)).to.have.been.calledWith('exported components from fileABCD'); + + expect(transformer).to.have.been.calledThrice; + expect(transformer.firstCall).to.have.been.calledWith(figmaDocument.svg.content); + expect(transformer.secondCall).to.have.been.calledWith(figmaDocument.svg.content); + expect(transformer.thirdCall).to.have.been.calledWith(figmaDocument.svg.content); + + expect(outputter).to.have.been.calledOnceWithExactly(pagesWithSvg); + }); + + it('should filter by selected page names when setting onlyFromPages', async () => { + const pagesWithSvg = await exportComponents({ + fileId: 'fileABCD', + version: 'versionABCD', + token: 'token1234', + log: logger, + outputters: [outputter], + transformers: [transformer], + onlyFromPages: ['page2'], + }); + + nockScope.done(); + + expect(FigmaExport.getClient).to.have.been.calledOnceWithExactly('token1234'); + expect(clientFileImages).to.have.been.calledOnceWith('fileABCD', { + format: 'svg', + ids: ['10:8', '8:1', '9:1'], + svg_include_id: true, + }); + + expect(clientFile).to.have.been.calledTwice; + expect(clientFile.firstCall).to.have.been.calledWith('fileABCD', { version: 'versionABCD', depth: 1, ids: undefined }); + expect(clientFile.secondCall).to.have.been.calledWith('fileABCD', { + version: 'versionABCD', depth: undefined, ids: ['10:7'], + }); expect(logger).to.have.been.callCount(6); expect(logger.getCall(0)).to.have.been.calledWith('fetching document'); @@ -147,7 +193,12 @@ describe('export-component', () => { ids: ['10:8'], svg_include_id: true, }); - expect(clientFile).to.have.been.calledOnceWithExactly('fileABCD', { version: 'versionABCD' }); + + expect(clientFile).to.have.been.calledOnce; + expect(clientFile.firstCall).to.have.been.calledWith( + 'fileABCD', + { version: 'versionABCD', depth: undefined, ids: undefined }, + ); expect(logger).to.have.been.callCount(4); expect(logger.getCall(0)).to.have.been.calledWith('fetching document'); diff --git a/packages/core/src/lib/export-components.ts b/packages/core/src/lib/export-components.ts index 7b1f2581..31e12955 100644 --- a/packages/core/src/lib/export-components.ts +++ b/packages/core/src/lib/export-components.ts @@ -1,6 +1,11 @@ import * as FigmaExport from '@figma-export/types'; -import { getClient, getPagesWithComponents, enrichPagesWithSvg } from './figma'; +import { + getClient, + enrichPagesWithSvg, + getDocument, + getPagesWithComponents, +} from './figma'; export const components: FigmaExport.ComponentsCommand = async ({ token, @@ -20,15 +25,16 @@ export const components: FigmaExport.ComponentsCommand = async ({ const client = getClient(token); log('fetching document'); - const { data: { document = null } = {} } = await client.file(fileId, { version }).catch((error: Error) => { - throw new Error(`while fetching file "${fileId}${version ? `?version=${version}` : ''}": ${error.message}`); - }); - - if (!document) { - throw new Error('\'document\' is missing.'); - } + const figmaDocument = await getDocument( + client, + { + fileId, + version, + onlyFromPages, + }, + ); - const pages = getPagesWithComponents(document, { only: onlyFromPages, filter: filterComponent }); + const pages = getPagesWithComponents(figmaDocument, { filterComponent }); log('preparing components'); const pagesWithSvg = await enrichPagesWithSvg(client, fileId, pages, { diff --git a/packages/core/src/lib/export-styles.test.ts b/packages/core/src/lib/export-styles.test.ts index 95dcd15e..30519725 100644 --- a/packages/core/src/lib/export-styles.test.ts +++ b/packages/core/src/lib/export-styles.test.ts @@ -47,7 +47,7 @@ describe('export-styles', () => { sinon.restore(); }); - it('should use outputter to export styles', async () => { + it('should use outputter to export styles without defining the "onlyFromPages" option', async () => { const pagesWithSvg = await exportStyles({ fileId: 'fileABCD', version: 'versionABCD', @@ -58,8 +58,8 @@ describe('export-styles', () => { expect(FigmaExport.getClient).to.have.been.calledOnceWithExactly('token1234'); expect(clientFileNodes).to.have.been.calledOnceWith('fileABCD', { ids: fileNodeIds, version: 'versionABCD' }); - expect(clientFile.firstCall).to.have.been.calledWith('fileABCD', { version: 'versionABCD', depth: 1 }); - expect(clientFile.secondCall).to.have.been.calledWith('fileABCD', { version: 'versionABCD', ids: undefined }); + expect(clientFile).to.have.been.calledOnce; + expect(clientFile.firstCall).to.have.been.calledWith('fileABCD', { version: 'versionABCD', depth: undefined, ids: undefined }); expect(logger).to.have.been.callCount(4); expect(logger.getCall(0)).to.have.been.calledWith('fetching document'); @@ -74,7 +74,7 @@ describe('export-styles', () => { const pagesWithSvg = await exportStyles({ fileId: 'fileABCD', version: 'versionABCD', - onlyFromPages: ['octicons-by-github'], + onlyFromPages: ['icons/octicons-by-github'], token: 'token1234', log: logger, outputters: [outputter], @@ -82,8 +82,9 @@ describe('export-styles', () => { expect(FigmaExport.getClient).to.have.been.calledOnceWithExactly('token1234'); expect(clientFileNodes).to.have.been.calledOnceWith('fileABCD', { ids: fileNodeIds, version: 'versionABCD' }); - expect(clientFile.firstCall).to.have.been.calledWith('fileABCD', { version: 'versionABCD', depth: 1 }); - expect(clientFile.secondCall).to.have.been.calledWith('fileABCD', { version: 'versionABCD', ids: ['254:0'] }); + expect(clientFile).to.have.been.calledTwice; + expect(clientFile.firstCall).to.have.been.calledWith('fileABCD', { version: 'versionABCD', depth: 1, ids: undefined }); + expect(clientFile.secondCall).to.have.been.calledWith('fileABCD', { version: 'versionABCD', depth: undefined, ids: ['254:0'] }); expect(logger).to.have.been.callCount(4); expect(logger.getCall(0)).to.have.been.calledWith('fetching document'); @@ -118,8 +119,7 @@ describe('export-styles', () => { }); it('should throw an error when fetching styles fails', async () => { - clientFile.onFirstCall().returns(Promise.resolve({ data: { document: file.document } })); - clientFile.onSecondCall().returns(Promise.reject(new Error('some error'))); + clientFile.onFirstCall().returns(Promise.reject(new Error('some error'))); await expect(exportStyles({ fileId: 'fileABCD', @@ -133,7 +133,7 @@ describe('export-styles', () => { await expect(exportStyles({ fileId: 'fileABCD', token: 'token1234', - })).to.be.rejectedWith(Error, '\'document\' is missing.'); + })).to.be.rejectedWith(Error, '\'styles\' are missing.'); }); it('should throw an error if styles property is missing when fetching file', async () => { diff --git a/packages/core/src/lib/export-styles.ts b/packages/core/src/lib/export-styles.ts index ba222760..014eb797 100644 --- a/packages/core/src/lib/export-styles.ts +++ b/packages/core/src/lib/export-styles.ts @@ -1,6 +1,6 @@ import * as FigmaExport from '@figma-export/types'; -import { getPages, getClient } from './figma'; +import { getClient, getStyles } from './figma'; import { fetchStyles, parseStyles } from './figmaStyles'; export const styles: FigmaExport.StylesCommand = async ({ @@ -17,19 +17,17 @@ export const styles: FigmaExport.StylesCommand = async ({ const client = getClient(token); log('fetching document'); - const { data: { document = null } = {} } = await client.file(fileId, { version, depth: 1 }).catch((error: Error) => { - throw new Error(`while fetching file "${fileId}${version ? `?version=${version}` : ''}": ${error.message}`); - }); - - if (!document) { - throw new Error('\'document\' is missing.'); - } - - const ids = getPages(document, onlyFromPages) - .map((page) => page.id); + const figmaStyles = await getStyles( + client, + { + fileId, + version, + onlyFromPages, + }, + ); log('fetching styles'); - const styleNodes = await fetchStyles(client, fileId, version, onlyFromPages.length > 0 ? ids : undefined); + const styleNodes = await fetchStyles(client, fileId, figmaStyles, version); log('parsing styles'); const parsedStyles = parseStyles(styleNodes); diff --git a/packages/core/src/lib/figma.test.ts b/packages/core/src/lib/figma.test.ts index 9b126454..d3261a03 100644 --- a/packages/core/src/lib/figma.test.ts +++ b/packages/core/src/lib/figma.test.ts @@ -18,6 +18,17 @@ describe('figma.', () => { sinon.restore(); }); + describe('', () => { + it('should throw an error if styles are not present', async () => { + const client = { + ...({} as Figma.ClientInterface), + file: sinon.stub().resolves({ data: {} }), + }; + + await expect(figma.getStyles(client, { fileId: 'ABC123' })).to.be.rejectedWith(Error, '\'styles\' are missing.'); + }); + }); + describe('getComponents', () => { it('should get zero results if no children are provided', () => { expect(figma.getComponents()).to.eql([]); @@ -34,7 +45,7 @@ describe('figma.', () => { }); }); - describe('getPages', () => { + describe('getPagesWithComponents', () => { const document = figmaDocument.createDocument({ children: [figmaDocument.page1, figmaDocument.page2] }); it('should get all pages by default', () => { @@ -43,33 +54,16 @@ describe('figma.', () => { .to.contain.an.item.with.property('name', 'page2'); }); - it('should get all pages if "empty" list is provided', () => { - expect(figma.getPagesWithComponents(document, { only: [''] })) - .to.contain.an.item.with.property('name', 'page1') - .to.contain.an.item.with.property('name', 'page2'); - - expect(figma.getPagesWithComponents(document, { only: [] })) - .to.contain.an.item.with.property('name', 'page1') - .to.contain.an.item.with.property('name', 'page2'); - - expect(figma.getPagesWithComponents(document, { only: '' })) + it('should get all the pages from the document', () => { + expect(figma.getPagesWithComponents(document)) .to.contain.an.item.with.property('name', 'page1') .to.contain.an.item.with.property('name', 'page2'); }); - it('should get all requested pages', () => { - expect(figma.getPagesWithComponents(document, { only: 'page2' })) + it('should be able to filter components', () => { + expect(figma.getPagesWithComponents(document, { filterComponent: (component) => ['9:1'].includes(component.id) })) .to.not.contain.an.item.with.property('name', 'page1') .to.contain.an.item.with.property('name', 'page2'); - - expect(figma.getPagesWithComponents(document, { only: ['page1', 'page2'] })) - .to.contain.an.item.with.property('name', 'page1') - .to.contain.an.item.with.property('name', 'page2'); - }); - - it('should get zero results if a non existing page is provided', () => { - expect(figma.getPagesWithComponents(document, { only: 'page20' })) - .to.be.an('array').that.is.empty; }); it('should excludes pages without components', () => { @@ -92,11 +86,9 @@ describe('figma.', () => { describe('getIdsFromPages', () => { it('should get component ids from specified pages', () => { const document = figmaDocument.createDocument({ children: [figmaDocument.page1, figmaDocument.page2] }); - const pages = figma.getPagesWithComponents(document, { - only: 'page2', - }); + const pages = figma.getPagesWithComponents(document); - expect(figma.getIdsFromPages(pages)).to.eql(['9:1']); + expect(figma.getIdsFromPages(pages)).to.eql(['10:8', '8:1', '9:1']); }); }); diff --git a/packages/core/src/lib/figma.ts b/packages/core/src/lib/figma.ts index 2949d1ab..45b2d294 100644 --- a/packages/core/src/lib/figma.ts +++ b/packages/core/src/lib/figma.ts @@ -6,14 +6,97 @@ import pRetry from 'p-retry'; import * as FigmaExport from '@figma-export/types'; import { - toArray, fetchAsSvgXml, promiseSequentially, fromEntries, chunk, emptySvg, + PickOption, + sanitizeOnlyFromPages, } from './utils'; +/** + * Create a new Figma client. + */ +export const getClient = (token: string): Figma.ClientInterface => { + if (!token) { + throw new Error('\'Access Token\' is missing. https://www.figma.com/developers/docs#authentication'); + } + + return Figma.Client({ personalAccessToken: token }); +}; + +/** + * Get the Figma document and styles from a `fileId` and a `version`. + */ +const getFile = async ( + client: Figma.ClientInterface, + options: PickOption, + params: { + depth?: number; + ids?: string[]; + }, +): Promise<{ + document: Figma.Document | null; + styles: { readonly [key: string]: Figma.Style } | null +}> => { + const { data: { document = null, styles = null } = {} } = await client.file( + options.fileId, + { + version: options.version, + depth: params.depth, + ids: params.ids, + }, + ) + .catch((error: Error) => { + throw new Error( + `while fetching file "${options.fileId}${options.version ? `?version=${options.version}` : ''}": ${error.message}`, + ); + }); + + return { document, styles }; +}; + +/** + * Get all the pages (`Figma.Canvas`) from a document filtered by `onlyFromPages` (when set). + * When `onlyFromPages` is not set it returns all the pages. + */ +const getPagesFromDocument = ( + document: Figma.Document, + options: PickOption = {}, +): Figma.Canvas[] => { + const onlyFromPages = sanitizeOnlyFromPages(options.onlyFromPages); + return document.children + .filter((node): node is Figma.Canvas => { + return node.type === 'CANVAS' && (onlyFromPages.length === 0 || onlyFromPages.includes(node.name)); + }); +}; + +/** + * Get all the page ids filtered by `onlyFromPages`. When `onlyFromPages` is not set it returns all page ids. + * + * This method is particularly fast because it looks to a Figma file with `depth=1`. + */ +const getAllPageIds = async ( + client: Figma.ClientInterface, + options: PickOption, +): Promise => { + const { document } = await getFile(client, options, { depth: 1 }); + + if (!document) { + throw new Error('\'document\' is missing.'); + } + + const pageIds = getPagesFromDocument(document, options) + .map((page) => page.id); + + if (pageIds.length === 0) { + throw new Error(`Cannot find any page with "onlyForPages" equal to [${sanitizeOnlyFromPages(options.onlyFromPages).join(', ')}].`); + } + + return pageIds; +}; + export const getComponents = ( children: readonly Figma.Node[] = [], filter: FigmaExport.ComponentFilter = () => true, @@ -51,33 +134,50 @@ export const getComponents = ( return components; }; -/** Check whether the given string is not empty. */ -function isNotEmpty(input: string): boolean { - return input.trim() !== ''; -} +export const getDocument = async ( + client: Figma.ClientInterface, + options: PickOption, +): Promise => { + const { document } = await getFile( + client, + options, + { + // when `onlyFromPages` is set, we avoid traversing all the document tree, but instead we get only requested ids. + ids: sanitizeOnlyFromPages(options.onlyFromPages).length > 0 + ? await getAllPageIds(client, options) + : undefined, + }, + ); -export const getPages = (document: Figma.Document, pageNames: string | string[] = []): Figma.Canvas[] => { - const only = toArray(pageNames).filter(isNotEmpty); - return document.children - .filter((node): node is Figma.Canvas => { - return node.type === 'CANVAS' && (only.length === 0 || only.includes(node.name)); - }); + if (!document) { + throw new Error('\'document\' is missing.'); + } + + return document; }; -type GetPagesOptions = { - only?: string | string[]; - filter?: FigmaExport.ComponentFilter; -} +export const getStyles = async ( + client: Figma.ClientInterface, + options: PickOption, +): Promise<{ + readonly [key: string]: Figma.Style +}> => { + const { styles } = await getFile( + client, + options, + { + // when `onlyFromPages` is set, we avoid traversing all the document tree, but instead we get only requested ids. + ids: sanitizeOnlyFromPages(options.onlyFromPages).length > 0 + ? await getAllPageIds(client, options) + : undefined, + }, + ); -export const getPagesWithComponents = (document: Figma.Document, options: GetPagesOptions = {}): FigmaExport.PageNode[] => { - const pages = getPages(document, options.only); + if (!styles) { + throw new Error('\'styles\' are missing.'); + } - return pages - .map((page) => ({ - ...page, - components: getComponents(page.children as readonly FigmaExport.ComponentNode[], options.filter), - })) - .filter((page) => page.components.length > 0); + return styles; }; export const getIdsFromPages = (pages: FigmaExport.PageNode[]): string[] => pages.reduce((ids: string[], page) => [ @@ -85,14 +185,6 @@ export const getIdsFromPages = (pages: FigmaExport.PageNode[]): string[] => page ...page.components.map((component) => component.id), ], []); -export const getClient = (token: string): Figma.ClientInterface => { - if (!token) { - throw new Error('\'Access Token\' is missing. https://www.figma.com/developers/docs#authentication'); - } - - return Figma.Client({ personalAccessToken: token }); -}; - const fileImages = async (client: Figma.ClientInterface, fileId: string, ids: string[]) => { const response = await client.fileImages(fileId, { ids, @@ -161,6 +253,20 @@ export const fileSvgs = async ( return fromEntries(svgs); }; +export const getPagesWithComponents = ( + document: Figma.Document, + options: PickOption = {}, +): FigmaExport.PageNode[] => { + const pages = getPagesFromDocument(document); + + return pages + .map((page) => ({ + ...page, + components: getComponents(page.children as readonly FigmaExport.ComponentNode[], options.filterComponent), + })) + .filter((page) => page.components.length > 0); +}; + export const enrichPagesWithSvg = async ( client: Figma.ClientInterface, fileId: string, diff --git a/packages/core/src/lib/figmaStyles/effectStyle.ts b/packages/core/src/lib/figmaStyles/effectStyle.ts index 2efa72a9..cf2ca84d 100644 --- a/packages/core/src/lib/figmaStyles/effectStyle.ts +++ b/packages/core/src/lib/figmaStyles/effectStyle.ts @@ -1,7 +1,7 @@ import * as Figma from 'figma-js'; import * as FigmaExport from '@figma-export/types'; -import { notEmpty } from '../utils'; +import { notNullish } from '../utils'; import { extractColor } from './paintStyle'; const createEffectStyle = (effect: Figma.Effect): FigmaExport.EffectStyle | undefined => { @@ -51,7 +51,7 @@ const parse = (node: FigmaExport.StyleNode): FigmaExport.StyleTypeEffect | undef effects: Array.from(node.effects) .reverse() .map(createEffectStyle) - .filter(notEmpty), + .filter(notNullish), }; } diff --git a/packages/core/src/lib/figmaStyles/index.test.ts b/packages/core/src/lib/figmaStyles/index.test.ts index f31d0d8c..d88c3ccb 100644 --- a/packages/core/src/lib/figmaStyles/index.test.ts +++ b/packages/core/src/lib/figmaStyles/index.test.ts @@ -8,6 +8,7 @@ import * as Figma from 'figma-js'; import * as FigmaExport from '@figma-export/types'; import * as figmaStyles from './index'; +import * as figma from '../figma'; import fileJson from '../_mocks_/figma.files.json'; import fileNodesJson from '../_mocks_/figma.fileNodes.json'; @@ -29,15 +30,6 @@ const getNode = (styleNodes: FigmaExport.StyleNode[], name: string): FigmaExport describe('figmaStyles.', () => { describe('fetch', () => { - it('should throw an error if styles are not present', async () => { - const client = { - ...({} as Figma.ClientInterface), - file: sinon.stub().resolves({ data: {} }), - }; - - await expect(figmaStyles.fetchStyles(client, 'ABC123')).to.be.rejectedWith(Error, '\'styles\' are missing.'); - }); - it('should fetch style from a specified Figma fileId', async () => { const client = { ...({} as Figma.ClientInterface), @@ -63,9 +55,12 @@ describe('figmaStyles.', () => { }), }; - const styleNodes = await figmaStyles.fetchStyles(client, 'ABC123', 'version123'); + const styles = await figma.getStyles(client, { fileId: 'ABC123', version: 'version123' }); + const styleNodes = await figmaStyles.fetchStyles(client, 'ABC123', styles, 'version123'); + + expect(client.file).to.have.been.calledOnce; + expect(client.file.firstCall).to.have.been.calledWith('ABC123', { version: 'version123', depth: undefined, ids: undefined }); - expect(client.file).to.have.been.calledOnceWith('ABC123', { version: 'version123', ids: undefined }); expect(client.fileNodes).to.have.been.calledWith('ABC123', { ids: ['121:10', '131:20'], version: 'version123' }); expect(styleNodes.length).to.equal(2); @@ -82,9 +77,12 @@ describe('figmaStyles.', () => { fileNodes: sinon.stub().resolves({ data: fileNodes }), }; - const styleNodes = await figmaStyles.fetchStyles(client, 'ABC123', 'version123', ['121:10']); + const styles = await figma.getStyles(client, { fileId: 'ABC123', version: 'version123' }); + expect(client.file).to.have.been.calledOnce; + expect(client.file).to.have.been.calledOnceWith('ABC123', { version: 'version123', depth: undefined, ids: undefined }); + + const styleNodes = await figmaStyles.fetchStyles(client, 'ABC123', styles, 'version123'); - expect(client.file).to.have.been.calledOnceWith('ABC123', { version: 'version123', ids: ['121:10'] }); expect(client.fileNodes).to.have.been.calledWith('ABC123', { ids: nodeIds, version: 'version123' }); const expectedStyleNodesLength = 30; @@ -108,7 +106,8 @@ describe('figmaStyles.', () => { fileNodes: sinon.stub().resolves({ data: fileNodes }), }; - styleNodes = await figmaStyles.fetchStyles(client, 'ABC123'); + const styles = await figma.getStyles(client, { fileId: 'ABC1234' }); + styleNodes = await figmaStyles.fetchStyles(client, 'ABC123', styles); }); describe('Color Styles', () => { diff --git a/packages/core/src/lib/figmaStyles/index.ts b/packages/core/src/lib/figmaStyles/index.ts index e9504209..0ff5692b 100644 --- a/packages/core/src/lib/figmaStyles/index.ts +++ b/packages/core/src/lib/figmaStyles/index.ts @@ -1,7 +1,7 @@ import * as Figma from 'figma-js'; import * as FigmaExport from '@figma-export/types'; -import { notEmpty } from '../utils'; +import { notNullish } from '../utils'; import { parse as parsePaintStyle } from './paintStyle'; import { parse as parseEffectStyle } from './effectStyle'; @@ -11,18 +11,16 @@ import { parse as parseTextStyle } from './textStyle'; const fetchStyles = async ( client: Figma.ClientInterface, fileId: string, + styles: { readonly [key: string]: Figma.Style }, version?: string, - ids?: string[], ): Promise => { - const { data: { styles = null } = {} } = await client.file(fileId, { version, ids }).catch((error: Error) => { - throw new Error(`while fetching file "${fileId}${version ? `?version=${version}` : ''}": ${error.message}`); - }); + const styleIds = Object.keys(styles); - if (!styles) { - throw new Error('\'styles\' are missing.'); + if (styleIds.length === 0) { + throw new Error('No styles found'); } - const { data: { nodes } } = await client.fileNodes(fileId, { ids: Object.keys(styles), version }).catch((error: Error) => { + const { data: { nodes } } = await client.fileNodes(fileId, { ids: styleIds, version }).catch((error: Error) => { throw new Error(`while fetching fileNodes: ${error.message}`); }); @@ -53,7 +51,7 @@ const parseStyles = (styleNodes: FigmaExport.StyleNode[]): FigmaExport.Style[] = originalNode: node, ...parsedStyles, }; - }).filter(notEmpty); + }).filter(notNullish); }; export { diff --git a/packages/core/src/lib/figmaStyles/paintStyle.ts b/packages/core/src/lib/figmaStyles/paintStyle.ts index fc5bca18..998ffb90 100644 --- a/packages/core/src/lib/figmaStyles/paintStyle.ts +++ b/packages/core/src/lib/figmaStyles/paintStyle.ts @@ -1,7 +1,7 @@ import * as Figma from 'figma-js'; import * as FigmaExport from '@figma-export/types'; -import { notEmpty } from '../utils'; +import { notNullish } from '../utils'; const extractColor = ({ color, opacity = 1 }: FigmaExport.ExtractableColor): (FigmaExport.Color | undefined) => { if (!color) { @@ -106,7 +106,7 @@ const parse = (node: FigmaExport.StyleNode): FigmaExport.StyleTypeFill | undefin fills: Array.from(node.fills) .reverse() .map(createFillStyles) - .filter(notEmpty), + .filter(notNullish), }; } diff --git a/packages/core/src/lib/utils.test.ts b/packages/core/src/lib/utils.test.ts index afd4d4aa..352d7b5a 100644 --- a/packages/core/src/lib/utils.test.ts +++ b/packages/core/src/lib/utils.test.ts @@ -6,6 +6,8 @@ import * as utils from './utils'; describe('utils.', () => { describe('toArray', () => { it('should convert the element into an array if the element is not an array. If is already an array, just returns it', () => { + expect(utils.toArray(undefined)).to.eql([undefined]); + expect(utils.toArray(null)).to.eql([null]); expect(utils.toArray('')).to.eql(['']); expect(utils.toArray('this is a string')).to.eql(['this is a string']); expect(utils.toArray(2)).to.eql([2]); @@ -74,4 +76,38 @@ describe('utils.', () => { ).to.deep.equal([[10], [20], [30]]); }); }); + + describe('notNullish', () => { + it('should return `false` when provided value is nullish', () => { + expect(utils.notNullish(null)).to.been.false; + expect(utils.notNullish(undefined)).to.been.false; + expect(utils.notNullish('John')).to.been.true; + expect(utils.notNullish(23)).to.been.true; + + expect( + [23, null, null, 'John', undefined].filter(utils.notNullish), + ).to.deep.equal([23, 'John']); + }); + }); + + describe('notEmptyString', () => { + it('should return `false` when provided value is an empty string', () => { + expect(utils.notEmptyString('')).to.been.false; + expect(utils.notEmptyString(' ')).to.been.false; + expect(utils.notEmptyString('John')).to.been.true; + + expect( + ['', 'John', ' '].filter(utils.notEmptyString), + ).to.deep.equal(['John']); + }); + }); + + describe('sanitizeOnlyFromPages', () => { + it('should return a not nullish and not empty string array', () => { + expect(utils.sanitizeOnlyFromPages(undefined)).to.deep.equal([]); + expect(utils.sanitizeOnlyFromPages([''])).to.deep.equal([]); + expect(utils.sanitizeOnlyFromPages(['John'])).to.deep.equal(['John']); + expect(utils.sanitizeOnlyFromPages(['John', 'Doe'])).to.deep.equal(['John', 'Doe']); + }); + }); }); diff --git a/packages/core/src/lib/utils.ts b/packages/core/src/lib/utils.ts index c062f23f..a0bfeb22 100644 --- a/packages/core/src/lib/utils.ts +++ b/packages/core/src/lib/utils.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import * as FigmaExport from '@figma-export/types'; export const toArray = (any: T | T[]): T[] => (Array.isArray(any) ? any : [any]); @@ -50,6 +51,34 @@ export const fetchAsSvgXml = (url: string): Promise => { }); }; -export const notEmpty = (value: TValue | null | undefined): value is TValue => { +/** + * Check whether the provided value is `undefined` or `null`. It this case it will return `false`. + * + * Useful when you need to exclude nullish values from a list. + * @example [23, null, null, 'John', undefined].filter(notNullish) //= [23, 'John'] + */ +export const notNullish = (value: T | null | undefined): value is T => { return value !== null && value !== undefined; }; + +/** + * Check whether the given string is not empty. + * + * Useful when you need to exclude empty strings from a list. + * @example ['', 'John'].filter(notEmptyString) //= ['John'] + */ +export function notEmptyString(value: string): boolean { + return value.trim() !== ''; +} + +export type PickOption[0]> = + Pick[0], K> + +/** + * Sanitize `onlyFromPages` option by converting to a not nullish and not empty string array. + */ +export function sanitizeOnlyFromPages( + onlyFromPages: PickOption['onlyFromPages'], +) { + return (onlyFromPages ?? []).filter((v) => notNullish(v) && notEmptyString(v)); +} diff --git a/packages/website/.figmaexportrc.js b/packages/website/.figmaexportrc.js index 188bdeeb..5eb76f76 100644 --- a/packages/website/.figmaexportrc.js +++ b/packages/website/.figmaexportrc.js @@ -14,7 +14,7 @@ module.exports = { ['components', { fileId: 'fzYhvQpqwhZDUImRz431Qo', - onlyFromPages: ['octicons-by-github'], + onlyFromPages: ['icons/octicons-by-github'], outputters: [ require('../output-components-as-es6')({ output: './output/es6-dataurl-octicons', diff --git a/packages/website/src/GitHubLink.jsx b/packages/website/src/GitHubLink.jsx index d97f0746..c79bf919 100644 --- a/packages/website/src/GitHubLink.jsx +++ b/packages/website/src/GitHubLink.jsx @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-unresolved, import/extensions -import { iconMarkGithub } from '../output/es6-dataurl-octicons/octicons-by-github'; +import { iconMarkGithub } from '../output/es6-dataurl-octicons/icons/octicons-by-github'; const GitHubLink = () => (
diff --git a/packages/website/src/SvgOcticons.jsx b/packages/website/src/SvgOcticons.jsx index 9e038906..b8980b25 100644 --- a/packages/website/src/SvgOcticons.jsx +++ b/packages/website/src/SvgOcticons.jsx @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-unresolved, import/extensions -import * as Octicons from '../output/es6-dataurl-octicons/octicons-by-github'; +import * as Octicons from '../output/es6-dataurl-octicons/icons/octicons-by-github'; // eslint-disable-next-line import/no-unresolved, import/extensions import { figmaArrow } from '../output/es6-dataurl/icons'; diff --git a/packages/website/src/output-components/ComponentsAsSvgr_default.jsx b/packages/website/src/output-components/ComponentsAsSvgr_default.jsx index 0cf1e8ee..1b30ce99 100644 --- a/packages/website/src/output-components/ComponentsAsSvgr_default.jsx +++ b/packages/website/src/output-components/ComponentsAsSvgr_default.jsx @@ -4,7 +4,7 @@ import CodeBlock from '../components/CodeBlock'; import * as FigmaIcons from '../../output/svgr/icons'; // eslint-disable-next-line import/no-unresolved, import/extensions -import { Squirrel } from '../../output/svgr-octicons/octicons-by-github'; +import { Squirrel } from '../../output/svgr-octicons/icons/octicons-by-github'; const props = { title: ( @@ -16,7 +16,7 @@ const props = { <> You can easily import the generated .jsx files into your project and start using your Figma components as React components.
- import {'{ Squirrel }'} from './output/octicons-by-github'; + import {'{ Squirrel }'} from './output/icons/octicons-by-github'; ), code: ` @@ -24,7 +24,7 @@ const props = { commands: [ ['components', { fileId: 'fzYhvQpqwhZDUImRz431Qo', - onlyFromPages: ['octicons-by-github'], + onlyFromPages: ['icons/octicons-by-github'], outputters: [ // https://www.npmjs.com/package/@figma-export/output-components-as-svgr require('@figma-export/output-components-as-svgr')({