• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marcomontalbano / figma-export / 8040637088

25 Feb 2024 08:41PM CUT coverage: 96.473%. Remained the same
8040637088

push

github

web-flow
Merge pull request #162 from marcomontalbano/add-overrides-for-axios

Add `overrides` for axios

236 of 258 branches covered (91.47%)

Branch coverage included in aggregate %.

612 of 621 relevant lines covered (98.55%)

17.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.79
/packages/core/src/lib/figma.ts
1
import * as Figma from 'figma-js';
1✔
2

3
import { basename, dirname } from 'path';
1✔
4
import pLimit from 'p-limit';
1✔
5
import pRetry from 'p-retry';
1✔
6
import * as FigmaExport from '@figma-export/types';
7

8
import {
1✔
9
    fetchAsSvgXml,
10
    promiseSequentially,
11
    fromEntries,
12
    chunk,
13
    emptySvg,
14
    PickOption,
15
    forceArray,
16
} from './utils';
17

18
/**
19
 * Create a new Figma client.
20
 */
21
export const getClient = (token: string): Figma.ClientInterface => {
1✔
22
    if (!token) {
2✔
23
        throw new Error('\'Access Token\' is missing. https://www.figma.com/developers/docs#authentication');
1✔
24
    }
25

26
    return Figma.Client({ personalAccessToken: token });
1✔
27
};
28

29
/**
30
 * Get the Figma document and styles from a `fileId` and a `version`.
31
 */
32
const getFile = async (
1✔
33
    client: Figma.ClientInterface,
34
    options: PickOption<FigmaExport.StylesCommand | FigmaExport.StylesCommand, 'fileId' | 'version'>,
35
    params: {
36
        depth?: number;
37
        ids?: string[];
38
    },
39
): Promise<{
40
    document: Figma.Document | null;
41
    styles: { readonly [key: string]: Figma.Style } | null
42
}> => {
37✔
43
    const { data: { document = null, styles = null } = {} } = await client.file(
37✔
44
        options.fileId,
45
        {
46
            version: options.version,
47
            depth: params.depth,
48
            ids: params.ids,
49
        },
50
    )
51
        .catch((error: Error) => {
52
            throw new Error(
3✔
53
                `while fetching file "${options.fileId}${options.version ? `?version=${options.version}` : ''}": ${error.message}`,
3!
54
            );
55
        });
56

57
    return { document, styles };
34✔
58
};
59

60
/**
61
 * Get all the pages (`Figma.Canvas`) from a document filtered by `onlyFromPages` (when set).
62
 * When `onlyFromPages` is not set it returns all the pages.
63
 */
64
const getPagesFromDocument = (
1✔
65
    document: Figma.Document,
66
    options: PickOption<FigmaExport.StylesCommand | FigmaExport.StylesCommand, 'onlyFromPages'> = {},
32✔
67
): Figma.Canvas[] => {
68
    const onlyFromPages = forceArray(options.onlyFromPages);
36✔
69
    return document.children
36✔
70
        .filter((node): node is Figma.Canvas => {
71
            return node.type === 'CANVAS' && (
56✔
72
                onlyFromPages.length === 0 || onlyFromPages.includes(node.name) || onlyFromPages.includes(node.id));
73
        });
74
};
75

76
/**
77
 * Get all the page ids filtered by `onlyFromPages`. When `onlyFromPages` is not set it returns all page ids.
78
 *
79
 * This method is particularly fast because it looks to a Figma file with `depth=1`.
80
 */
81
const getAllPageIds = async (
1✔
82
    client: Figma.ClientInterface,
83
    options: PickOption<FigmaExport.StylesCommand | FigmaExport.StylesCommand, 'fileId' | 'version' | 'onlyFromPages'>,
84
): Promise<string[]> => {
4✔
85
    const { document } = await getFile(client, options, { depth: 1 });
4✔
86

87
    if (!document) {
4!
88
        throw new Error('\'document\' is missing.');
×
89
    }
90

91
    const pageIds = getPagesFromDocument(document, options)
4✔
92
        .map((page) => page.id);
3✔
93

94
    if (pageIds.length === 0) {
4✔
95
        const errorAsString = forceArray(options.onlyFromPages)
1✔
96
            .map((page) => `"${page}"`)
1✔
97
            .join(', ');
98

99
        throw new Error(`Cannot find any page with "onlyForPages" equal to [${errorAsString}].`);
1✔
100
    }
101

102
    return pageIds;
3✔
103
};
104

