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

marcomontalbano / figma-export / 4952563450

pending completion
4952563450

Pull #145

github

GitHub
Merge f9fadd50b into e7ae576aa
Pull Request #145: Add `output-styles-as-style-dictionary` package

211 of 231 branches covered (91.34%)

Branch coverage included in aggregate %.

60 of 60 new or added lines in 2 files covered. (100.0%)

561 of 566 relevant lines covered (99.12%)

58.01 hits per line

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

100.0
/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
    toArray,
10
    fetchAsSvgXml,
11
    promiseSequentially,
12
    fromEntries,
13
    chunk,
14
    emptySvg,
15
} from './utils';
16

17
const getComponents = (
1✔
18
    children: readonly Figma.Node[] = [],
1✔
19
    filter: FigmaExport.ComponentFilter = () => true,
1,252✔
20
    pathToComponent: FigmaExport.ComponentExtras['pathToComponent'] = [],
69✔
21
): FigmaExport.ComponentNode[] => {
22
    let components: FigmaExport.ComponentNode[] = [];
1,925✔
23

24
    children.forEach((node) => {
1,925✔
25
        if (node.type === 'COMPONENT' && filter(node)) {
6,656✔
26
            components.push({
1,262✔
27
                ...node,
28
                svg: '',
29
                figmaExport: {
30
                    id: node.id,
31
                    dirname: dirname(node.name),
32
                    basename: basename(node.name),
33
                    pathToComponent,
34
                },
35
            });
36
            return;
1,262✔
37
        }
38

39
        if ('children' in node) {
5,394✔
40
            components = [
1,855✔
41
                ...components,
42
                ...getComponents(
43
                    (node.children),
44
                    filter,
45
                    [...pathToComponent, { name: node.name, type: node.type }],
46
                ),
47
            ];
48
        }
49
    });
50

51
    return components;
1,925✔
52
};
53

54
const filterPagesByName = (pages: readonly Figma.Canvas[], pageNames: string | string[] = []): Figma.Canvas[] => {
1✔
55
    const only = toArray(pageNames).filter((p) => p.length);
39✔
56
    return pages.filter((page) => only.length === 0 || only.includes(page.name));
76✔
57
};
58

59
type GetPagesOptions = {
60
    only?: string | string[];
61
    filter?: FigmaExport.ComponentFilter;
62
}
63

64
const getPages = (document: Figma.Document, options: GetPagesOptions = {}): FigmaExport.PageNode[] => {
1✔
65
    const pages = filterPagesByName(document.children as Figma.Canvas[], options.only);
39✔
66

67
    return pages
39✔
68
        .map((page) => ({
68✔
69
            ...page,
70
            components: getComponents(page.children as readonly FigmaExport.ComponentNode[], options.filter),
71
        }))
72
        .filter((page) => page.components.length > 0);
68✔
73
};
74

75
const getIdsFromPages = (pages: FigmaExport.PageNode[]): string[] => pages.reduce((ids: string[], page) => [
15✔
76
    ...ids,
77
    ...page.components.map((component) => component.id),
24✔
78
], []);
79

80
const getClient = (token: string): Figma.ClientInterface => {
1✔
81
    if (!token) {
2✔
82
        throw new Error('\'Access Token\' is missing. https://www.figma.com/developers/docs#authentication');
1✔
83
    }
84

85
    return Figma.Client({ personalAccessToken: token });
1✔
86
};
87

88
const fileImages = async (client: Figma.ClientInterface, fileId: string, ids: string[]): Promise<{readonly [key: string]: string}> => {
14✔
89
    const response = await client.fileImages(fileId, {
14✔
90
        ids,
91
        format: 'svg',
92
        svg_include_id: true,
93
    }).catch((error: Error) => {
94
        throw new Error(`while fetching fileImages: ${error.message}`);
1✔
95
    });
96

97
    return response.data.images;
13✔
98
};
99

100
const getImages = async (client: Figma.ClientInterface, fileId: string, ids: string[]): Promise<{readonly [key: string]: string}> => {
14✔
101
    const idss = chunk(ids, 200);
14✔
102
    const limit = pLimit(30);
14✔
103

104
    const resolves = await Promise.all(idss.map((groupIds) => {
14✔
105
        return limit(() => fileImages(client, fileId, groupIds));
14✔
106
    }));
107

108
    return Object.assign({}, ...resolves);
13✔
109
};
110

111
type FigmaExportFileSvg = {
112
    [key: string]: string;
113
}
114

115
type FileSvgOptions = {
116
    transformers?: FigmaExport.StringTransformer[]
117
    concurrency?: number
118
    retries?: number
119
    onFetchCompleted?: (data: { index: number, total: number }) => void
120
}
121

122
const fileSvgs = async (
1✔
123
    client: Figma.ClientInterface,
124
    fileId: string,
125
    ids: string[],
126
    {
7✔
127
        concurrency = 30,
8✔
128
        retries = 3,
8✔
129
        transformers = [],
7✔
130
        // eslint-disable-next-line @typescript-eslint/no-empty-function
131
        onFetchCompleted = () => {},
8✔
132
    }: FileSvgOptions = {},
133
): Promise<FigmaExportFileSvg> => {
12✔
134
    const images = await getImages(client, fileId, ids);
12✔
135
    const limit = pLimit(concurrency);
12✔
136
    let index = 0;
12✔
137
    const svgPromises = Object.entries(images).map(async ([id, url]) => {
31✔
138
        const svg = await limit(
31✔
139
            () => pRetry(() => fetchAsSvgXml(url), { retries }),
31✔
140
        );
141
        const svgTransformed = await promiseSequentially(transformers, svg);
28✔
142

143
        onFetchCompleted({
28✔
144
            index: index += 1,
145
            total: ids.length,
146
        });
147

148
        return [id, svgTransformed];
28✔
149
    });
150

151
    const svgs = await Promise.all(svgPromises);
12✔
152

153
    return fromEntries(svgs);
11✔
154
};
155

156
const enrichPagesWithSvg = async (
1✔
157
    client: Figma.ClientInterface,
158
    fileId: string,
159
    pages: FigmaExport.PageNode[],
160
    svgOptions?: FileSvgOptions,
161
): Promise<FigmaExport.PageNode[]> => {
12✔
162
    const componentIds = getIdsFromPages(pages);
12✔
163

164
    if (componentIds.length === 0) {
12✔
165
        throw new Error('No components found');
1✔
166
    }
167

168
    const svgs = await fileSvgs(client, fileId, componentIds, svgOptions);
11✔
169

170
    return pages.map((page) => ({
12✔
171
        ...page,
172
        components: page.components.map((component) => ({
20✔
173
            ...component,
174
            svg: svgs[component.id] || emptySvg,
22✔
175
        })),
176
    }));
177
};
178

179
export {
180
    getComponents,
1✔
181
    getPages,
1✔
182
    getIdsFromPages,
1✔
183
    getClient,
1✔
184
    getImages,
1✔
185
    fileSvgs,
1✔
186
    enrichPagesWithSvg,
1✔
187
};
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