figma-metadata-extractor 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -94
- package/dist/__vite-browser-external-Dyvby5gX.cjs +1 -0
- package/dist/__vite-browser-external-l0sNRNKZ.js +1 -0
- package/dist/index.cjs +146 -131
- package/dist/index.js +146 -131
- package/dist/lib.d.ts +4 -8
- package/dist/services/figma.d.ts +3 -4
- package/dist/utils/common.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,21 +22,21 @@ npm install figma-metadata-extractor
|
|
|
22
22
|
### Get Metadata with Auto-Downloaded Images (LLM-Ready!)
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
import { getFigmaMetadata } from
|
|
25
|
+
import { getFigmaMetadata } from "figma-metadata-extractor";
|
|
26
26
|
|
|
27
27
|
// Extract metadata AND automatically download image assets to disk
|
|
28
28
|
const metadata = await getFigmaMetadata(
|
|
29
|
-
|
|
29
|
+
"https://figma.com/file/ABC123/My-Design",
|
|
30
30
|
{
|
|
31
|
-
apiKey:
|
|
32
|
-
outputFormat:
|
|
33
|
-
downloadImages: true,
|
|
34
|
-
localPath:
|
|
35
|
-
}
|
|
31
|
+
apiKey: "your-figma-api-key",
|
|
32
|
+
outputFormat: "object",
|
|
33
|
+
downloadImages: true, // Auto-download image assets
|
|
34
|
+
localPath: "./assets/images", // Where to save images
|
|
35
|
+
},
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
// Nodes are enriched with downloadedImage property
|
|
39
|
-
metadata.nodes.forEach(node => {
|
|
39
|
+
metadata.nodes.forEach((node) => {
|
|
40
40
|
if (node.downloadedImage) {
|
|
41
41
|
console.log(node.downloadedImage.filePath);
|
|
42
42
|
console.log(node.downloadedImage.markdown); // 
|
|
@@ -47,17 +47,20 @@ metadata.nodes.forEach(node => {
|
|
|
47
47
|
### Get Images as Buffers (No Disk Write)
|
|
48
48
|
|
|
49
49
|
```typescript
|
|
50
|
-
import {
|
|
51
|
-
|
|
50
|
+
import {
|
|
51
|
+
getFigmaMetadata,
|
|
52
|
+
enrichMetadataWithImages,
|
|
53
|
+
} from "figma-metadata-extractor";
|
|
54
|
+
import fs from "fs/promises";
|
|
52
55
|
|
|
53
56
|
// Get metadata with images as ArrayBuffers
|
|
54
57
|
const result = await getFigmaMetadata(
|
|
55
|
-
|
|
58
|
+
"https://figma.com/file/ABC123/My-Design",
|
|
56
59
|
{
|
|
57
|
-
apiKey:
|
|
60
|
+
apiKey: "your-figma-api-key",
|
|
58
61
|
downloadImages: true,
|
|
59
|
-
returnBuffer: true
|
|
60
|
-
}
|
|
62
|
+
returnBuffer: true, // Get images as ArrayBuffer
|
|
63
|
+
},
|
|
61
64
|
);
|
|
62
65
|
|
|
63
66
|
// Images are returned separately, metadata is not enriched
|
|
@@ -75,11 +78,11 @@ for (const image of result.images) {
|
|
|
75
78
|
|
|
76
79
|
// Optionally enrich metadata with saved file paths
|
|
77
80
|
const enrichedMetadata = enrichMetadataWithImages(result, savedPaths, {
|
|
78
|
-
useRelativePaths: true
|
|
81
|
+
useRelativePaths: true,
|
|
79
82
|
});
|
|
80
83
|
|
|
81
84
|
// Now nodes have downloadedImage properties
|
|
82
|
-
enrichedMetadata.nodes.forEach(node => {
|
|
85
|
+
enrichedMetadata.nodes.forEach((node) => {
|
|
83
86
|
if (node.downloadedImage) {
|
|
84
87
|
console.log(node.downloadedImage.markdown);
|
|
85
88
|
}
|
|
@@ -89,15 +92,15 @@ enrichedMetadata.nodes.forEach(node => {
|
|
|
89
92
|
### Get Metadata Only (No Downloads)
|
|
90
93
|
|
|
91
94
|
```typescript
|
|
92
|
-
import { getFigmaMetadata } from
|
|
95
|
+
import { getFigmaMetadata } from "figma-metadata-extractor";
|
|
93
96
|
|
|
94
97
|
// Extract metadata from a Figma file
|
|
95
98
|
const metadata = await getFigmaMetadata(
|
|
96
|
-
|
|
99
|
+
"https://figma.com/file/ABC123/My-Design",
|
|
97
100
|
{
|
|
98
|
-
apiKey:
|
|
99
|
-
outputFormat:
|
|
100
|
-
}
|
|
101
|
+
apiKey: "your-figma-api-key",
|
|
102
|
+
outputFormat: "object", // or 'json' or 'yaml'
|
|
103
|
+
},
|
|
101
104
|
);
|
|
102
105
|
|
|
103
106
|
console.log(metadata.nodes); // Array of design nodes
|
|
@@ -105,35 +108,35 @@ console.log(metadata.globalVars); // Styles, colors, etc.
|
|
|
105
108
|
|
|
106
109
|
// Download images from the file
|
|
107
110
|
const images = await downloadFigmaImages(
|
|
108
|
-
|
|
111
|
+
"https://figma.com/file/ABC123/My-Design",
|
|
109
112
|
[
|
|
110
113
|
{
|
|
111
|
-
nodeId:
|
|
112
|
-
fileName:
|
|
114
|
+
nodeId: "1234:5678",
|
|
115
|
+
fileName: "icon.svg",
|
|
113
116
|
},
|
|
114
117
|
{
|
|
115
|
-
nodeId:
|
|
116
|
-
fileName:
|
|
117
|
-
}
|
|
118
|
+
nodeId: "9876:5432",
|
|
119
|
+
fileName: "hero-image.png",
|
|
120
|
+
},
|
|
118
121
|
],
|
|
119
122
|
{
|
|
120
|
-
apiKey:
|
|
121
|
-
localPath:
|
|
122
|
-
}
|
|
123
|
+
apiKey: "your-figma-api-key",
|
|
124
|
+
localPath: "./assets/images",
|
|
125
|
+
},
|
|
123
126
|
);
|
|
124
127
|
|
|
125
128
|
console.log(images); // Array of download results
|
|
126
129
|
|
|
127
130
|
// Download a single frame image from a Figma URL
|
|
128
131
|
const frameImage = await downloadFigmaFrameImage(
|
|
129
|
-
|
|
132
|
+
"https://figma.com/file/ABC123/My-Design?node-id=1234-5678",
|
|
130
133
|
{
|
|
131
|
-
apiKey:
|
|
132
|
-
localPath:
|
|
133
|
-
fileName:
|
|
134
|
-
format:
|
|
135
|
-
pngScale: 2
|
|
136
|
-
}
|
|
134
|
+
apiKey: "your-figma-api-key",
|
|
135
|
+
localPath: "./assets/frames",
|
|
136
|
+
fileName: "my-frame.png",
|
|
137
|
+
format: "png", // or 'svg'
|
|
138
|
+
pngScale: 2,
|
|
139
|
+
},
|
|
137
140
|
);
|
|
138
141
|
|
|
139
142
|
console.log(frameImage.filePath); // Path to downloaded image
|
|
@@ -146,13 +149,14 @@ console.log(frameImage.filePath); // Path to downloaded image
|
|
|
146
149
|
Extracts comprehensive metadata from a Figma file including layout, content, visuals, and component information.
|
|
147
150
|
|
|
148
151
|
**Parameters:**
|
|
152
|
+
|
|
149
153
|
- `figmaUrl` (string): The Figma file URL
|
|
150
154
|
- `options` (FigmaMetadataOptions): Configuration options
|
|
151
155
|
|
|
152
156
|
**Options:**
|
|
153
|
-
|
|
154
|
-
- `
|
|
155
|
-
- `
|
|
157
|
+
|
|
158
|
+
- `apiKey?: string` - Figma API key (Personal Access Token). Either apiKey or oauthToken is required
|
|
159
|
+
- `oauthToken?: string` - Figma OAuth Bearer token. When provided, OAuth is used automatically
|
|
156
160
|
- `outputFormat?: 'json' | 'yaml' | 'object'` - Output format (default: 'object')
|
|
157
161
|
- `depth?: number` - Maximum depth to traverse the node tree
|
|
158
162
|
- `downloadImages?: boolean` - Automatically download image assets and enrich metadata (default: false)
|
|
@@ -165,6 +169,7 @@ Extracts comprehensive metadata from a Figma file including layout, content, vis
|
|
|
165
169
|
**Returns:** Promise<FigmaMetadataResult | string>
|
|
166
170
|
|
|
167
171
|
**FigmaMetadataResult:**
|
|
172
|
+
|
|
168
173
|
```typescript
|
|
169
174
|
{
|
|
170
175
|
metadata: any; // File metadata
|
|
@@ -175,13 +180,16 @@ Extracts comprehensive metadata from a Figma file including layout, content, vis
|
|
|
175
180
|
```
|
|
176
181
|
|
|
177
182
|
When `downloadImages: true` and `returnBuffer: false`, nodes with image assets will include a `downloadedImage` property:
|
|
183
|
+
|
|
178
184
|
```typescript
|
|
179
185
|
{
|
|
180
|
-
filePath: string;
|
|
181
|
-
relativePath: string;
|
|
182
|
-
dimensions: {
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
filePath: string; // Absolute path
|
|
187
|
+
relativePath: string; // Relative path for code
|
|
188
|
+
dimensions: {
|
|
189
|
+
width, height;
|
|
190
|
+
}
|
|
191
|
+
markdown: string; // 
|
|
192
|
+
html: string; // <img src="..." />
|
|
185
193
|
}
|
|
186
194
|
```
|
|
187
195
|
|
|
@@ -192,11 +200,13 @@ When `downloadImages: true` and `returnBuffer: true`, images are returned in the
|
|
|
192
200
|
Downloads SVG and PNG images from a Figma file.
|
|
193
201
|
|
|
194
202
|
**Parameters:**
|
|
203
|
+
|
|
195
204
|
- `figmaUrl` (string): The Figma file URL
|
|
196
205
|
- `nodes` (FigmaImageNode[]): Array of image nodes to download
|
|
197
206
|
- `options` (FigmaMetadataOptions & FigmaImageOptions): Configuration options
|
|
198
207
|
|
|
199
208
|
**Node Properties:**
|
|
209
|
+
|
|
200
210
|
- `nodeId: string` - The Figma node ID (format: '1234:5678')
|
|
201
211
|
- `fileName: string` - Local filename (must end with .png or .svg)
|
|
202
212
|
- `imageRef?: string` - Image reference for image fills
|
|
@@ -206,6 +216,7 @@ Downloads SVG and PNG images from a Figma file.
|
|
|
206
216
|
- `filenameSuffix?: string` - Suffix for unique filenames
|
|
207
217
|
|
|
208
218
|
**Additional Options:**
|
|
219
|
+
|
|
209
220
|
- `pngScale?: number` - Export scale for PNG images (default: 2)
|
|
210
221
|
- `localPath?: string` - Absolute path to save images (optional if returnBuffer is true)
|
|
211
222
|
- `returnBuffer?: boolean` - Return images as ArrayBuffer instead of saving to disk (default: false)
|
|
@@ -220,6 +231,7 @@ When `returnBuffer` is true, each result will contain a `buffer` property instea
|
|
|
220
231
|
Enriches metadata with saved image file paths after saving buffers to disk.
|
|
221
232
|
|
|
222
233
|
**Parameters:**
|
|
234
|
+
|
|
223
235
|
- `metadata` (FigmaMetadataResult): The metadata result from getFigmaMetadata with returnBuffer: true
|
|
224
236
|
- `imagePaths` (string[]): Array of file paths where images were saved (must match order of metadata.images)
|
|
225
237
|
- `options` (object): Configuration options
|
|
@@ -229,25 +241,27 @@ Enriches metadata with saved image file paths after saving buffers to disk.
|
|
|
229
241
|
**Returns:** FigmaMetadataResult with enriched nodes
|
|
230
242
|
|
|
231
243
|
**Example:**
|
|
244
|
+
|
|
232
245
|
```typescript
|
|
233
246
|
// Get metadata with buffers
|
|
234
247
|
const result = await getFigmaMetadata(url, {
|
|
235
|
-
apiKey:
|
|
248
|
+
apiKey: "key",
|
|
236
249
|
downloadImages: true,
|
|
237
|
-
returnBuffer: true
|
|
250
|
+
returnBuffer: true,
|
|
238
251
|
});
|
|
239
252
|
|
|
240
253
|
// Save buffers to disk
|
|
241
254
|
const paths = await Promise.all(
|
|
242
|
-
result.images.map((img, i) =>
|
|
243
|
-
fs
|
|
244
|
-
.
|
|
245
|
-
|
|
255
|
+
result.images.map((img, i) =>
|
|
256
|
+
fs
|
|
257
|
+
.writeFile(`./images/img-${i}.png`, Buffer.from(img.buffer))
|
|
258
|
+
.then(() => `./images/img-${i}.png`),
|
|
259
|
+
),
|
|
246
260
|
);
|
|
247
261
|
|
|
248
262
|
// Enrich metadata with file paths
|
|
249
263
|
const enriched = enrichMetadataWithImages(result, paths, {
|
|
250
|
-
useRelativePaths: true
|
|
264
|
+
useRelativePaths: true,
|
|
251
265
|
});
|
|
252
266
|
```
|
|
253
267
|
|
|
@@ -256,13 +270,14 @@ const enriched = enrichMetadataWithImages(result, paths, {
|
|
|
256
270
|
Downloads a single frame image from a Figma URL that contains a node-id parameter.
|
|
257
271
|
|
|
258
272
|
**Parameters:**
|
|
273
|
+
|
|
259
274
|
- `figmaUrl` (string): The Figma URL with node-id parameter (e.g., `https://figma.com/file/ABC123/My-Design?node-id=1234-5678`)
|
|
260
275
|
- `options` (FigmaFrameImageOptions): Configuration options
|
|
261
276
|
|
|
262
277
|
**Options:**
|
|
263
|
-
|
|
264
|
-
- `
|
|
265
|
-
- `
|
|
278
|
+
|
|
279
|
+
- `apiKey?: string` - Figma API key (Personal Access Token). Either apiKey or oauthToken is required
|
|
280
|
+
- `oauthToken?: string` - Figma OAuth Bearer token. When provided, OAuth is used automatically
|
|
266
281
|
- `localPath?: string` - Absolute path to save the image (optional if returnBuffer is true)
|
|
267
282
|
- `fileName?: string` - Local filename (must end with .png or .svg, optional if returnBuffer is true)
|
|
268
283
|
- `format?: 'png' | 'svg'` - Image format to download (default: 'png')
|
|
@@ -273,6 +288,7 @@ Downloads a single frame image from a Figma URL that contains a node-id paramete
|
|
|
273
288
|
**Returns:** Promise<FigmaImageResult>
|
|
274
289
|
|
|
275
290
|
**Result Properties:**
|
|
291
|
+
|
|
276
292
|
- `filePath?: string` - Path to saved file (only when returnBuffer is false)
|
|
277
293
|
- `buffer?: ArrayBuffer` - Image data as ArrayBuffer (only when returnBuffer is true)
|
|
278
294
|
- `finalDimensions: { width: number; height: number }` - Image dimensions
|
|
@@ -281,17 +297,28 @@ Downloads a single frame image from a Figma URL that contains a node-id paramete
|
|
|
281
297
|
|
|
282
298
|
## Authentication
|
|
283
299
|
|
|
284
|
-
You need either a Figma API key or OAuth token:
|
|
300
|
+
You need either a Figma API key (Personal Access Token) or an OAuth token:
|
|
285
301
|
|
|
286
302
|
### API Key (Personal Access Token)
|
|
303
|
+
|
|
287
304
|
1. Go to Figma → Settings → Account → Personal Access Tokens
|
|
288
305
|
2. Generate a new token
|
|
289
306
|
3. Use it in the `apiKey` option
|
|
290
307
|
|
|
308
|
+
```typescript
|
|
309
|
+
getFigmaMetadata(url, { apiKey: "figd_xxx..." });
|
|
310
|
+
```
|
|
311
|
+
|
|
291
312
|
### OAuth Token
|
|
313
|
+
|
|
292
314
|
1. Set up Figma OAuth in your application
|
|
293
315
|
2. Use the bearer token in the `oauthToken` option
|
|
294
|
-
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
getFigmaMetadata(url, { oauthToken: "figu_xxx..." });
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Note:** When `oauthToken` is provided, OAuth (Bearer auth) is used automatically. If both `apiKey` and `oauthToken` are provided, `oauthToken` takes precedence.
|
|
295
322
|
|
|
296
323
|
## Usage Examples
|
|
297
324
|
|
|
@@ -300,22 +327,25 @@ You need either a Figma API key or OAuth token:
|
|
|
300
327
|
The easiest way to download a frame image is to copy the Figma URL directly from your browser when viewing a specific frame:
|
|
301
328
|
|
|
302
329
|
```typescript
|
|
303
|
-
import { downloadFigmaFrameImage } from
|
|
330
|
+
import { downloadFigmaFrameImage } from "figma-metadata-extractor";
|
|
304
331
|
|
|
305
332
|
// Copy this URL from Figma when viewing a frame
|
|
306
|
-
const figmaUrl =
|
|
333
|
+
const figmaUrl =
|
|
334
|
+
"https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678&t=xyz123";
|
|
307
335
|
|
|
308
336
|
// Save to disk
|
|
309
337
|
const result = await downloadFigmaFrameImage(figmaUrl, {
|
|
310
|
-
apiKey:
|
|
311
|
-
localPath:
|
|
312
|
-
fileName:
|
|
313
|
-
format:
|
|
314
|
-
pngScale: 2 // High resolution
|
|
338
|
+
apiKey: "your-figma-api-key",
|
|
339
|
+
localPath: "./downloads",
|
|
340
|
+
fileName: "my-frame.png",
|
|
341
|
+
format: "png",
|
|
342
|
+
pngScale: 2, // High resolution
|
|
315
343
|
});
|
|
316
344
|
|
|
317
345
|
console.log(`Downloaded to: ${result.filePath}`);
|
|
318
|
-
console.log(
|
|
346
|
+
console.log(
|
|
347
|
+
`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`,
|
|
348
|
+
);
|
|
319
349
|
```
|
|
320
350
|
|
|
321
351
|
### Get Frame Image as ArrayBuffer (No Disk Write)
|
|
@@ -323,19 +353,22 @@ console.log(`Dimensions: ${result.finalDimensions.width}x${result.finalDimension
|
|
|
323
353
|
If you want to process the image in memory without saving to disk:
|
|
324
354
|
|
|
325
355
|
```typescript
|
|
326
|
-
import { downloadFigmaFrameImage } from
|
|
356
|
+
import { downloadFigmaFrameImage } from "figma-metadata-extractor";
|
|
327
357
|
|
|
328
|
-
const figmaUrl =
|
|
358
|
+
const figmaUrl =
|
|
359
|
+
"https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678";
|
|
329
360
|
|
|
330
361
|
// Get as ArrayBuffer
|
|
331
362
|
const result = await downloadFigmaFrameImage(figmaUrl, {
|
|
332
|
-
apiKey:
|
|
363
|
+
apiKey: "your-figma-api-key",
|
|
333
364
|
returnBuffer: true,
|
|
334
|
-
format:
|
|
365
|
+
format: "png",
|
|
335
366
|
});
|
|
336
367
|
|
|
337
368
|
console.log(`Buffer size: ${result.buffer.byteLength} bytes`);
|
|
338
|
-
console.log(
|
|
369
|
+
console.log(
|
|
370
|
+
`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`,
|
|
371
|
+
);
|
|
339
372
|
|
|
340
373
|
// Use the buffer directly (e.g., upload to cloud storage, process with sharp, etc.)
|
|
341
374
|
// const processedImage = await sharp(Buffer.from(result.buffer)).resize(100, 100).toBuffer();
|
|
@@ -344,39 +377,39 @@ console.log(`Dimensions: ${result.finalDimensions.width}x${result.finalDimension
|
|
|
344
377
|
### Download Multiple Frame Images
|
|
345
378
|
|
|
346
379
|
```typescript
|
|
347
|
-
import { downloadFigmaImages } from
|
|
380
|
+
import { downloadFigmaImages } from "figma-metadata-extractor";
|
|
348
381
|
|
|
349
382
|
// For multiple frames, use the batch download function
|
|
350
383
|
const results = await downloadFigmaImages(
|
|
351
|
-
|
|
384
|
+
"https://figma.com/file/ABC123/My-Design",
|
|
352
385
|
[
|
|
353
|
-
{ nodeId:
|
|
354
|
-
{ nodeId:
|
|
355
|
-
{ nodeId:
|
|
386
|
+
{ nodeId: "1234:5678", fileName: "frame1.png" },
|
|
387
|
+
{ nodeId: "9876:5432", fileName: "frame2.svg" },
|
|
388
|
+
{ nodeId: "1111:2222", fileName: "frame3.png" },
|
|
356
389
|
],
|
|
357
390
|
{
|
|
358
|
-
apiKey:
|
|
359
|
-
localPath:
|
|
360
|
-
}
|
|
391
|
+
apiKey: "your-figma-api-key",
|
|
392
|
+
localPath: "./frames",
|
|
393
|
+
},
|
|
361
394
|
);
|
|
362
395
|
```
|
|
363
396
|
|
|
364
397
|
### Download Multiple Images as Buffers
|
|
365
398
|
|
|
366
399
|
```typescript
|
|
367
|
-
import { downloadFigmaImages } from
|
|
400
|
+
import { downloadFigmaImages } from "figma-metadata-extractor";
|
|
368
401
|
|
|
369
402
|
// Get multiple images as ArrayBuffers
|
|
370
403
|
const results = await downloadFigmaImages(
|
|
371
|
-
|
|
404
|
+
"https://figma.com/file/ABC123/My-Design",
|
|
372
405
|
[
|
|
373
|
-
{ nodeId:
|
|
374
|
-
{ nodeId:
|
|
406
|
+
{ nodeId: "1234:5678", fileName: "frame1.png" },
|
|
407
|
+
{ nodeId: "9876:5432", fileName: "frame2.png" },
|
|
375
408
|
],
|
|
376
409
|
{
|
|
377
|
-
apiKey:
|
|
378
|
-
returnBuffer: true
|
|
379
|
-
}
|
|
410
|
+
apiKey: "your-figma-api-key",
|
|
411
|
+
returnBuffer: true,
|
|
412
|
+
},
|
|
380
413
|
);
|
|
381
414
|
|
|
382
415
|
// Process each buffer
|
|
@@ -391,20 +424,20 @@ results.forEach((result, index) => {
|
|
|
391
424
|
The library also exports the underlying extractor system for custom processing:
|
|
392
425
|
|
|
393
426
|
```typescript
|
|
394
|
-
import {
|
|
395
|
-
simplifyRawFigmaObject,
|
|
427
|
+
import {
|
|
428
|
+
simplifyRawFigmaObject,
|
|
396
429
|
allExtractors,
|
|
397
430
|
layoutExtractor,
|
|
398
|
-
textExtractor
|
|
399
|
-
} from
|
|
431
|
+
textExtractor,
|
|
432
|
+
} from "figma-metadata-extractor";
|
|
400
433
|
|
|
401
434
|
// Use specific extractors
|
|
402
|
-
const customResult = simplifyRawFigmaObject(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
);
|
|
435
|
+
const customResult = simplifyRawFigmaObject(rawFigmaResponse, [
|
|
436
|
+
layoutExtractor,
|
|
437
|
+
textExtractor,
|
|
438
|
+
]);
|
|
406
439
|
```
|
|
407
440
|
|
|
408
441
|
## License
|
|
409
442
|
|
|
410
|
-
MIT
|
|
443
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/index.cjs
CHANGED
|
@@ -265,15 +265,131 @@ function formatHeadersForCurl(headers) {
|
|
|
265
265
|
}
|
|
266
266
|
return headerArgs;
|
|
267
267
|
}
|
|
268
|
+
async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
|
|
269
|
+
const { pipeline } = await Promise.resolve().then(() => require("./__vite-browser-external-Dyvby5gX.cjs"));
|
|
270
|
+
const { Readable } = await Promise.resolve().then(() => require("./__vite-browser-external-Dyvby5gX.cjs"));
|
|
271
|
+
try {
|
|
272
|
+
const response = await fetch(imageUrl, {
|
|
273
|
+
method: "GET"
|
|
274
|
+
});
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Failed to download image: ${response.status} ${response.statusText}`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (!response.body) {
|
|
281
|
+
throw new Error("Response body is empty");
|
|
282
|
+
}
|
|
283
|
+
if (returnBuffer) {
|
|
284
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
285
|
+
if (arrayBuffer.byteLength === 0) {
|
|
286
|
+
throw new Error("Downloaded image buffer is empty");
|
|
287
|
+
}
|
|
288
|
+
return arrayBuffer;
|
|
289
|
+
}
|
|
290
|
+
if (!fs.existsSync(localPath)) {
|
|
291
|
+
fs.mkdirSync(localPath, { recursive: true });
|
|
292
|
+
}
|
|
293
|
+
const fullPath = path.join(localPath, fileName);
|
|
294
|
+
const fileStream = fs.createWriteStream(fullPath);
|
|
295
|
+
await pipeline(Readable.fromWeb(response.body), fileStream);
|
|
296
|
+
const stats = fs.statSync(fullPath);
|
|
297
|
+
if (stats.size === 0) {
|
|
298
|
+
fs.unlinkSync(fullPath);
|
|
299
|
+
throw new Error("Downloaded file is empty (0 bytes)");
|
|
300
|
+
}
|
|
301
|
+
return fullPath;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
304
|
+
throw new Error(`Error downloading image: ${errorMessage}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function generateVarId(prefix = "var") {
|
|
308
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
309
|
+
let result = "";
|
|
310
|
+
for (let i = 0; i < 6; i++) {
|
|
311
|
+
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
312
|
+
result += chars[randomIndex];
|
|
313
|
+
}
|
|
314
|
+
return `${prefix}_${result}`;
|
|
315
|
+
}
|
|
316
|
+
function generateCSSShorthand(values, {
|
|
317
|
+
ignoreZero = true,
|
|
318
|
+
suffix = "px"
|
|
319
|
+
} = {}) {
|
|
320
|
+
const { top, right, bottom, left } = values;
|
|
321
|
+
if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) {
|
|
322
|
+
return void 0;
|
|
323
|
+
}
|
|
324
|
+
if (top === right && right === bottom && bottom === left) {
|
|
325
|
+
return `${top}${suffix}`;
|
|
326
|
+
}
|
|
327
|
+
if (right === left) {
|
|
328
|
+
if (top === bottom) {
|
|
329
|
+
return `${top}${suffix} ${right}${suffix}`;
|
|
330
|
+
}
|
|
331
|
+
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`;
|
|
332
|
+
}
|
|
333
|
+
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`;
|
|
334
|
+
}
|
|
335
|
+
function isVisible(element) {
|
|
336
|
+
return element.visible ?? true;
|
|
337
|
+
}
|
|
338
|
+
function pixelRound(num) {
|
|
339
|
+
if (isNaN(num)) {
|
|
340
|
+
throw new TypeError(`Input must be a valid number`);
|
|
341
|
+
}
|
|
342
|
+
return Number(Number(num).toFixed(2));
|
|
343
|
+
}
|
|
344
|
+
async function runWithConcurrency(tasks, limit) {
|
|
345
|
+
const results = new Array(tasks.length);
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
if (tasks.length === 0) {
|
|
348
|
+
resolve([]);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
let completed = 0;
|
|
352
|
+
let launched = 0;
|
|
353
|
+
let failed = false;
|
|
354
|
+
const next = () => {
|
|
355
|
+
if (failed) return;
|
|
356
|
+
if (completed === tasks.length) {
|
|
357
|
+
resolve(results);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
while (launched < tasks.length && launched - completed < limit) {
|
|
361
|
+
const index = launched++;
|
|
362
|
+
tasks[index]().then((result) => {
|
|
363
|
+
results[index] = result;
|
|
364
|
+
completed++;
|
|
365
|
+
next();
|
|
366
|
+
}).catch((err) => {
|
|
367
|
+
failed = true;
|
|
368
|
+
reject(err);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
next();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const common = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
376
|
+
__proto__: null,
|
|
377
|
+
downloadFigmaImage,
|
|
378
|
+
generateCSSShorthand,
|
|
379
|
+
generateVarId,
|
|
380
|
+
isVisible,
|
|
381
|
+
pixelRound,
|
|
382
|
+
runWithConcurrency
|
|
383
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
268
384
|
class FigmaService {
|
|
269
385
|
apiKey;
|
|
270
386
|
oauthToken;
|
|
271
387
|
useOAuth;
|
|
272
388
|
baseUrl = "https://api.figma.com/v1";
|
|
273
|
-
constructor({ figmaApiKey, figmaOAuthToken
|
|
274
|
-
this.apiKey = figmaApiKey
|
|
275
|
-
this.oauthToken = figmaOAuthToken
|
|
276
|
-
this.useOAuth = !!
|
|
389
|
+
constructor({ figmaApiKey, figmaOAuthToken }) {
|
|
390
|
+
this.apiKey = figmaApiKey ?? "";
|
|
391
|
+
this.oauthToken = figmaOAuthToken ?? "";
|
|
392
|
+
this.useOAuth = !!this.oauthToken;
|
|
277
393
|
}
|
|
278
394
|
getAuthHeaders() {
|
|
279
395
|
if (this.useOAuth) {
|
|
@@ -372,6 +488,7 @@ class FigmaService {
|
|
|
372
488
|
async downloadImages(fileKey, localPath, items, options = {}) {
|
|
373
489
|
if (items.length === 0) return [];
|
|
374
490
|
const { pngScale = 2, svgOptions, returnBuffer = false } = options;
|
|
491
|
+
const CONCURRENCY_LIMIT = 10;
|
|
375
492
|
let resolvedPath = "";
|
|
376
493
|
if (!returnBuffer) {
|
|
377
494
|
const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
@@ -391,7 +508,7 @@ class FigmaService {
|
|
|
391
508
|
);
|
|
392
509
|
if (imageFills.length > 0) {
|
|
393
510
|
const fillUrls = await this.getImageFillUrls(fileKey);
|
|
394
|
-
const
|
|
511
|
+
const fillTasks = imageFills.map(
|
|
395
512
|
({
|
|
396
513
|
imageRef,
|
|
397
514
|
fileName,
|
|
@@ -406,7 +523,7 @@ class FigmaService {
|
|
|
406
523
|
);
|
|
407
524
|
return null;
|
|
408
525
|
}
|
|
409
|
-
return downloadAndProcessImage(
|
|
526
|
+
return () => downloadAndProcessImage(
|
|
410
527
|
fileName,
|
|
411
528
|
resolvedPath,
|
|
412
529
|
imageUrl,
|
|
@@ -417,10 +534,10 @@ class FigmaService {
|
|
|
417
534
|
);
|
|
418
535
|
}
|
|
419
536
|
).filter(
|
|
420
|
-
(
|
|
537
|
+
(task) => task !== null
|
|
421
538
|
);
|
|
422
|
-
if (
|
|
423
|
-
downloadPromises.push(
|
|
539
|
+
if (fillTasks.length > 0) {
|
|
540
|
+
downloadPromises.push(runWithConcurrency(fillTasks, CONCURRENCY_LIMIT));
|
|
424
541
|
}
|
|
425
542
|
}
|
|
426
543
|
if (renderNodes.length > 0) {
|
|
@@ -437,7 +554,7 @@ class FigmaService {
|
|
|
437
554
|
"png",
|
|
438
555
|
{ pngScale }
|
|
439
556
|
);
|
|
440
|
-
const
|
|
557
|
+
const pngTasks = pngNodes.map(
|
|
441
558
|
({
|
|
442
559
|
nodeId,
|
|
443
560
|
fileName,
|
|
@@ -452,7 +569,7 @@ class FigmaService {
|
|
|
452
569
|
);
|
|
453
570
|
return null;
|
|
454
571
|
}
|
|
455
|
-
return downloadAndProcessImage(
|
|
572
|
+
return () => downloadAndProcessImage(
|
|
456
573
|
fileName,
|
|
457
574
|
resolvedPath,
|
|
458
575
|
imageUrl,
|
|
@@ -465,10 +582,12 @@ class FigmaService {
|
|
|
465
582
|
);
|
|
466
583
|
}
|
|
467
584
|
).filter(
|
|
468
|
-
(
|
|
585
|
+
(task) => task !== null
|
|
469
586
|
);
|
|
470
|
-
if (
|
|
471
|
-
downloadPromises.push(
|
|
587
|
+
if (pngTasks.length > 0) {
|
|
588
|
+
downloadPromises.push(
|
|
589
|
+
runWithConcurrency(pngTasks, CONCURRENCY_LIMIT)
|
|
590
|
+
);
|
|
472
591
|
}
|
|
473
592
|
}
|
|
474
593
|
if (svgNodes.length > 0) {
|
|
@@ -478,7 +597,7 @@ class FigmaService {
|
|
|
478
597
|
"svg",
|
|
479
598
|
{ svgOptions }
|
|
480
599
|
);
|
|
481
|
-
const
|
|
600
|
+
const svgTasks = svgNodes.map(
|
|
482
601
|
({
|
|
483
602
|
nodeId,
|
|
484
603
|
fileName,
|
|
@@ -493,7 +612,7 @@ class FigmaService {
|
|
|
493
612
|
);
|
|
494
613
|
return null;
|
|
495
614
|
}
|
|
496
|
-
return downloadAndProcessImage(
|
|
615
|
+
return () => downloadAndProcessImage(
|
|
497
616
|
fileName,
|
|
498
617
|
resolvedPath,
|
|
499
618
|
imageUrl,
|
|
@@ -506,10 +625,12 @@ class FigmaService {
|
|
|
506
625
|
);
|
|
507
626
|
}
|
|
508
627
|
).filter(
|
|
509
|
-
(
|
|
628
|
+
(task) => task !== null
|
|
510
629
|
);
|
|
511
|
-
if (
|
|
512
|
-
downloadPromises.push(
|
|
630
|
+
if (svgTasks.length > 0) {
|
|
631
|
+
downloadPromises.push(
|
|
632
|
+
runWithConcurrency(svgTasks, CONCURRENCY_LIMIT)
|
|
633
|
+
);
|
|
513
634
|
}
|
|
514
635
|
}
|
|
515
636
|
}
|
|
@@ -541,106 +662,6 @@ class FigmaService {
|
|
|
541
662
|
return response;
|
|
542
663
|
}
|
|
543
664
|
}
|
|
544
|
-
async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
|
|
545
|
-
try {
|
|
546
|
-
const response = await fetch(imageUrl, {
|
|
547
|
-
method: "GET"
|
|
548
|
-
});
|
|
549
|
-
if (!response.ok) {
|
|
550
|
-
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
551
|
-
}
|
|
552
|
-
if (returnBuffer) {
|
|
553
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
554
|
-
return arrayBuffer;
|
|
555
|
-
}
|
|
556
|
-
if (!fs.existsSync(localPath)) {
|
|
557
|
-
fs.mkdirSync(localPath, { recursive: true });
|
|
558
|
-
}
|
|
559
|
-
const fullPath = path.join(localPath, fileName);
|
|
560
|
-
const reader = response.body?.getReader();
|
|
561
|
-
if (!reader) {
|
|
562
|
-
throw new Error("Failed to get response body");
|
|
563
|
-
}
|
|
564
|
-
const writer = fs.createWriteStream(fullPath);
|
|
565
|
-
return new Promise((resolve, reject) => {
|
|
566
|
-
const processStream = async () => {
|
|
567
|
-
try {
|
|
568
|
-
while (true) {
|
|
569
|
-
const { done, value } = await reader.read();
|
|
570
|
-
if (done) {
|
|
571
|
-
writer.end();
|
|
572
|
-
break;
|
|
573
|
-
}
|
|
574
|
-
writer.write(value);
|
|
575
|
-
}
|
|
576
|
-
} catch (err) {
|
|
577
|
-
writer.end();
|
|
578
|
-
fs.unlink(fullPath, () => {
|
|
579
|
-
});
|
|
580
|
-
reject(err);
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
writer.on("finish", () => {
|
|
584
|
-
resolve(fullPath);
|
|
585
|
-
});
|
|
586
|
-
writer.on("error", (err) => {
|
|
587
|
-
reader.cancel();
|
|
588
|
-
fs.unlink(fullPath, () => {
|
|
589
|
-
});
|
|
590
|
-
reject(new Error(`Failed to write image: ${err.message}`));
|
|
591
|
-
});
|
|
592
|
-
processStream();
|
|
593
|
-
});
|
|
594
|
-
} catch (error) {
|
|
595
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
596
|
-
throw new Error(`Error downloading image: ${errorMessage}`);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
function generateVarId(prefix = "var") {
|
|
600
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
601
|
-
let result = "";
|
|
602
|
-
for (let i = 0; i < 6; i++) {
|
|
603
|
-
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
604
|
-
result += chars[randomIndex];
|
|
605
|
-
}
|
|
606
|
-
return `${prefix}_${result}`;
|
|
607
|
-
}
|
|
608
|
-
function generateCSSShorthand(values, {
|
|
609
|
-
ignoreZero = true,
|
|
610
|
-
suffix = "px"
|
|
611
|
-
} = {}) {
|
|
612
|
-
const { top, right, bottom, left } = values;
|
|
613
|
-
if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) {
|
|
614
|
-
return void 0;
|
|
615
|
-
}
|
|
616
|
-
if (top === right && right === bottom && bottom === left) {
|
|
617
|
-
return `${top}${suffix}`;
|
|
618
|
-
}
|
|
619
|
-
if (right === left) {
|
|
620
|
-
if (top === bottom) {
|
|
621
|
-
return `${top}${suffix} ${right}${suffix}`;
|
|
622
|
-
}
|
|
623
|
-
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`;
|
|
624
|
-
}
|
|
625
|
-
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`;
|
|
626
|
-
}
|
|
627
|
-
function isVisible(element) {
|
|
628
|
-
return element.visible ?? true;
|
|
629
|
-
}
|
|
630
|
-
function pixelRound(num) {
|
|
631
|
-
if (isNaN(num)) {
|
|
632
|
-
throw new TypeError(`Input must be a valid number`);
|
|
633
|
-
}
|
|
634
|
-
return Number(Number(num).toFixed(2));
|
|
635
|
-
}
|
|
636
|
-
const common = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
637
|
-
__proto__: null,
|
|
638
|
-
downloadFigmaImage,
|
|
639
|
-
generateCSSShorthand,
|
|
640
|
-
generateVarId,
|
|
641
|
-
isVisible,
|
|
642
|
-
pixelRound
|
|
643
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
644
665
|
function hasValue(key, obj, typeGuard) {
|
|
645
666
|
const isObject = typeof obj === "object" && obj !== null;
|
|
646
667
|
if (!isObject || !(key in obj)) return false;
|
|
@@ -1504,7 +1525,6 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1504
1525
|
const {
|
|
1505
1526
|
apiKey,
|
|
1506
1527
|
oauthToken,
|
|
1507
|
-
useOAuth = false,
|
|
1508
1528
|
outputFormat = "object",
|
|
1509
1529
|
depth,
|
|
1510
1530
|
downloadImages = false,
|
|
@@ -1527,9 +1547,8 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1527
1547
|
const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
|
|
1528
1548
|
const nodeId = nodeIdMatch ? nodeIdMatch[1].replace(/-/g, ":") : void 0;
|
|
1529
1549
|
const figmaService = new FigmaService({
|
|
1530
|
-
figmaApiKey: apiKey
|
|
1531
|
-
figmaOAuthToken: oauthToken
|
|
1532
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1550
|
+
figmaApiKey: apiKey,
|
|
1551
|
+
figmaOAuthToken: oauthToken
|
|
1533
1552
|
});
|
|
1534
1553
|
try {
|
|
1535
1554
|
Logger.log(
|
|
@@ -1624,7 +1643,6 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1624
1643
|
const {
|
|
1625
1644
|
apiKey,
|
|
1626
1645
|
oauthToken,
|
|
1627
|
-
useOAuth = false,
|
|
1628
1646
|
pngScale = 2,
|
|
1629
1647
|
localPath,
|
|
1630
1648
|
enableLogging = false,
|
|
@@ -1643,9 +1661,8 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1643
1661
|
}
|
|
1644
1662
|
const fileKey = urlMatch[2];
|
|
1645
1663
|
const figmaService = new FigmaService({
|
|
1646
|
-
figmaApiKey: apiKey
|
|
1647
|
-
figmaOAuthToken: oauthToken
|
|
1648
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1664
|
+
figmaApiKey: apiKey,
|
|
1665
|
+
figmaOAuthToken: oauthToken
|
|
1649
1666
|
});
|
|
1650
1667
|
try {
|
|
1651
1668
|
const processedNodes = nodes.map((node) => ({
|
|
@@ -1673,7 +1690,6 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1673
1690
|
const {
|
|
1674
1691
|
apiKey,
|
|
1675
1692
|
oauthToken,
|
|
1676
|
-
useOAuth = false,
|
|
1677
1693
|
pngScale = 2,
|
|
1678
1694
|
localPath,
|
|
1679
1695
|
fileName,
|
|
@@ -1711,9 +1727,8 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1711
1727
|
}
|
|
1712
1728
|
}
|
|
1713
1729
|
const figmaService = new FigmaService({
|
|
1714
|
-
figmaApiKey: apiKey
|
|
1715
|
-
figmaOAuthToken: oauthToken
|
|
1716
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1730
|
+
figmaApiKey: apiKey,
|
|
1731
|
+
figmaOAuthToken: oauthToken
|
|
1717
1732
|
});
|
|
1718
1733
|
try {
|
|
1719
1734
|
Logger.log(
|
package/dist/index.js
CHANGED
|
@@ -263,15 +263,131 @@ function formatHeadersForCurl(headers) {
|
|
|
263
263
|
}
|
|
264
264
|
return headerArgs;
|
|
265
265
|
}
|
|
266
|
+
async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
|
|
267
|
+
const { pipeline } = await import("./__vite-browser-external-l0sNRNKZ.js");
|
|
268
|
+
const { Readable } = await import("./__vite-browser-external-l0sNRNKZ.js");
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch(imageUrl, {
|
|
271
|
+
method: "GET"
|
|
272
|
+
});
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Failed to download image: ${response.status} ${response.statusText}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (!response.body) {
|
|
279
|
+
throw new Error("Response body is empty");
|
|
280
|
+
}
|
|
281
|
+
if (returnBuffer) {
|
|
282
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
283
|
+
if (arrayBuffer.byteLength === 0) {
|
|
284
|
+
throw new Error("Downloaded image buffer is empty");
|
|
285
|
+
}
|
|
286
|
+
return arrayBuffer;
|
|
287
|
+
}
|
|
288
|
+
if (!fs.existsSync(localPath)) {
|
|
289
|
+
fs.mkdirSync(localPath, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
const fullPath = path.join(localPath, fileName);
|
|
292
|
+
const fileStream = fs.createWriteStream(fullPath);
|
|
293
|
+
await pipeline(Readable.fromWeb(response.body), fileStream);
|
|
294
|
+
const stats = fs.statSync(fullPath);
|
|
295
|
+
if (stats.size === 0) {
|
|
296
|
+
fs.unlinkSync(fullPath);
|
|
297
|
+
throw new Error("Downloaded file is empty (0 bytes)");
|
|
298
|
+
}
|
|
299
|
+
return fullPath;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
302
|
+
throw new Error(`Error downloading image: ${errorMessage}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function generateVarId(prefix = "var") {
|
|
306
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
307
|
+
let result = "";
|
|
308
|
+
for (let i = 0; i < 6; i++) {
|
|
309
|
+
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
310
|
+
result += chars[randomIndex];
|
|
311
|
+
}
|
|
312
|
+
return `${prefix}_${result}`;
|
|
313
|
+
}
|
|
314
|
+
function generateCSSShorthand(values, {
|
|
315
|
+
ignoreZero = true,
|
|
316
|
+
suffix = "px"
|
|
317
|
+
} = {}) {
|
|
318
|
+
const { top, right, bottom, left } = values;
|
|
319
|
+
if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) {
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
if (top === right && right === bottom && bottom === left) {
|
|
323
|
+
return `${top}${suffix}`;
|
|
324
|
+
}
|
|
325
|
+
if (right === left) {
|
|
326
|
+
if (top === bottom) {
|
|
327
|
+
return `${top}${suffix} ${right}${suffix}`;
|
|
328
|
+
}
|
|
329
|
+
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`;
|
|
330
|
+
}
|
|
331
|
+
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`;
|
|
332
|
+
}
|
|
333
|
+
function isVisible(element) {
|
|
334
|
+
return element.visible ?? true;
|
|
335
|
+
}
|
|
336
|
+
function pixelRound(num) {
|
|
337
|
+
if (isNaN(num)) {
|
|
338
|
+
throw new TypeError(`Input must be a valid number`);
|
|
339
|
+
}
|
|
340
|
+
return Number(Number(num).toFixed(2));
|
|
341
|
+
}
|
|
342
|
+
async function runWithConcurrency(tasks, limit) {
|
|
343
|
+
const results = new Array(tasks.length);
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
if (tasks.length === 0) {
|
|
346
|
+
resolve([]);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
let completed = 0;
|
|
350
|
+
let launched = 0;
|
|
351
|
+
let failed = false;
|
|
352
|
+
const next = () => {
|
|
353
|
+
if (failed) return;
|
|
354
|
+
if (completed === tasks.length) {
|
|
355
|
+
resolve(results);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
while (launched < tasks.length && launched - completed < limit) {
|
|
359
|
+
const index = launched++;
|
|
360
|
+
tasks[index]().then((result) => {
|
|
361
|
+
results[index] = result;
|
|
362
|
+
completed++;
|
|
363
|
+
next();
|
|
364
|
+
}).catch((err) => {
|
|
365
|
+
failed = true;
|
|
366
|
+
reject(err);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
next();
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const common = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
374
|
+
__proto__: null,
|
|
375
|
+
downloadFigmaImage,
|
|
376
|
+
generateCSSShorthand,
|
|
377
|
+
generateVarId,
|
|
378
|
+
isVisible,
|
|
379
|
+
pixelRound,
|
|
380
|
+
runWithConcurrency
|
|
381
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
266
382
|
class FigmaService {
|
|
267
383
|
apiKey;
|
|
268
384
|
oauthToken;
|
|
269
385
|
useOAuth;
|
|
270
386
|
baseUrl = "https://api.figma.com/v1";
|
|
271
|
-
constructor({ figmaApiKey, figmaOAuthToken
|
|
272
|
-
this.apiKey = figmaApiKey
|
|
273
|
-
this.oauthToken = figmaOAuthToken
|
|
274
|
-
this.useOAuth = !!
|
|
387
|
+
constructor({ figmaApiKey, figmaOAuthToken }) {
|
|
388
|
+
this.apiKey = figmaApiKey ?? "";
|
|
389
|
+
this.oauthToken = figmaOAuthToken ?? "";
|
|
390
|
+
this.useOAuth = !!this.oauthToken;
|
|
275
391
|
}
|
|
276
392
|
getAuthHeaders() {
|
|
277
393
|
if (this.useOAuth) {
|
|
@@ -370,6 +486,7 @@ class FigmaService {
|
|
|
370
486
|
async downloadImages(fileKey, localPath, items, options = {}) {
|
|
371
487
|
if (items.length === 0) return [];
|
|
372
488
|
const { pngScale = 2, svgOptions, returnBuffer = false } = options;
|
|
489
|
+
const CONCURRENCY_LIMIT = 10;
|
|
373
490
|
let resolvedPath = "";
|
|
374
491
|
if (!returnBuffer) {
|
|
375
492
|
const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
@@ -389,7 +506,7 @@ class FigmaService {
|
|
|
389
506
|
);
|
|
390
507
|
if (imageFills.length > 0) {
|
|
391
508
|
const fillUrls = await this.getImageFillUrls(fileKey);
|
|
392
|
-
const
|
|
509
|
+
const fillTasks = imageFills.map(
|
|
393
510
|
({
|
|
394
511
|
imageRef,
|
|
395
512
|
fileName,
|
|
@@ -404,7 +521,7 @@ class FigmaService {
|
|
|
404
521
|
);
|
|
405
522
|
return null;
|
|
406
523
|
}
|
|
407
|
-
return downloadAndProcessImage(
|
|
524
|
+
return () => downloadAndProcessImage(
|
|
408
525
|
fileName,
|
|
409
526
|
resolvedPath,
|
|
410
527
|
imageUrl,
|
|
@@ -415,10 +532,10 @@ class FigmaService {
|
|
|
415
532
|
);
|
|
416
533
|
}
|
|
417
534
|
).filter(
|
|
418
|
-
(
|
|
535
|
+
(task) => task !== null
|
|
419
536
|
);
|
|
420
|
-
if (
|
|
421
|
-
downloadPromises.push(
|
|
537
|
+
if (fillTasks.length > 0) {
|
|
538
|
+
downloadPromises.push(runWithConcurrency(fillTasks, CONCURRENCY_LIMIT));
|
|
422
539
|
}
|
|
423
540
|
}
|
|
424
541
|
if (renderNodes.length > 0) {
|
|
@@ -435,7 +552,7 @@ class FigmaService {
|
|
|
435
552
|
"png",
|
|
436
553
|
{ pngScale }
|
|
437
554
|
);
|
|
438
|
-
const
|
|
555
|
+
const pngTasks = pngNodes.map(
|
|
439
556
|
({
|
|
440
557
|
nodeId,
|
|
441
558
|
fileName,
|
|
@@ -450,7 +567,7 @@ class FigmaService {
|
|
|
450
567
|
);
|
|
451
568
|
return null;
|
|
452
569
|
}
|
|
453
|
-
return downloadAndProcessImage(
|
|
570
|
+
return () => downloadAndProcessImage(
|
|
454
571
|
fileName,
|
|
455
572
|
resolvedPath,
|
|
456
573
|
imageUrl,
|
|
@@ -463,10 +580,12 @@ class FigmaService {
|
|
|
463
580
|
);
|
|
464
581
|
}
|
|
465
582
|
).filter(
|
|
466
|
-
(
|
|
583
|
+
(task) => task !== null
|
|
467
584
|
);
|
|
468
|
-
if (
|
|
469
|
-
downloadPromises.push(
|
|
585
|
+
if (pngTasks.length > 0) {
|
|
586
|
+
downloadPromises.push(
|
|
587
|
+
runWithConcurrency(pngTasks, CONCURRENCY_LIMIT)
|
|
588
|
+
);
|
|
470
589
|
}
|
|
471
590
|
}
|
|
472
591
|
if (svgNodes.length > 0) {
|
|
@@ -476,7 +595,7 @@ class FigmaService {
|
|
|
476
595
|
"svg",
|
|
477
596
|
{ svgOptions }
|
|
478
597
|
);
|
|
479
|
-
const
|
|
598
|
+
const svgTasks = svgNodes.map(
|
|
480
599
|
({
|
|
481
600
|
nodeId,
|
|
482
601
|
fileName,
|
|
@@ -491,7 +610,7 @@ class FigmaService {
|
|
|
491
610
|
);
|
|
492
611
|
return null;
|
|
493
612
|
}
|
|
494
|
-
return downloadAndProcessImage(
|
|
613
|
+
return () => downloadAndProcessImage(
|
|
495
614
|
fileName,
|
|
496
615
|
resolvedPath,
|
|
497
616
|
imageUrl,
|
|
@@ -504,10 +623,12 @@ class FigmaService {
|
|
|
504
623
|
);
|
|
505
624
|
}
|
|
506
625
|
).filter(
|
|
507
|
-
(
|
|
626
|
+
(task) => task !== null
|
|
508
627
|
);
|
|
509
|
-
if (
|
|
510
|
-
downloadPromises.push(
|
|
628
|
+
if (svgTasks.length > 0) {
|
|
629
|
+
downloadPromises.push(
|
|
630
|
+
runWithConcurrency(svgTasks, CONCURRENCY_LIMIT)
|
|
631
|
+
);
|
|
511
632
|
}
|
|
512
633
|
}
|
|
513
634
|
}
|
|
@@ -539,106 +660,6 @@ class FigmaService {
|
|
|
539
660
|
return response;
|
|
540
661
|
}
|
|
541
662
|
}
|
|
542
|
-
async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
|
|
543
|
-
try {
|
|
544
|
-
const response = await fetch(imageUrl, {
|
|
545
|
-
method: "GET"
|
|
546
|
-
});
|
|
547
|
-
if (!response.ok) {
|
|
548
|
-
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
549
|
-
}
|
|
550
|
-
if (returnBuffer) {
|
|
551
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
552
|
-
return arrayBuffer;
|
|
553
|
-
}
|
|
554
|
-
if (!fs.existsSync(localPath)) {
|
|
555
|
-
fs.mkdirSync(localPath, { recursive: true });
|
|
556
|
-
}
|
|
557
|
-
const fullPath = path.join(localPath, fileName);
|
|
558
|
-
const reader = response.body?.getReader();
|
|
559
|
-
if (!reader) {
|
|
560
|
-
throw new Error("Failed to get response body");
|
|
561
|
-
}
|
|
562
|
-
const writer = fs.createWriteStream(fullPath);
|
|
563
|
-
return new Promise((resolve, reject) => {
|
|
564
|
-
const processStream = async () => {
|
|
565
|
-
try {
|
|
566
|
-
while (true) {
|
|
567
|
-
const { done, value } = await reader.read();
|
|
568
|
-
if (done) {
|
|
569
|
-
writer.end();
|
|
570
|
-
break;
|
|
571
|
-
}
|
|
572
|
-
writer.write(value);
|
|
573
|
-
}
|
|
574
|
-
} catch (err) {
|
|
575
|
-
writer.end();
|
|
576
|
-
fs.unlink(fullPath, () => {
|
|
577
|
-
});
|
|
578
|
-
reject(err);
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
writer.on("finish", () => {
|
|
582
|
-
resolve(fullPath);
|
|
583
|
-
});
|
|
584
|
-
writer.on("error", (err) => {
|
|
585
|
-
reader.cancel();
|
|
586
|
-
fs.unlink(fullPath, () => {
|
|
587
|
-
});
|
|
588
|
-
reject(new Error(`Failed to write image: ${err.message}`));
|
|
589
|
-
});
|
|
590
|
-
processStream();
|
|
591
|
-
});
|
|
592
|
-
} catch (error) {
|
|
593
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
594
|
-
throw new Error(`Error downloading image: ${errorMessage}`);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
function generateVarId(prefix = "var") {
|
|
598
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
599
|
-
let result = "";
|
|
600
|
-
for (let i = 0; i < 6; i++) {
|
|
601
|
-
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
602
|
-
result += chars[randomIndex];
|
|
603
|
-
}
|
|
604
|
-
return `${prefix}_${result}`;
|
|
605
|
-
}
|
|
606
|
-
function generateCSSShorthand(values, {
|
|
607
|
-
ignoreZero = true,
|
|
608
|
-
suffix = "px"
|
|
609
|
-
} = {}) {
|
|
610
|
-
const { top, right, bottom, left } = values;
|
|
611
|
-
if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) {
|
|
612
|
-
return void 0;
|
|
613
|
-
}
|
|
614
|
-
if (top === right && right === bottom && bottom === left) {
|
|
615
|
-
return `${top}${suffix}`;
|
|
616
|
-
}
|
|
617
|
-
if (right === left) {
|
|
618
|
-
if (top === bottom) {
|
|
619
|
-
return `${top}${suffix} ${right}${suffix}`;
|
|
620
|
-
}
|
|
621
|
-
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`;
|
|
622
|
-
}
|
|
623
|
-
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`;
|
|
624
|
-
}
|
|
625
|
-
function isVisible(element) {
|
|
626
|
-
return element.visible ?? true;
|
|
627
|
-
}
|
|
628
|
-
function pixelRound(num) {
|
|
629
|
-
if (isNaN(num)) {
|
|
630
|
-
throw new TypeError(`Input must be a valid number`);
|
|
631
|
-
}
|
|
632
|
-
return Number(Number(num).toFixed(2));
|
|
633
|
-
}
|
|
634
|
-
const common = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
635
|
-
__proto__: null,
|
|
636
|
-
downloadFigmaImage,
|
|
637
|
-
generateCSSShorthand,
|
|
638
|
-
generateVarId,
|
|
639
|
-
isVisible,
|
|
640
|
-
pixelRound
|
|
641
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
642
663
|
function hasValue(key, obj, typeGuard) {
|
|
643
664
|
const isObject = typeof obj === "object" && obj !== null;
|
|
644
665
|
if (!isObject || !(key in obj)) return false;
|
|
@@ -1502,7 +1523,6 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1502
1523
|
const {
|
|
1503
1524
|
apiKey,
|
|
1504
1525
|
oauthToken,
|
|
1505
|
-
useOAuth = false,
|
|
1506
1526
|
outputFormat = "object",
|
|
1507
1527
|
depth,
|
|
1508
1528
|
downloadImages = false,
|
|
@@ -1525,9 +1545,8 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1525
1545
|
const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
|
|
1526
1546
|
const nodeId = nodeIdMatch ? nodeIdMatch[1].replace(/-/g, ":") : void 0;
|
|
1527
1547
|
const figmaService = new FigmaService({
|
|
1528
|
-
figmaApiKey: apiKey
|
|
1529
|
-
figmaOAuthToken: oauthToken
|
|
1530
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1548
|
+
figmaApiKey: apiKey,
|
|
1549
|
+
figmaOAuthToken: oauthToken
|
|
1531
1550
|
});
|
|
1532
1551
|
try {
|
|
1533
1552
|
Logger.log(
|
|
@@ -1622,7 +1641,6 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1622
1641
|
const {
|
|
1623
1642
|
apiKey,
|
|
1624
1643
|
oauthToken,
|
|
1625
|
-
useOAuth = false,
|
|
1626
1644
|
pngScale = 2,
|
|
1627
1645
|
localPath,
|
|
1628
1646
|
enableLogging = false,
|
|
@@ -1641,9 +1659,8 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1641
1659
|
}
|
|
1642
1660
|
const fileKey = urlMatch[2];
|
|
1643
1661
|
const figmaService = new FigmaService({
|
|
1644
|
-
figmaApiKey: apiKey
|
|
1645
|
-
figmaOAuthToken: oauthToken
|
|
1646
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1662
|
+
figmaApiKey: apiKey,
|
|
1663
|
+
figmaOAuthToken: oauthToken
|
|
1647
1664
|
});
|
|
1648
1665
|
try {
|
|
1649
1666
|
const processedNodes = nodes.map((node) => ({
|
|
@@ -1671,7 +1688,6 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1671
1688
|
const {
|
|
1672
1689
|
apiKey,
|
|
1673
1690
|
oauthToken,
|
|
1674
|
-
useOAuth = false,
|
|
1675
1691
|
pngScale = 2,
|
|
1676
1692
|
localPath,
|
|
1677
1693
|
fileName,
|
|
@@ -1709,9 +1725,8 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1709
1725
|
}
|
|
1710
1726
|
}
|
|
1711
1727
|
const figmaService = new FigmaService({
|
|
1712
|
-
figmaApiKey: apiKey
|
|
1713
|
-
figmaOAuthToken: oauthToken
|
|
1714
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1728
|
+
figmaApiKey: apiKey,
|
|
1729
|
+
figmaOAuthToken: oauthToken
|
|
1715
1730
|
});
|
|
1716
1731
|
try {
|
|
1717
1732
|
Logger.log(
|
package/dist/lib.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
export interface FigmaMetadataOptions {
|
|
2
|
-
/** The Figma API key (Personal Access Token) */
|
|
2
|
+
/** The Figma API key (Personal Access Token). Either apiKey or oauthToken must be provided. */
|
|
3
3
|
apiKey?: string;
|
|
4
|
-
/** The Figma OAuth Bearer token */
|
|
4
|
+
/** The Figma OAuth Bearer token. When provided, OAuth is used automatically. Preferred over apiKey if both are set. */
|
|
5
5
|
oauthToken?: string;
|
|
6
|
-
/** Whether to use OAuth instead of API key */
|
|
7
|
-
useOAuth?: boolean;
|
|
8
6
|
/** Output format for the metadata */
|
|
9
7
|
outputFormat?: "json" | "yaml" | "object";
|
|
10
8
|
/** Maximum depth to traverse the node tree */
|
|
@@ -72,12 +70,10 @@ export interface FigmaImageResult {
|
|
|
72
70
|
cssVariables?: string;
|
|
73
71
|
}
|
|
74
72
|
export interface FigmaFrameImageOptions {
|
|
75
|
-
/** The Figma API key (Personal Access Token) */
|
|
73
|
+
/** The Figma API key (Personal Access Token). Either apiKey or oauthToken must be provided. */
|
|
76
74
|
apiKey?: string;
|
|
77
|
-
/** The Figma OAuth Bearer token */
|
|
75
|
+
/** The Figma OAuth Bearer token. When provided, OAuth is used automatically. Preferred over apiKey if both are set. */
|
|
78
76
|
oauthToken?: string;
|
|
79
|
-
/** Whether to use OAuth instead of API key */
|
|
80
|
-
useOAuth?: boolean;
|
|
81
77
|
/** Export scale for PNG images (defaults to 2) */
|
|
82
78
|
pngScale?: number;
|
|
83
79
|
/** The absolute path to the directory where the image should be stored (optional if returnBuffer is true) */
|
package/dist/services/figma.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { GetFileResponse, GetFileNodesResponse } from "@figma/rest-api-spec";
|
|
2
2
|
import { type ImageProcessingResult } from "~/utils/image-processing.js";
|
|
3
3
|
export type FigmaAuthOptions = {
|
|
4
|
-
figmaApiKey
|
|
5
|
-
figmaOAuthToken
|
|
6
|
-
useOAuth: boolean;
|
|
4
|
+
figmaApiKey?: string;
|
|
5
|
+
figmaOAuthToken?: string;
|
|
7
6
|
};
|
|
8
7
|
type SvgOptions = {
|
|
9
8
|
outlineText: boolean;
|
|
@@ -15,7 +14,7 @@ export declare class FigmaService {
|
|
|
15
14
|
private readonly oauthToken;
|
|
16
15
|
private readonly useOAuth;
|
|
17
16
|
private readonly baseUrl;
|
|
18
|
-
constructor({ figmaApiKey, figmaOAuthToken
|
|
17
|
+
constructor({ figmaApiKey, figmaOAuthToken }: FigmaAuthOptions);
|
|
19
18
|
private getAuthHeaders;
|
|
20
19
|
/**
|
|
21
20
|
* Filters out null values from Figma image responses. This ensures we only work with valid image URLs.
|
package/dist/utils/common.d.ts
CHANGED
|
@@ -68,3 +68,10 @@ export declare function isVisible(element: {
|
|
|
68
68
|
* @throws TypeError If the input is not a valid number
|
|
69
69
|
*/
|
|
70
70
|
export declare function pixelRound(num: number): number;
|
|
71
|
+
/**
|
|
72
|
+
* Run a list of async tasks with a concurrency limit
|
|
73
|
+
* @param tasks - Array of functions that return a Promise
|
|
74
|
+
* @param limit - Maximum number of concurrent tasks
|
|
75
|
+
* @returns Promise resolving to array of results
|
|
76
|
+
*/
|
|
77
|
+
export declare function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: number): Promise<T[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-metadata-extractor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Extract metadata and download images from Figma files. A standalone library for accessing Figma design data and downloading frame images programmatically.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|