105
/**
106
 * Determines whether the `searchElement.type` is included in the `availableTypes` list, returning true or false as appropriate.
107
 * @param availableTypes List of available node types.
108
 * @param searchNode The node to search for.
109
 */
110
function isNodeOfType<
111
    AvailableTypes extends Figma.Node['type'][],
112
    SearchNode extends Figma.Node,
113
>(availableTypes: AvailableTypes, searchNode: SearchNode): searchNode is Extract<SearchNode, { type: AvailableTypes[number] }> {
114
    return availableTypes.includes(searchNode.type);
101✔
115
}
116

117
export const getComponents = (
1✔
118
    children: readonly Figma.Node[],
119
    { filterComponent, includeTypes }:
120
        Required<PickOption<FigmaExport.ComponentsCommand, 'filterComponent' | 'includeTypes'>>,
121
    pathToComponent: FigmaExport.ComponentExtras['pathToComponent'] = [],
45✔
122
): FigmaExport.ComponentNode[] => {
123
    let components: FigmaExport.ComponentNode[] = [];
62✔
124

125
    children.forEach((node) => {
62✔
126
        if (isNodeOfType(includeTypes, node) && filterComponent(node)) {
101✔
127
            components.push({
69✔
128
                ...node,
129
                svg: '',
130
                figmaExport: {
131
                    id: node.id,
132
                    dirname: dirname(node.name),
133
                    basename: basename(node.name),
134
                    pathToComponent,
135
                },
136
            });
137
            return;
69✔
138
        }
139

140
        if ('children' in node) {
32✔
141
            components = [
14✔
142
                ...components,
143
                ...getComponents(
144
                    (node.children),
145
                    { filterComponent, includeTypes },
146
                    [...pathToComponent, { name: node.name, type: node.type }],
147
                ),
148
            ];
149
        }
150
    });
151

152
    return components;
62✔
153
};
154

155
// eslint-disable-next-line no-underscore-dangle
156
const __getDocumentAndStyles = async (
1✔
157
    client: Figma.ClientInterface,
158
    options: PickOption<FigmaExport.ComponentsCommand, 'fileId' | 'version' | 'ids' | 'onlyFromPages'>,
159
): ReturnType<typeof getFile> => {
34✔
160
    return getFile(
34✔
161
        client,
162
        options,
163
        {
164
            // when `onlyFromPages` is set, we avoid traversing all the document tree, but instead we get only requested ids.
165
            // eslint-disable-next-line no-nested-ternary
166
            ids: forceArray(options.ids).length > 0
34✔
167
                ? options.ids
168
                : forceArray(options.onlyFromPages).length > 0
33✔
169
                    ? await getAllPageIds(client, options)
170
                    : undefined,
171
        },
172
    );
173
};
174

175
export const getDocument = async (
1✔
176
    client: Figma.ClientInterface,
177
    options: PickOption<FigmaExport.ComponentsCommand, 'fileId' | 'version' | 'ids' | 'onlyFromPages'>,
178
): Promise<Figma.Document> => {
11✔
179
    const { document } = await __getDocumentAndStyles(client, options);
11✔
180

181
    if (document == null) {
9✔
182
        throw new Error('\'document\' is missing.');
1✔
183
    }
184

185
    return document;
8✔
186
};
187

188
export const getStyles = async (
1✔
189
    client: Figma.ClientInterface,
190
    options: PickOption<FigmaExport.StylesCommand, 'fileId' | 'version' | 'ids' | 'onlyFromPages'>,
191
): Promise<{
192
    readonly [key: string]: Figma.Style
193
}> => {
23✔
194
    const { styles } = await __getDocumentAndStyles(client, options);
23✔
195

196
    if (styles == null) {
21✔
197
        throw new Error('\'styles\' are missing.');
3✔
198
    }
199

200
    return styles;
18✔
201
};
202

203
export const getIdsFromPages = (pages: FigmaExport.PageNode[]): string[] => pages.reduce((ids: string[], page) => [
22✔
204
    ...ids,
205
    ...page.components.map((component) => component.id),
35✔
206
], []);
207

