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

marcomontalbano / figma-export / 7806505184

06 Feb 2024 09:43PM CUT coverage: 96.69% (-0.3%) from 96.962%
7806505184

push

github

web-flow
Merge pull request #153 from marcomontalbano/i147-onlyfrompages-styles

`onlyFromPages` can be used when exporting styles

223 of 243 branches covered (0.0%)

Branch coverage included in aggregate %.

595 of 603 relevant lines covered (98.67%)

17.05 hits per line

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

96.18
/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
    sanitizeOnlyFromPages,
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
}> => {
32✔
43
    const { data: { document = null, styles = null } = {} } = await client.file(
32✔
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 };
29✔
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'> = {},
30✔
67
): Figma.Canvas[] => {
68
    const onlyFromPages = sanitizeOnlyFromPages(options.onlyFromPages);
32✔
69
    return document.children
32✔
70
        .filter((node): node is Figma.Canvas => {
71
            return node.type === 'CANVAS' && (onlyFromPages.length === 0 || onlyFromPages.includes(node.name));
47✔
72
        });
73
};
74

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

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

90
    const pageIds = getPagesFromDocument(document, options)
2✔
91
        .map((page) => page.id);
2✔
92

93
    if (pageIds.length === 0) {
2!
94
        throw new Error(`Cannot find any page with "onlyForPages" equal to [${sanitizeOnlyFromPages(options.onlyFromPages).join(', ')}].`);
×
95
    }
96

97
    return pageIds;
2✔
98
};
99

100
export const getComponents = (
1✔
101
    children: readonly Figma.Node[] = [],
1✔
102
    filter: FigmaExport.ComponentFilter = () => true,
45✔
103
    pathToComponent: FigmaExport.ComponentExtras['pathToComponent'] = [],
41✔
104
): FigmaExport.ComponentNode[] => {
105
    let components: FigmaExport.ComponentNode[] = [];
52✔
106

107
    children.forEach((node) => {
52✔
108
        if (node.type === 'COMPONENT' && filter(node)) {
73✔
109
            components.push({
59✔
110
                ...node,
111
                svg: '',
112
                figmaExport: {
113
                    id: node.id,
114
                    dirname: dirname(node.name),
115
                    basename: basename(node.name),
116
                    pathToComponent,
117
                },
118
            });
119
            return;
59✔
120
        }
121

122
        if ('children' in node) {
14✔
123
            components = [
10✔
124
                ...components,
125
                ...getComponents(
126
                    (node.children),
127
                    filter,
128
                    [...pathToComponent, { name: node.name, type: node.type }],
129
                ),
130
            ];
131
        }
132
    });
133

134
    return components;
52✔
135
};
136

137
// eslint-disable-next-line no-underscore-dangle
138
const __getDocumentAndStyles = async (
1✔
139
    client: Figma.ClientInterface,
140
    options: PickOption<FigmaExport.ComponentsCommand, 'fileId' | 'version' | 'onlyFromPages'>,
141
): ReturnType<typeof getFile> => {
30✔
142
    return getFile(
30✔
143
        client,
144
        options,
145
        {
146
            // when `onlyFromPages` is set, we avoid traversing all the document tree, but instead we get only requested ids.
147
            ids: sanitizeOnlyFromPages(options.onlyFromPages).length > 0
30✔
148
                ? await getAllPageIds(client, options)
149
                : undefined,
150
        },
151
    );
152
};
153

154
export const getDocument = async (
1✔
155
    client: Figma.ClientInterface,
156
    options: PickOption<FigmaExport.ComponentsCommand, 'fileId' | 'version' | 'onlyFromPages'>,
157
): Promise<Figma.Document> => {
8✔
158
    const { document } = await __getDocumentAndStyles(client, options);
8✔
159

160
    if (document == null) {
7✔
161
        throw new Error('\'document\' is missing.');
1✔
162
    }
163

164
    return document;
6✔
165
};
166

167
export const getStyles = async (
1✔
168
    client: Figma.ClientInterface,
169
    options: PickOption<FigmaExport.ComponentsCommand, 'fileId' | 'version' | 'onlyFromPages'>,
170
): Promise<{
171
    readonly [key: string]: Figma.Style
172
}> => {
22✔
173
    const { styles } = await __getDocumentAndStyles(client, options);
22✔
174

175
    if (styles == null) {
20✔
176
        throw new Error('\'styles\' are missing.');
3✔
177
    }
178

179
    return styles;
17✔
180
};
181

182
export const getIdsFromPages = (pages: FigmaExport.PageNode[]): string[] => pages.reduce((ids: string[], page) => [
18✔
183
    ...ids,
184
    ...page.components.map((component) => component.id),
29✔
185
], []);
186

187
const fileImages = async (client: Figma.ClientInterface, fileId: string, ids: string[], version?: string) => {
15✔
188
    const response = await client.fileImages(fileId, {
15✔
189
        ids,
190
        format: 'svg',
191
        svg_include_id: true,
192
        version,
193
    }).catch((error: Error) => {
194
        throw new Error(`while fetching fileImages: ${error.message}`);
1✔
195
    });
196

197
    return response.data.images;
14✔
198
};
199

200
export const getImages = async (client: Figma.ClientInterface, fileId: string, ids: string[], version?: string) => {
15✔
201
    const idss = chunk(ids, 200);
15✔
202
    const limit = pLimit(30);
15✔
203

204
    const resolves = await Promise.all(idss.map((groupIds) => {
15✔
205
        return limit(() => fileImages(client, fileId, groupIds, version));
15✔
206
    }));
207

208
    return Object.assign({}, ...resolves) as typeof resolves[number];
14✔
209
};
210

211
type FigmaExportFileSvg = {
212
    [key: string]: string;
213
}
214

215
type FileSvgOptions = {
216
    transformers?: FigmaExport.StringTransformer[]
217
    concurrency?: number
218
    retries?: number
219
    onFetchCompleted?: (data: { index: number, total: number }) => void
220
}
221

222
export const fileSvgs = async (
1✔
223
    client: Figma.ClientInterface,
224
    fileId: string,
225
    ids: string[],
226
    version?: string,
227
    {
7✔
228
        concurrency = 30,
8✔
229
        retries = 3,
8✔
230
        transformers = [],
7✔
231
        // eslint-disable-next-line @typescript-eslint/no-empty-function
232
        onFetchCompleted = () => {},
8✔
233
    }: FileSvgOptions = {},
234
): Promise<FigmaExportFileSvg> => {
13✔
235
    const images = await getImages(client, fileId, ids, version);
13✔
236
    const limit = pLimit(concurrency);
13✔
237
    let index = 0;
13✔
238
    const svgPromises = Object.entries(images).map(async ([id, url]) => {
34✔
239
        const svg = await limit(
34✔
240
            () => pRetry(() => fetchAsSvgXml(url), { retries }),
34✔
241
        );
242
        const svgTransformed = await promiseSequentially(transformers, svg);
31✔
243

244
        onFetchCompleted({
31✔
245
            index: index += 1,
246
            total: ids.length,
247
        });
248

249
        return [id, svgTransformed];
31✔
250
    });
251

252
    const svgs = await Promise.all(svgPromises);
13✔
253

254
    return fromEntries(svgs);
12✔
255
};
256

257
export const getPagesWithComponents = (
1✔
258
    document: Figma.Document,
259
    options: PickOption<FigmaExport.ComponentsCommand, 'filterComponent'> = {},
23✔
260
): FigmaExport.PageNode[] => {
261
    const pages = getPagesFromDocument(document);
30✔
262

263
    return pages
30✔
264
        .map((page) => ({
40✔
265
            ...page,
266
            components: getComponents(page.children as readonly FigmaExport.ComponentNode[], options.filterComponent),
267
        }))
268
        .filter((page) => page.components.length > 0);
40✔
269
};
270

271
export const enrichPagesWithSvg = async (
1✔
272
    client: Figma.ClientInterface,
273
    fileId: string,
274
    pages: FigmaExport.PageNode[],
275
    version?: string,
276
    svgOptions?: FileSvgOptions,
277
): Promise<FigmaExport.PageNode[]> => {
13✔
278
    const componentIds = getIdsFromPages(pages);
13✔
279

280
    if (componentIds.length === 0) {
13✔
281
        throw new Error('No components found');
1✔
282
    }
283

284
    const svgs = await fileSvgs(client, fileId, componentIds, version, svgOptions);
12✔
285

286
    return pages.map((page) => ({
14✔
287
        ...page,
288
        components: page.components.map((component) => ({
23✔
289
            ...component,
290
            svg: svgs[component.id] || emptySvg,
25✔
291
        })),
292
    }));
293
};
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