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 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 'figma-metadata-extractor';
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
- 'https://figma.com/file/ABC123/My-Design',
29
+ "https://figma.com/file/ABC123/My-Design",
30
30
  {
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
- }
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); // ![name](path)
@@ -47,17 +47,20 @@ metadata.nodes.forEach(node => {
47
47
  ### Get Images as Buffers (No Disk Write)
48
48
 
49
49
  ```typescript
50
- import { getFigmaMetadata, enrichMetadataWithImages } from 'figma-metadata-extractor';
51
- import fs from 'fs/promises';
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
- 'https://figma.com/file/ABC123/My-Design',
58
+ "https://figma.com/file/ABC123/My-Design",
56
59
  {
57
- apiKey: 'your-figma-api-key',
60
+ apiKey: "your-figma-api-key",
58
61
  downloadImages: true,
59
- returnBuffer: true // Get images as ArrayBuffer
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 'figma-metadata-extractor';
95
+ import { getFigmaMetadata } from "figma-metadata-extractor";
93
96
 
94
97
  // Extract metadata from a Figma file
95
98
  const metadata = await getFigmaMetadata(
96
- 'https://figma.com/file/ABC123/My-Design',
99
+ "https://figma.com/file/ABC123/My-Design",
97
100
  {
98
- apiKey: 'your-figma-api-key',
99
- outputFormat: 'object' // or 'json' or 'yaml'
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
- 'https://figma.com/file/ABC123/My-Design',
111
+ "https://figma.com/file/ABC123/My-Design",
109
112
  [
110
113
  {
111
- nodeId: '1234:5678',
112
- fileName: 'icon.svg'
114
+ nodeId: "1234:5678",
115
+ fileName: "icon.svg",
113
116
  },
114
117
  {
115
- nodeId: '9876:5432',
116
- fileName: 'hero-image.png'
117
- }
118
+ nodeId: "9876:5432",
119
+ fileName: "hero-image.png",
120
+ },
118
121
  ],
119
122
  {
120
- apiKey: 'your-figma-api-key',
121
- localPath: './assets/images'
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
- 'https://figma.com/file/ABC123/My-Design?node-id=1234-5678',
132
+ "https://figma.com/file/ABC123/My-Design?node-id=1234-5678",
130
133
  {
131
- apiKey: 'your-figma-api-key',
132
- localPath: './assets/frames',
133
- fileName: 'my-frame.png',
134
- format: 'png', // or 'svg'
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
- - `apiKey?: string` - Figma API key (Personal Access Token)
154
- - `oauthToken?: string` - Figma OAuth Bearer token
155
- - `useOAuth?: boolean` - Whether to use OAuth instead of API key
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; // Absolute path
181
- relativePath: string; // Relative path for code
182
- dimensions: { width, height };
183
- markdown: string; // ![name](path)
184
- html: string; // <img src="..." />
186
+ filePath: string; // Absolute path
187
+ relativePath: string; // Relative path for code
188
+ dimensions: {
189
+ width, height;
190
+ }
191
+ markdown: string; // ![name](path)
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: 'key',
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.writeFile(`./images/img-${i}.png`, Buffer.from(img.buffer))
244
- .then(() => `./images/img-${i}.png`)
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
- - `apiKey?: string` - Figma API key (Personal Access Token)
264
- - `oauthToken?: string` - Figma OAuth Bearer token
265
- - `useOAuth?: boolean` - Whether to use OAuth instead of API key
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
- 3. Set `useOAuth: true`
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 'figma-metadata-extractor';
330
+ import { downloadFigmaFrameImage } from "figma-metadata-extractor";
304
331
 
305
332
  // Copy this URL from Figma when viewing a frame
306
- const figmaUrl = 'https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678&t=xyz123';
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: 'your-figma-api-key',
311
- localPath: './downloads',
312
- fileName: 'my-frame.png',
313
- format: 'png',
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(`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`);
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 'figma-metadata-extractor';
356
+ import { downloadFigmaFrameImage } from "figma-metadata-extractor";
327
357
 
328
- const figmaUrl = 'https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678';
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: 'your-figma-api-key',
363
+ apiKey: "your-figma-api-key",
333
364
  returnBuffer: true,
334
- format: 'png'
365
+ format: "png",
335
366
  });
336
367
 
337
368
  console.log(`Buffer size: ${result.buffer.byteLength} bytes`);
338
- console.log(`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`);
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 'figma-metadata-extractor';
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
- 'https://figma.com/file/ABC123/My-Design',
384
+ "https://figma.com/file/ABC123/My-Design",
352
385
  [
353
- { nodeId: '1234:5678', fileName: 'frame1.png' },
354
- { nodeId: '9876:5432', fileName: 'frame2.svg' },
355
- { nodeId: '1111:2222', fileName: 'frame3.png' }
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: 'your-figma-api-key',
359
- localPath: './frames'
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 'figma-metadata-extractor';
400
+ import { downloadFigmaImages } from "figma-metadata-extractor";
368
401
 
369
402
  // Get multiple images as ArrayBuffers
370
403
  const results = await downloadFigmaImages(
371
- 'https://figma.com/file/ABC123/My-Design',
404
+ "https://figma.com/file/ABC123/My-Design",
372
405
  [
373
- { nodeId: '1234:5678', fileName: 'frame1.png' },
374
- { nodeId: '9876:5432', fileName: 'frame2.png' }
406
+ { nodeId: "1234:5678", fileName: "frame1.png" },
407
+ { nodeId: "9876:5432", fileName: "frame2.png" },
375
408
  ],
376
409
  {
377
- apiKey: 'your-figma-api-key',
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 'figma-metadata-extractor';
431
+ textExtractor,
432
+ } from "figma-metadata-extractor";
400
433
 
401
434
  // Use specific extractors
402
- const customResult = simplifyRawFigmaObject(
403
- rawFigmaResponse,
404
- [layoutExtractor, textExtractor]
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";
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, useOAuth }) {
274
- this.apiKey = figmaApiKey || "";
275
- this.oauthToken = figmaOAuthToken || "";
276
- this.useOAuth = !!useOAuth && !!this.oauthToken;
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 fillDownloads = imageFills.map(
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
- (promise) => promise !== null
537
+ (task) => task !== null
421
538
  );
422
- if (fillDownloads.length > 0) {
423
- downloadPromises.push(Promise.all(fillDownloads));
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 pngDownloads = pngNodes.map(
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
- (promise) => promise !== null
585
+ (task) => task !== null
469
586
  );
470
- if (pngDownloads.length > 0) {
471
- downloadPromises.push(Promise.all(pngDownloads));
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 svgDownloads = svgNodes.map(
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
- (promise) => promise !== null
628
+ (task) => task !== null
510
629
  );
511
- if (svgDownloads.length > 0) {
512
- downloadPromises.push(Promise.all(svgDownloads));
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, useOAuth }) {
272
- this.apiKey = figmaApiKey || "";
273
- this.oauthToken = figmaOAuthToken || "";
274
- this.useOAuth = !!useOAuth && !!this.oauthToken;
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 fillDownloads = imageFills.map(
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
- (promise) => promise !== null
535
+ (task) => task !== null
419
536
  );
420
- if (fillDownloads.length > 0) {
421
- downloadPromises.push(Promise.all(fillDownloads));
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 pngDownloads = pngNodes.map(
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
- (promise) => promise !== null
583
+ (task) => task !== null
467
584
  );
468
- if (pngDownloads.length > 0) {
469
- downloadPromises.push(Promise.all(pngDownloads));
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 svgDownloads = svgNodes.map(
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
- (promise) => promise !== null
626
+ (task) => task !== null
508
627
  );
509
- if (svgDownloads.length > 0) {
510
- downloadPromises.push(Promise.all(svgDownloads));
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) */
@@ -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: string;
5
- figmaOAuthToken: string;
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, useOAuth }: FigmaAuthOptions);
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.
@@ -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.13",
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",