208
const fileImages = async (client: Figma.ClientInterface, fileId: string, ids: string[], version?: string) => {
17✔
209
    const response = await client.fileImages(fileId, {
17✔
210
        ids,
211
        format: 'svg',
212
        svg_include_id: true,
213
        version,
214
    }).catch((error: Error) => {
215
        throw new Error(`while fetching fileImages: ${error.message}`);
1✔
216
    });
217

218
    /**
219
     * // TODO: wrong `Figma.FileImageResponse` type on `figma-js`
220
     *
221
     * Important: the image map may contain values that are null.
222
     * This indicates that rendering of that specific node has failed.
223
     * This may be due to the node id not existing, or other reasons such has the node having no renderable components.
224
     * For example, a node that is invisible or has 0 % opacity cannot be rendered.
225
     */
226
    const { images } = response.data as Figma.FileImageResponse | {
16✔
227
        readonly images: {
228
            readonly [key: string]: string | null
229
        };
230
    };
231

232
    return images;
16✔
233
};
234

235
export const getImages = async (client: Figma.ClientInterface, fileId: string, ids: string[], version?: string) => {
17✔
236
    const idss = chunk(ids, 200);
17✔
237
    const limit = pLimit(30);
17✔
238

239
    const resolves = await Promise.all(idss.map((groupIds) => {
17✔
240
        return limit(() => fileImages(client, fileId, groupIds, version));
17✔
241
    }));
242

243
    return Object.assign({}, ...resolves) as typeof resolves[number];
16✔
244
};
245

246
type FigmaExportFileSvg = {
247
    [key: string]: string;
248
}
249

250
type FileSvgOptions = {
251
    transformers?: FigmaExport.StringTransformer[]
252
    concurrency?: number
253
    retries?: number
254
    onFetchCompleted?: (data: { index: number, total: number }) => void
255
}
256

257
export const fileSvgs = async (
1✔
258
    client: Figma.ClientInterface,
259
    fileId: string,
260
    ids: string[],
261
    version?: string,
262
    {
7✔
263
        concurrency = 30,
8✔
264
        retries = 3,
8✔
265
        transformers = [],
7✔
266
        // eslint-disable-next-line @typescript-eslint/no-empty-function
267
        onFetchCompleted = () => {},
8✔
268
    }: FileSvgOptions = {},
269
): Promise<FigmaExportFileSvg> => {
15✔
270
    const images = await getImages(client, fileId, ids, version);
15✔
271
    const limit = pLimit(concurrency);
15✔
272
    let index = 0;
15✔
273
    const svgPromises = Object.entries(images)
15✔
274
        .filter((image): image is [string, string] => typeof image[1] === 'string')
40✔
275
        .map(async ([id, url]) => {
40✔
276
            const svg = await limit(
40✔
277
                () => pRetry(() => fetchAsSvgXml(url), { retries }),
40✔
278
            );
279
            const svgTransformed = await promiseSequentially(transformers, svg);
37✔
280

281
            onFetchCompleted({
37✔
282
                index: index += 1,
283
                total: ids.length,
284
            });
285

286
            return [id, svgTransformed];
37✔
287
        });
288

289
    const svgs = await Promise.all(svgPromises);
15✔
290

291
    return fromEntries(svgs);
14✔
292
};
293

294
export const getPagesWithComponents = (
1✔
295
    document: Figma.Document,
296
    options: Required<PickOption<FigmaExport.ComponentsCommand, 'filterComponent' | 'includeTypes'>>,
297
): FigmaExport.PageNode[] => {
298
    const pages = getPagesFromDocument(document);
32✔
299
    return pages
32✔
300
        .map((page) => ({
44✔
301
            ...page,
302
            components: getComponents(page.children as readonly FigmaExport.ComponentNode[], options),
303
        }))
304
        .filter((page) => page.components.length > 0);
44✔
305
};
306

307
export const enrichPagesWithSvg = async (
1✔
308
    client: Figma.ClientInterface,
309
    fileId: string,
310
    pages: FigmaExport.PageNode[],
311
    version?: string,
312
    svgOptions?: FileSvgOptions,
313
): Promise<FigmaExport.PageNode[]> => {
15✔
314
    const componentIds = getIdsFromPages(pages);
15✔
315

316
    if (componentIds.length === 0) {
15✔
317
        throw new Error('No components found');
1✔
318
    }
319

320
    const svgs = await fileSvgs(client, fileId, componentIds, version, svgOptions);
14✔
321

322
    return pages.map((page) => ({
18✔
323
        ...page,
324
        components: page.components.map((component) => ({
29✔
325
            ...component,
326
            svg: svgs[component.id] || emptySvg,
31✔
327
        })),
328
    }));
329
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc