figma-metadata-extractor 1.0.14 → 1.0.16
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/index.cjs +95 -164
- package/dist/index.js +95 -164
- package/dist/lib.d.ts +4 -8
- package/dist/services/figma.d.ts +3 -4
- 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
|
package/dist/index.cjs
CHANGED
|
@@ -386,10 +386,10 @@ class FigmaService {
|
|
|
386
386
|
oauthToken;
|
|
387
387
|
useOAuth;
|
|
388
388
|
baseUrl = "https://api.figma.com/v1";
|
|
389
|
-
constructor({ figmaApiKey, figmaOAuthToken
|
|
390
|
-
this.apiKey = figmaApiKey
|
|
391
|
-
this.oauthToken = figmaOAuthToken
|
|
392
|
-
this.useOAuth = !!
|
|
389
|
+
constructor({ figmaApiKey, figmaOAuthToken }) {
|
|
390
|
+
this.apiKey = figmaApiKey ?? "";
|
|
391
|
+
this.oauthToken = figmaOAuthToken ?? "";
|
|
392
|
+
this.useOAuth = !!this.oauthToken;
|
|
393
393
|
}
|
|
394
394
|
getAuthHeaders() {
|
|
395
395
|
if (this.useOAuth) {
|
|
@@ -499,143 +499,67 @@ class FigmaService {
|
|
|
499
499
|
);
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
returnBuffer
|
|
534
|
-
);
|
|
502
|
+
const imageRefs = [
|
|
503
|
+
...new Set(items.map((i) => i.imageRef).filter((r) => !!r))
|
|
504
|
+
];
|
|
505
|
+
const pngNodeIds = items.filter(
|
|
506
|
+
(i) => !i.imageRef && i.nodeId && !i.fileName.toLowerCase().endsWith(".svg")
|
|
507
|
+
).map((i) => i.nodeId);
|
|
508
|
+
const svgNodeIds = items.filter(
|
|
509
|
+
(i) => !i.imageRef && i.nodeId && i.fileName.toLowerCase().endsWith(".svg")
|
|
510
|
+
).map((i) => i.nodeId);
|
|
511
|
+
const [fillUrls, pngUrls, svgUrls] = await Promise.all([
|
|
512
|
+
imageRefs.length > 0 ? this.getImageFillUrls(fileKey) : Promise.resolve({}),
|
|
513
|
+
pngNodeIds.length > 0 ? this.getNodeRenderUrls(fileKey, pngNodeIds, "png", { pngScale }) : Promise.resolve({}),
|
|
514
|
+
svgNodeIds.length > 0 ? this.getNodeRenderUrls(fileKey, svgNodeIds, "svg", { svgOptions }) : Promise.resolve({})
|
|
515
|
+
]);
|
|
516
|
+
const tasks = items.map((item) => {
|
|
517
|
+
const {
|
|
518
|
+
imageRef,
|
|
519
|
+
nodeId,
|
|
520
|
+
fileName,
|
|
521
|
+
needsCropping,
|
|
522
|
+
cropTransform,
|
|
523
|
+
requiresImageDimensions
|
|
524
|
+
} = item;
|
|
525
|
+
let imageUrl;
|
|
526
|
+
if (imageRef) {
|
|
527
|
+
imageUrl = fillUrls[imageRef];
|
|
528
|
+
} else if (nodeId) {
|
|
529
|
+
if (fileName.toLowerCase().endsWith(".svg")) {
|
|
530
|
+
imageUrl = svgUrls[nodeId];
|
|
531
|
+
} else {
|
|
532
|
+
imageUrl = pngUrls[nodeId];
|
|
535
533
|
}
|
|
536
|
-
).filter(
|
|
537
|
-
(task) => task !== null
|
|
538
|
-
);
|
|
539
|
-
if (fillTasks.length > 0) {
|
|
540
|
-
downloadPromises.push(runWithConcurrency(fillTasks, CONCURRENCY_LIMIT));
|
|
541
534
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
);
|
|
547
|
-
const svgNodes = renderNodes.filter(
|
|
548
|
-
(node) => node.fileName.toLowerCase().endsWith(".svg")
|
|
549
|
-
);
|
|
550
|
-
if (pngNodes.length > 0) {
|
|
551
|
-
const pngUrls = await this.getNodeRenderUrls(
|
|
552
|
-
fileKey,
|
|
553
|
-
pngNodes.map((n) => n.nodeId),
|
|
554
|
-
"png",
|
|
555
|
-
{ pngScale }
|
|
556
|
-
);
|
|
557
|
-
const pngTasks = pngNodes.map(
|
|
558
|
-
({
|
|
559
|
-
nodeId,
|
|
560
|
-
fileName,
|
|
561
|
-
needsCropping,
|
|
562
|
-
cropTransform,
|
|
563
|
-
requiresImageDimensions
|
|
564
|
-
}) => {
|
|
565
|
-
const imageUrl = pngUrls[nodeId];
|
|
566
|
-
if (!imageUrl) {
|
|
567
|
-
Logger.log(
|
|
568
|
-
`Skipping PNG render with missing URL for nodeId: ${nodeId}`
|
|
569
|
-
);
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
return () => downloadAndProcessImage(
|
|
573
|
-
fileName,
|
|
574
|
-
resolvedPath,
|
|
575
|
-
imageUrl,
|
|
576
|
-
needsCropping,
|
|
577
|
-
cropTransform,
|
|
578
|
-
requiresImageDimensions,
|
|
579
|
-
returnBuffer
|
|
580
|
-
).then(
|
|
581
|
-
(result) => ({ ...result, nodeId })
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
).filter(
|
|
585
|
-
(task) => task !== null
|
|
586
|
-
);
|
|
587
|
-
if (pngTasks.length > 0) {
|
|
588
|
-
downloadPromises.push(
|
|
589
|
-
runWithConcurrency(pngTasks, CONCURRENCY_LIMIT)
|
|
535
|
+
if (!imageUrl) {
|
|
536
|
+
return () => {
|
|
537
|
+
Logger.log(
|
|
538
|
+
`Skipping missing image for ${imageRef ? `imageRef: ${imageRef}` : `nodeId: ${nodeId}`}`
|
|
590
539
|
);
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
if (svgNodes.length > 0) {
|
|
594
|
-
const svgUrls = await this.getNodeRenderUrls(
|
|
595
|
-
fileKey,
|
|
596
|
-
svgNodes.map((n) => n.nodeId),
|
|
597
|
-
"svg",
|
|
598
|
-
{ svgOptions }
|
|
599
|
-
);
|
|
600
|
-
const svgTasks = svgNodes.map(
|
|
601
|
-
({
|
|
540
|
+
return Promise.resolve({
|
|
602
541
|
nodeId,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
})
|
|
608
|
-
|
|
609
|
-
if (!imageUrl) {
|
|
610
|
-
Logger.log(
|
|
611
|
-
`Skipping SVG render with missing URL for nodeId: ${nodeId}`
|
|
612
|
-
);
|
|
613
|
-
return null;
|
|
614
|
-
}
|
|
615
|
-
return () => downloadAndProcessImage(
|
|
616
|
-
fileName,
|
|
617
|
-
resolvedPath,
|
|
618
|
-
imageUrl,
|
|
619
|
-
needsCropping,
|
|
620
|
-
cropTransform,
|
|
621
|
-
requiresImageDimensions,
|
|
622
|
-
returnBuffer
|
|
623
|
-
).then(
|
|
624
|
-
(result) => ({ ...result, nodeId })
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
).filter(
|
|
628
|
-
(task) => task !== null
|
|
629
|
-
);
|
|
630
|
-
if (svgTasks.length > 0) {
|
|
631
|
-
downloadPromises.push(
|
|
632
|
-
runWithConcurrency(svgTasks, CONCURRENCY_LIMIT)
|
|
633
|
-
);
|
|
634
|
-
}
|
|
542
|
+
originalDimensions: { width: 0, height: 0 },
|
|
543
|
+
finalDimensions: { width: 0, height: 0 },
|
|
544
|
+
wasCropped: false,
|
|
545
|
+
processingLog: ["Skipped: URL not found"]
|
|
546
|
+
});
|
|
547
|
+
};
|
|
635
548
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
549
|
+
return async () => {
|
|
550
|
+
const result = await downloadAndProcessImage(
|
|
551
|
+
fileName,
|
|
552
|
+
resolvedPath,
|
|
553
|
+
imageUrl,
|
|
554
|
+
needsCropping,
|
|
555
|
+
cropTransform,
|
|
556
|
+
requiresImageDimensions,
|
|
557
|
+
returnBuffer
|
|
558
|
+
);
|
|
559
|
+
return { ...result, nodeId };
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
return runWithConcurrency(tasks, CONCURRENCY_LIMIT);
|
|
639
563
|
}
|
|
640
564
|
/**
|
|
641
565
|
* Get raw Figma API response for a file (for use with flexible extractors)
|
|
@@ -1525,7 +1449,6 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1525
1449
|
const {
|
|
1526
1450
|
apiKey,
|
|
1527
1451
|
oauthToken,
|
|
1528
|
-
useOAuth = false,
|
|
1529
1452
|
outputFormat = "object",
|
|
1530
1453
|
depth,
|
|
1531
1454
|
downloadImages = false,
|
|
@@ -1548,9 +1471,8 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1548
1471
|
const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
|
|
1549
1472
|
const nodeId = nodeIdMatch ? nodeIdMatch[1].replace(/-/g, ":") : void 0;
|
|
1550
1473
|
const figmaService = new FigmaService({
|
|
1551
|
-
figmaApiKey: apiKey
|
|
1552
|
-
figmaOAuthToken: oauthToken
|
|
1553
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1474
|
+
figmaApiKey: apiKey,
|
|
1475
|
+
figmaOAuthToken: oauthToken
|
|
1554
1476
|
});
|
|
1555
1477
|
try {
|
|
1556
1478
|
Logger.log(
|
|
@@ -1598,7 +1520,11 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1598
1520
|
if (imageAssets.length > 0) {
|
|
1599
1521
|
const imageNodes = imageAssets.map((asset) => ({
|
|
1600
1522
|
nodeId: asset.id,
|
|
1601
|
-
|
|
1523
|
+
imageRef: asset.imageRef,
|
|
1524
|
+
fileName: sanitizeFileName(asset.name) + (asset.filenameSuffix ? `-${asset.filenameSuffix}` : "") + `.${imageFormat}`,
|
|
1525
|
+
needsCropping: asset.needsCropping,
|
|
1526
|
+
cropTransform: asset.cropTransform,
|
|
1527
|
+
requiresImageDimensions: asset.requiresImageDimensions
|
|
1602
1528
|
}));
|
|
1603
1529
|
const downloadResults = await figmaService.downloadImages(
|
|
1604
1530
|
fileKey,
|
|
@@ -1645,7 +1571,6 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1645
1571
|
const {
|
|
1646
1572
|
apiKey,
|
|
1647
1573
|
oauthToken,
|
|
1648
|
-
useOAuth = false,
|
|
1649
1574
|
pngScale = 2,
|
|
1650
1575
|
localPath,
|
|
1651
1576
|
enableLogging = false,
|
|
@@ -1664,9 +1589,8 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1664
1589
|
}
|
|
1665
1590
|
const fileKey = urlMatch[2];
|
|
1666
1591
|
const figmaService = new FigmaService({
|
|
1667
|
-
figmaApiKey: apiKey
|
|
1668
|
-
figmaOAuthToken: oauthToken
|
|
1669
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1592
|
+
figmaApiKey: apiKey,
|
|
1593
|
+
figmaOAuthToken: oauthToken
|
|
1670
1594
|
});
|
|
1671
1595
|
try {
|
|
1672
1596
|
const processedNodes = nodes.map((node) => ({
|
|
@@ -1694,7 +1618,6 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1694
1618
|
const {
|
|
1695
1619
|
apiKey,
|
|
1696
1620
|
oauthToken,
|
|
1697
|
-
useOAuth = false,
|
|
1698
1621
|
pngScale = 2,
|
|
1699
1622
|
localPath,
|
|
1700
1623
|
fileName,
|
|
@@ -1732,9 +1655,8 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1732
1655
|
}
|
|
1733
1656
|
}
|
|
1734
1657
|
const figmaService = new FigmaService({
|
|
1735
|
-
figmaApiKey: apiKey
|
|
1736
|
-
figmaOAuthToken: oauthToken
|
|
1737
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1658
|
+
figmaApiKey: apiKey,
|
|
1659
|
+
figmaOAuthToken: oauthToken
|
|
1738
1660
|
});
|
|
1739
1661
|
try {
|
|
1740
1662
|
Logger.log(
|
|
@@ -1837,28 +1759,37 @@ function sanitizeFileName(name) {
|
|
|
1837
1759
|
return name.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
1838
1760
|
}
|
|
1839
1761
|
function findImageAssets(nodes, globalVars) {
|
|
1840
|
-
const
|
|
1762
|
+
const assets = [];
|
|
1841
1763
|
function traverse(node) {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1764
|
+
if (node.type === "IMAGE-SVG") {
|
|
1765
|
+
assets.push({
|
|
1766
|
+
...node,
|
|
1767
|
+
id: node.id,
|
|
1768
|
+
name: node.name,
|
|
1769
|
+
type: "render"
|
|
1770
|
+
});
|
|
1771
|
+
} else if (node.fills && typeof node.fills === "string") {
|
|
1772
|
+
const fillData = globalVars?.styles?.[node.fills];
|
|
1773
|
+
if (Array.isArray(fillData)) {
|
|
1774
|
+
const imageFill = fillData.find((f) => f.type === "IMAGE");
|
|
1775
|
+
if (imageFill) {
|
|
1776
|
+
assets.push({
|
|
1777
|
+
...node,
|
|
1778
|
+
id: node.id,
|
|
1779
|
+
name: node.name,
|
|
1780
|
+
type: "fill",
|
|
1781
|
+
imageRef: imageFill.imageRef,
|
|
1782
|
+
...imageFill.imageDownloadArguments
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1845
1786
|
}
|
|
1846
1787
|
if (node.children && Array.isArray(node.children)) {
|
|
1847
1788
|
node.children.forEach(traverse);
|
|
1848
1789
|
}
|
|
1849
1790
|
}
|
|
1850
1791
|
nodes.forEach(traverse);
|
|
1851
|
-
return
|
|
1852
|
-
}
|
|
1853
|
-
function hasImageFill(node, globalVars) {
|
|
1854
|
-
if (!node.fills || typeof node.fills !== "string") {
|
|
1855
|
-
return false;
|
|
1856
|
-
}
|
|
1857
|
-
const fillData = globalVars?.styles?.[node.fills];
|
|
1858
|
-
if (!fillData || !Array.isArray(fillData)) {
|
|
1859
|
-
return false;
|
|
1860
|
-
}
|
|
1861
|
-
return fillData.some((fill) => fill?.type === "IMAGE");
|
|
1792
|
+
return assets;
|
|
1862
1793
|
}
|
|
1863
1794
|
function enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths = true, localPath) {
|
|
1864
1795
|
const imageMap = /* @__PURE__ */ new Map();
|
package/dist/index.js
CHANGED
|
@@ -384,10 +384,10 @@ class FigmaService {
|
|
|
384
384
|
oauthToken;
|
|
385
385
|
useOAuth;
|
|
386
386
|
baseUrl = "https://api.figma.com/v1";
|
|
387
|
-
constructor({ figmaApiKey, figmaOAuthToken
|
|
388
|
-
this.apiKey = figmaApiKey
|
|
389
|
-
this.oauthToken = figmaOAuthToken
|
|
390
|
-
this.useOAuth = !!
|
|
387
|
+
constructor({ figmaApiKey, figmaOAuthToken }) {
|
|
388
|
+
this.apiKey = figmaApiKey ?? "";
|
|
389
|
+
this.oauthToken = figmaOAuthToken ?? "";
|
|
390
|
+
this.useOAuth = !!this.oauthToken;
|
|
391
391
|
}
|
|
392
392
|
getAuthHeaders() {
|
|
393
393
|
if (this.useOAuth) {
|
|
@@ -497,143 +497,67 @@ class FigmaService {
|
|
|
497
497
|
);
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
returnBuffer
|
|
532
|
-
);
|
|
500
|
+
const imageRefs = [
|
|
501
|
+
...new Set(items.map((i) => i.imageRef).filter((r) => !!r))
|
|
502
|
+
];
|
|
503
|
+
const pngNodeIds = items.filter(
|
|
504
|
+
(i) => !i.imageRef && i.nodeId && !i.fileName.toLowerCase().endsWith(".svg")
|
|
505
|
+
).map((i) => i.nodeId);
|
|
506
|
+
const svgNodeIds = items.filter(
|
|
507
|
+
(i) => !i.imageRef && i.nodeId && i.fileName.toLowerCase().endsWith(".svg")
|
|
508
|
+
).map((i) => i.nodeId);
|
|
509
|
+
const [fillUrls, pngUrls, svgUrls] = await Promise.all([
|
|
510
|
+
imageRefs.length > 0 ? this.getImageFillUrls(fileKey) : Promise.resolve({}),
|
|
511
|
+
pngNodeIds.length > 0 ? this.getNodeRenderUrls(fileKey, pngNodeIds, "png", { pngScale }) : Promise.resolve({}),
|
|
512
|
+
svgNodeIds.length > 0 ? this.getNodeRenderUrls(fileKey, svgNodeIds, "svg", { svgOptions }) : Promise.resolve({})
|
|
513
|
+
]);
|
|
514
|
+
const tasks = items.map((item) => {
|
|
515
|
+
const {
|
|
516
|
+
imageRef,
|
|
517
|
+
nodeId,
|
|
518
|
+
fileName,
|
|
519
|
+
needsCropping,
|
|
520
|
+
cropTransform,
|
|
521
|
+
requiresImageDimensions
|
|
522
|
+
} = item;
|
|
523
|
+
let imageUrl;
|
|
524
|
+
if (imageRef) {
|
|
525
|
+
imageUrl = fillUrls[imageRef];
|
|
526
|
+
} else if (nodeId) {
|
|
527
|
+
if (fileName.toLowerCase().endsWith(".svg")) {
|
|
528
|
+
imageUrl = svgUrls[nodeId];
|
|
529
|
+
} else {
|
|
530
|
+
imageUrl = pngUrls[nodeId];
|
|
533
531
|
}
|
|
534
|
-
).filter(
|
|
535
|
-
(task) => task !== null
|
|
536
|
-
);
|
|
537
|
-
if (fillTasks.length > 0) {
|
|
538
|
-
downloadPromises.push(runWithConcurrency(fillTasks, CONCURRENCY_LIMIT));
|
|
539
532
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
);
|
|
545
|
-
const svgNodes = renderNodes.filter(
|
|
546
|
-
(node) => node.fileName.toLowerCase().endsWith(".svg")
|
|
547
|
-
);
|
|
548
|
-
if (pngNodes.length > 0) {
|
|
549
|
-
const pngUrls = await this.getNodeRenderUrls(
|
|
550
|
-
fileKey,
|
|
551
|
-
pngNodes.map((n) => n.nodeId),
|
|
552
|
-
"png",
|
|
553
|
-
{ pngScale }
|
|
554
|
-
);
|
|
555
|
-
const pngTasks = pngNodes.map(
|
|
556
|
-
({
|
|
557
|
-
nodeId,
|
|
558
|
-
fileName,
|
|
559
|
-
needsCropping,
|
|
560
|
-
cropTransform,
|
|
561
|
-
requiresImageDimensions
|
|
562
|
-
}) => {
|
|
563
|
-
const imageUrl = pngUrls[nodeId];
|
|
564
|
-
if (!imageUrl) {
|
|
565
|
-
Logger.log(
|
|
566
|
-
`Skipping PNG render with missing URL for nodeId: ${nodeId}`
|
|
567
|
-
);
|
|
568
|
-
return null;
|
|
569
|
-
}
|
|
570
|
-
return () => downloadAndProcessImage(
|
|
571
|
-
fileName,
|
|
572
|
-
resolvedPath,
|
|
573
|
-
imageUrl,
|
|
574
|
-
needsCropping,
|
|
575
|
-
cropTransform,
|
|
576
|
-
requiresImageDimensions,
|
|
577
|
-
returnBuffer
|
|
578
|
-
).then(
|
|
579
|
-
(result) => ({ ...result, nodeId })
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
).filter(
|
|
583
|
-
(task) => task !== null
|
|
584
|
-
);
|
|
585
|
-
if (pngTasks.length > 0) {
|
|
586
|
-
downloadPromises.push(
|
|
587
|
-
runWithConcurrency(pngTasks, CONCURRENCY_LIMIT)
|
|
533
|
+
if (!imageUrl) {
|
|
534
|
+
return () => {
|
|
535
|
+
Logger.log(
|
|
536
|
+
`Skipping missing image for ${imageRef ? `imageRef: ${imageRef}` : `nodeId: ${nodeId}`}`
|
|
588
537
|
);
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
if (svgNodes.length > 0) {
|
|
592
|
-
const svgUrls = await this.getNodeRenderUrls(
|
|
593
|
-
fileKey,
|
|
594
|
-
svgNodes.map((n) => n.nodeId),
|
|
595
|
-
"svg",
|
|
596
|
-
{ svgOptions }
|
|
597
|
-
);
|
|
598
|
-
const svgTasks = svgNodes.map(
|
|
599
|
-
({
|
|
538
|
+
return Promise.resolve({
|
|
600
539
|
nodeId,
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
if (!imageUrl) {
|
|
608
|
-
Logger.log(
|
|
609
|
-
`Skipping SVG render with missing URL for nodeId: ${nodeId}`
|
|
610
|
-
);
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
return () => downloadAndProcessImage(
|
|
614
|
-
fileName,
|
|
615
|
-
resolvedPath,
|
|
616
|
-
imageUrl,
|
|
617
|
-
needsCropping,
|
|
618
|
-
cropTransform,
|
|
619
|
-
requiresImageDimensions,
|
|
620
|
-
returnBuffer
|
|
621
|
-
).then(
|
|
622
|
-
(result) => ({ ...result, nodeId })
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
).filter(
|
|
626
|
-
(task) => task !== null
|
|
627
|
-
);
|
|
628
|
-
if (svgTasks.length > 0) {
|
|
629
|
-
downloadPromises.push(
|
|
630
|
-
runWithConcurrency(svgTasks, CONCURRENCY_LIMIT)
|
|
631
|
-
);
|
|
632
|
-
}
|
|
540
|
+
originalDimensions: { width: 0, height: 0 },
|
|
541
|
+
finalDimensions: { width: 0, height: 0 },
|
|
542
|
+
wasCropped: false,
|
|
543
|
+
processingLog: ["Skipped: URL not found"]
|
|
544
|
+
});
|
|
545
|
+
};
|
|
633
546
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
547
|
+
return async () => {
|
|
548
|
+
const result = await downloadAndProcessImage(
|
|
549
|
+
fileName,
|
|
550
|
+
resolvedPath,
|
|
551
|
+
imageUrl,
|
|
552
|
+
needsCropping,
|
|
553
|
+
cropTransform,
|
|
554
|
+
requiresImageDimensions,
|
|
555
|
+
returnBuffer
|
|
556
|
+
);
|
|
557
|
+
return { ...result, nodeId };
|
|
558
|
+
};
|
|
559
|
+
});
|
|
560
|
+
return runWithConcurrency(tasks, CONCURRENCY_LIMIT);
|
|
637
561
|
}
|
|
638
562
|
/**
|
|
639
563
|
* Get raw Figma API response for a file (for use with flexible extractors)
|
|
@@ -1523,7 +1447,6 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1523
1447
|
const {
|
|
1524
1448
|
apiKey,
|
|
1525
1449
|
oauthToken,
|
|
1526
|
-
useOAuth = false,
|
|
1527
1450
|
outputFormat = "object",
|
|
1528
1451
|
depth,
|
|
1529
1452
|
downloadImages = false,
|
|
@@ -1546,9 +1469,8 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1546
1469
|
const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
|
|
1547
1470
|
const nodeId = nodeIdMatch ? nodeIdMatch[1].replace(/-/g, ":") : void 0;
|
|
1548
1471
|
const figmaService = new FigmaService({
|
|
1549
|
-
figmaApiKey: apiKey
|
|
1550
|
-
figmaOAuthToken: oauthToken
|
|
1551
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1472
|
+
figmaApiKey: apiKey,
|
|
1473
|
+
figmaOAuthToken: oauthToken
|
|
1552
1474
|
});
|
|
1553
1475
|
try {
|
|
1554
1476
|
Logger.log(
|
|
@@ -1596,7 +1518,11 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1596
1518
|
if (imageAssets.length > 0) {
|
|
1597
1519
|
const imageNodes = imageAssets.map((asset) => ({
|
|
1598
1520
|
nodeId: asset.id,
|
|
1599
|
-
|
|
1521
|
+
imageRef: asset.imageRef,
|
|
1522
|
+
fileName: sanitizeFileName(asset.name) + (asset.filenameSuffix ? `-${asset.filenameSuffix}` : "") + `.${imageFormat}`,
|
|
1523
|
+
needsCropping: asset.needsCropping,
|
|
1524
|
+
cropTransform: asset.cropTransform,
|
|
1525
|
+
requiresImageDimensions: asset.requiresImageDimensions
|
|
1600
1526
|
}));
|
|
1601
1527
|
const downloadResults = await figmaService.downloadImages(
|
|
1602
1528
|
fileKey,
|
|
@@ -1643,7 +1569,6 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1643
1569
|
const {
|
|
1644
1570
|
apiKey,
|
|
1645
1571
|
oauthToken,
|
|
1646
|
-
useOAuth = false,
|
|
1647
1572
|
pngScale = 2,
|
|
1648
1573
|
localPath,
|
|
1649
1574
|
enableLogging = false,
|
|
@@ -1662,9 +1587,8 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1662
1587
|
}
|
|
1663
1588
|
const fileKey = urlMatch[2];
|
|
1664
1589
|
const figmaService = new FigmaService({
|
|
1665
|
-
figmaApiKey: apiKey
|
|
1666
|
-
figmaOAuthToken: oauthToken
|
|
1667
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1590
|
+
figmaApiKey: apiKey,
|
|
1591
|
+
figmaOAuthToken: oauthToken
|
|
1668
1592
|
});
|
|
1669
1593
|
try {
|
|
1670
1594
|
const processedNodes = nodes.map((node) => ({
|
|
@@ -1692,7 +1616,6 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1692
1616
|
const {
|
|
1693
1617
|
apiKey,
|
|
1694
1618
|
oauthToken,
|
|
1695
|
-
useOAuth = false,
|
|
1696
1619
|
pngScale = 2,
|
|
1697
1620
|
localPath,
|
|
1698
1621
|
fileName,
|
|
@@ -1730,9 +1653,8 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1730
1653
|
}
|
|
1731
1654
|
}
|
|
1732
1655
|
const figmaService = new FigmaService({
|
|
1733
|
-
figmaApiKey: apiKey
|
|
1734
|
-
figmaOAuthToken: oauthToken
|
|
1735
|
-
useOAuth: useOAuth && !!oauthToken
|
|
1656
|
+
figmaApiKey: apiKey,
|
|
1657
|
+
figmaOAuthToken: oauthToken
|
|
1736
1658
|
});
|
|
1737
1659
|
try {
|
|
1738
1660
|
Logger.log(
|
|
@@ -1835,28 +1757,37 @@ function sanitizeFileName(name) {
|
|
|
1835
1757
|
return name.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
1836
1758
|
}
|
|
1837
1759
|
function findImageAssets(nodes, globalVars) {
|
|
1838
|
-
const
|
|
1760
|
+
const assets = [];
|
|
1839
1761
|
function traverse(node) {
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1762
|
+
if (node.type === "IMAGE-SVG") {
|
|
1763
|
+
assets.push({
|
|
1764
|
+
...node,
|
|
1765
|
+
id: node.id,
|
|
1766
|
+
name: node.name,
|
|
1767
|
+
type: "render"
|
|
1768
|
+
});
|
|
1769
|
+
} else if (node.fills && typeof node.fills === "string") {
|
|
1770
|
+
const fillData = globalVars?.styles?.[node.fills];
|
|
1771
|
+
if (Array.isArray(fillData)) {
|
|
1772
|
+
const imageFill = fillData.find((f) => f.type === "IMAGE");
|
|
1773
|
+
if (imageFill) {
|
|
1774
|
+
assets.push({
|
|
1775
|
+
...node,
|
|
1776
|
+
id: node.id,
|
|
1777
|
+
name: node.name,
|
|
1778
|
+
type: "fill",
|
|
1779
|
+
imageRef: imageFill.imageRef,
|
|
1780
|
+
...imageFill.imageDownloadArguments
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1843
1784
|
}
|
|
1844
1785
|
if (node.children && Array.isArray(node.children)) {
|
|
1845
1786
|
node.children.forEach(traverse);
|
|
1846
1787
|
}
|
|
1847
1788
|
}
|
|
1848
1789
|
nodes.forEach(traverse);
|
|
1849
|
-
return
|
|
1850
|
-
}
|
|
1851
|
-
function hasImageFill(node, globalVars) {
|
|
1852
|
-
if (!node.fills || typeof node.fills !== "string") {
|
|
1853
|
-
return false;
|
|
1854
|
-
}
|
|
1855
|
-
const fillData = globalVars?.styles?.[node.fills];
|
|
1856
|
-
if (!fillData || !Array.isArray(fillData)) {
|
|
1857
|
-
return false;
|
|
1858
|
-
}
|
|
1859
|
-
return fillData.some((fill) => fill?.type === "IMAGE");
|
|
1790
|
+
return assets;
|
|
1860
1791
|
}
|
|
1861
1792
|
function enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths = true, localPath) {
|
|
1862
1793
|
const imageMap = /* @__PURE__ */ new Map();
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-metadata-extractor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
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",
|