figma-metadata-extractor 1.0.12 → 1.0.14

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.
@@ -0,0 +1 @@
1
+ "use strict";
package/dist/index.cjs CHANGED
@@ -265,6 +265,122 @@ 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;
@@ -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;
@@ -1537,14 +1558,25 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1537
1558
  );
1538
1559
  let rawApiResponse;
1539
1560
  if (nodeId) {
1540
- rawApiResponse = await figmaService.getRawNode(fileKey, nodeId, depth || void 0);
1561
+ rawApiResponse = await figmaService.getRawNode(
1562
+ fileKey,
1563
+ nodeId,
1564
+ depth || void 0
1565
+ );
1541
1566
  } else {
1542
- rawApiResponse = await figmaService.getRawFile(fileKey, depth || void 0);
1567
+ rawApiResponse = await figmaService.getRawFile(
1568
+ fileKey,
1569
+ depth || void 0
1570
+ );
1543
1571
  }
1544
- const simplifiedDesign = simplifyRawFigmaObject(rawApiResponse, allExtractors, {
1545
- maxDepth: depth || void 0,
1546
- afterChildren: collapseSvgContainers
1547
- });
1572
+ const simplifiedDesign = simplifyRawFigmaObject(
1573
+ rawApiResponse,
1574
+ allExtractors,
1575
+ {
1576
+ maxDepth: depth || void 0,
1577
+ afterChildren: collapseSvgContainers
1578
+ }
1579
+ );
1548
1580
  Logger.log(
1549
1581
  `Successfully extracted data: ${simplifiedDesign.nodes.length} nodes, ${Object.keys(simplifiedDesign.globalVars?.styles || {}).length} styles`
1550
1582
  );
@@ -1556,7 +1588,9 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1556
1588
  };
1557
1589
  if (downloadImages) {
1558
1590
  if (!returnBuffer && !localPath) {
1559
- throw new Error("localPath is required when downloadImages is true and returnBuffer is false");
1591
+ throw new Error(
1592
+ "localPath is required when downloadImages is true and returnBuffer is false"
1593
+ );
1560
1594
  }
1561
1595
  Logger.log("Discovering and downloading image assets...");
1562
1596
  const imageAssets = findImageAssets(nodes, globalVars);
@@ -1577,10 +1611,20 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1577
1611
  );
1578
1612
  if (returnBuffer) {
1579
1613
  result.images = downloadResults;
1580
- Logger.log(`Successfully downloaded ${downloadResults.length} images as buffers`);
1614
+ Logger.log(
1615
+ `Successfully downloaded ${downloadResults.length} images as buffers`
1616
+ );
1581
1617
  } else {
1582
- result.nodes = enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths, localPath);
1583
- Logger.log(`Successfully downloaded and enriched ${downloadResults.length} images`);
1618
+ result.nodes = enrichNodesWithImages(
1619
+ nodes,
1620
+ imageAssets,
1621
+ downloadResults,
1622
+ useRelativePaths,
1623
+ localPath
1624
+ );
1625
+ Logger.log(
1626
+ `Successfully downloaded and enriched ${downloadResults.length} images`
1627
+ );
1584
1628
  }
1585
1629
  }
1586
1630
  }
@@ -1598,7 +1642,15 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1598
1642
  }
1599
1643
  }
1600
1644
  async function downloadFigmaImages(figmaUrl, nodes, options) {
1601
- const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false, returnBuffer = false } = options;
1645
+ const {
1646
+ apiKey,
1647
+ oauthToken,
1648
+ useOAuth = false,
1649
+ pngScale = 2,
1650
+ localPath,
1651
+ enableLogging = false,
1652
+ returnBuffer = false
1653
+ } = options;
1602
1654
  Logger.enableLogging = enableLogging;
1603
1655
  if (!apiKey && !oauthToken) {
1604
1656
  throw new Error("Either apiKey or oauthToken is required");
@@ -1621,14 +1673,21 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
1621
1673
  ...node,
1622
1674
  nodeId: node.nodeId.replace(/-/g, ":")
1623
1675
  }));
1624
- const results = await figmaService.downloadImages(fileKey, localPath || "", processedNodes, {
1625
- pngScale,
1626
- returnBuffer
1627
- });
1676
+ const results = await figmaService.downloadImages(
1677
+ fileKey,
1678
+ localPath || "",
1679
+ processedNodes,
1680
+ {
1681
+ pngScale,
1682
+ returnBuffer
1683
+ }
1684
+ );
1628
1685
  return results;
1629
1686
  } catch (error) {
1630
1687
  Logger.error(`Error downloading images from ${fileKey}:`, error);
1631
- throw new Error(`Failed to download images: ${error instanceof Error ? error.message : String(error)}`);
1688
+ throw new Error(
1689
+ `Failed to download images: ${error instanceof Error ? error.message : String(error)}`
1690
+ );
1632
1691
  }
1633
1692
  }
1634
1693
  async function downloadFigmaFrameImage(figmaUrl, options) {
@@ -1648,7 +1707,9 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1648
1707
  throw new Error("Either apiKey or oauthToken is required");
1649
1708
  }
1650
1709
  if (!returnBuffer && (!localPath || !fileName)) {
1651
- throw new Error("localPath and fileName are required when returnBuffer is false");
1710
+ throw new Error(
1711
+ "localPath and fileName are required when returnBuffer is false"
1712
+ );
1652
1713
  }
1653
1714
  const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1654
1715
  if (!urlMatch) {
@@ -1657,13 +1718,17 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1657
1718
  const fileKey = urlMatch[2];
1658
1719
  const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
1659
1720
  if (!nodeIdMatch) {
1660
- throw new Error("No frame node-id found in URL. Please provide a Figma URL with a node-id parameter (e.g., ?node-id=123-456)");
1721
+ throw new Error(
1722
+ "No frame node-id found in URL. Please provide a Figma URL with a node-id parameter (e.g., ?node-id=123-456)"
1723
+ );
1661
1724
  }
1662
1725
  const nodeId = nodeIdMatch[1].replace(/-/g, ":");
1663
1726
  if (fileName) {
1664
1727
  const expectedExtension = `.${format}`;
1665
1728
  if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1666
- throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1729
+ throw new Error(
1730
+ `Filename must end with ${expectedExtension} for ${format} format`
1731
+ );
1667
1732
  }
1668
1733
  }
1669
1734
  const figmaService = new FigmaService({
@@ -1672,27 +1737,38 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1672
1737
  useOAuth: useOAuth && !!oauthToken
1673
1738
  });
1674
1739
  try {
1675
- Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
1740
+ Logger.log(
1741
+ `Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`
1742
+ );
1676
1743
  const imageNode = {
1677
1744
  nodeId,
1678
1745
  fileName: fileName || `temp.${format}`
1679
1746
  };
1680
- const results = await figmaService.downloadImages(fileKey, localPath || "", [imageNode], {
1681
- pngScale: format === "png" ? pngScale : void 0,
1682
- returnBuffer
1683
- });
1747
+ const results = await figmaService.downloadImages(
1748
+ fileKey,
1749
+ localPath || "",
1750
+ [imageNode],
1751
+ {
1752
+ pngScale: format === "png" ? pngScale : void 0,
1753
+ returnBuffer
1754
+ }
1755
+ );
1684
1756
  if (results.length === 0) {
1685
1757
  throw new Error(`Failed to download image for frame ${nodeId}`);
1686
1758
  }
1687
1759
  if (returnBuffer) {
1688
1760
  Logger.log(`Successfully downloaded frame image as buffer`);
1689
1761
  } else {
1690
- Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1762
+ Logger.log(
1763
+ `Successfully downloaded frame image to: ${results[0].filePath}`
1764
+ );
1691
1765
  }
1692
1766
  return results[0];
1693
1767
  } catch (error) {
1694
1768
  Logger.error(`Error downloading frame image from ${fileKey}:`, error);
1695
- throw new Error(`Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`);
1769
+ throw new Error(
1770
+ `Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`
1771
+ );
1696
1772
  }
1697
1773
  }
1698
1774
  function getImageNodeInfo(metadata) {
@@ -1714,7 +1790,9 @@ function enrichMetadataWithImages(metadata, imagePaths, options = {}) {
1714
1790
  let downloadResults;
1715
1791
  if (Array.isArray(imagePaths)) {
1716
1792
  if (imagePaths.length !== metadata.images.length) {
1717
- throw new Error(`Number of image paths (${imagePaths.length}) must match number of images (${metadata.images.length})`);
1793
+ throw new Error(
1794
+ `Number of image paths (${imagePaths.length}) must match number of images (${metadata.images.length})`
1795
+ );
1718
1796
  }
1719
1797
  downloadResults = imagePaths.map((filePath, index) => ({
1720
1798
  filePath,
@@ -1726,16 +1804,23 @@ function enrichMetadataWithImages(metadata, imagePaths, options = {}) {
1726
1804
  downloadResults = imageAssets.map((asset) => {
1727
1805
  const filePath = imagePaths[asset.id];
1728
1806
  if (!filePath) {
1729
- throw new Error(`No image path provided for node ID: ${asset.id}`);
1807
+ return null;
1730
1808
  }
1731
- const imageMetadata = metadata.images.find((img) => img.nodeId === asset.id);
1809
+ const imageMetadata = metadata.images.find(
1810
+ (img) => img.nodeId === asset.id
1811
+ );
1732
1812
  return {
1733
1813
  filePath,
1734
- finalDimensions: imageMetadata?.finalDimensions || { width: 0, height: 0 },
1814
+ finalDimensions: imageMetadata?.finalDimensions || {
1815
+ width: 0,
1816
+ height: 0
1817
+ },
1735
1818
  wasCropped: imageMetadata?.wasCropped || false,
1736
1819
  cssVariables: imageMetadata?.cssVariables
1737
1820
  };
1738
- });
1821
+ }).filter(
1822
+ (result) => result !== null
1823
+ );
1739
1824
  }
1740
1825
  const enrichedNodes = enrichNodesWithImages(
1741
1826
  metadata.nodes,
package/dist/index.js CHANGED
@@ -263,6 +263,122 @@ 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;
@@ -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;
@@ -1535,14 +1556,25 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1535
1556
  );
1536
1557
  let rawApiResponse;
1537
1558
  if (nodeId) {
1538
- rawApiResponse = await figmaService.getRawNode(fileKey, nodeId, depth || void 0);
1559
+ rawApiResponse = await figmaService.getRawNode(
1560
+ fileKey,
1561
+ nodeId,
1562
+ depth || void 0
1563
+ );
1539
1564
  } else {
1540
- rawApiResponse = await figmaService.getRawFile(fileKey, depth || void 0);
1565
+ rawApiResponse = await figmaService.getRawFile(
1566
+ fileKey,
1567
+ depth || void 0
1568
+ );
1541
1569
  }
1542
- const simplifiedDesign = simplifyRawFigmaObject(rawApiResponse, allExtractors, {
1543
- maxDepth: depth || void 0,
1544
- afterChildren: collapseSvgContainers
1545
- });
1570
+ const simplifiedDesign = simplifyRawFigmaObject(
1571
+ rawApiResponse,
1572
+ allExtractors,
1573
+ {
1574
+ maxDepth: depth || void 0,
1575
+ afterChildren: collapseSvgContainers
1576
+ }
1577
+ );
1546
1578
  Logger.log(
1547
1579
  `Successfully extracted data: ${simplifiedDesign.nodes.length} nodes, ${Object.keys(simplifiedDesign.globalVars?.styles || {}).length} styles`
1548
1580
  );
@@ -1554,7 +1586,9 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1554
1586
  };
1555
1587
  if (downloadImages) {
1556
1588
  if (!returnBuffer && !localPath) {
1557
- throw new Error("localPath is required when downloadImages is true and returnBuffer is false");
1589
+ throw new Error(
1590
+ "localPath is required when downloadImages is true and returnBuffer is false"
1591
+ );
1558
1592
  }
1559
1593
  Logger.log("Discovering and downloading image assets...");
1560
1594
  const imageAssets = findImageAssets(nodes, globalVars);
@@ -1575,10 +1609,20 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1575
1609
  );
1576
1610
  if (returnBuffer) {
1577
1611
  result.images = downloadResults;
1578
- Logger.log(`Successfully downloaded ${downloadResults.length} images as buffers`);
1612
+ Logger.log(
1613
+ `Successfully downloaded ${downloadResults.length} images as buffers`
1614
+ );
1579
1615
  } else {
1580
- result.nodes = enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths, localPath);
1581
- Logger.log(`Successfully downloaded and enriched ${downloadResults.length} images`);
1616
+ result.nodes = enrichNodesWithImages(
1617
+ nodes,
1618
+ imageAssets,
1619
+ downloadResults,
1620
+ useRelativePaths,
1621
+ localPath
1622
+ );
1623
+ Logger.log(
1624
+ `Successfully downloaded and enriched ${downloadResults.length} images`
1625
+ );
1582
1626
  }
1583
1627
  }
1584
1628
  }
@@ -1596,7 +1640,15 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1596
1640
  }
1597
1641
  }
1598
1642
  async function downloadFigmaImages(figmaUrl, nodes, options) {
1599
- const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false, returnBuffer = false } = options;
1643
+ const {
1644
+ apiKey,
1645
+ oauthToken,
1646
+ useOAuth = false,
1647
+ pngScale = 2,
1648
+ localPath,
1649
+ enableLogging = false,
1650
+ returnBuffer = false
1651
+ } = options;
1600
1652
  Logger.enableLogging = enableLogging;
1601
1653
  if (!apiKey && !oauthToken) {
1602
1654
  throw new Error("Either apiKey or oauthToken is required");
@@ -1619,14 +1671,21 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
1619
1671
  ...node,
1620
1672
  nodeId: node.nodeId.replace(/-/g, ":")
1621
1673
  }));
1622
- const results = await figmaService.downloadImages(fileKey, localPath || "", processedNodes, {
1623
- pngScale,
1624
- returnBuffer
1625
- });
1674
+ const results = await figmaService.downloadImages(
1675
+ fileKey,
1676
+ localPath || "",
1677
+ processedNodes,
1678
+ {
1679
+ pngScale,
1680
+ returnBuffer
1681
+ }
1682
+ );
1626
1683
  return results;
1627
1684
  } catch (error) {
1628
1685
  Logger.error(`Error downloading images from ${fileKey}:`, error);
1629
- throw new Error(`Failed to download images: ${error instanceof Error ? error.message : String(error)}`);
1686
+ throw new Error(
1687
+ `Failed to download images: ${error instanceof Error ? error.message : String(error)}`
1688
+ );
1630
1689
  }
1631
1690
  }
1632
1691
  async function downloadFigmaFrameImage(figmaUrl, options) {
@@ -1646,7 +1705,9 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1646
1705
  throw new Error("Either apiKey or oauthToken is required");
1647
1706
  }
1648
1707
  if (!returnBuffer && (!localPath || !fileName)) {
1649
- throw new Error("localPath and fileName are required when returnBuffer is false");
1708
+ throw new Error(
1709
+ "localPath and fileName are required when returnBuffer is false"
1710
+ );
1650
1711
  }
1651
1712
  const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1652
1713
  if (!urlMatch) {
@@ -1655,13 +1716,17 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1655
1716
  const fileKey = urlMatch[2];
1656
1717
  const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
1657
1718
  if (!nodeIdMatch) {
1658
- throw new Error("No frame node-id found in URL. Please provide a Figma URL with a node-id parameter (e.g., ?node-id=123-456)");
1719
+ throw new Error(
1720
+ "No frame node-id found in URL. Please provide a Figma URL with a node-id parameter (e.g., ?node-id=123-456)"
1721
+ );
1659
1722
  }
1660
1723
  const nodeId = nodeIdMatch[1].replace(/-/g, ":");
1661
1724
  if (fileName) {
1662
1725
  const expectedExtension = `.${format}`;
1663
1726
  if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1664
- throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1727
+ throw new Error(
1728
+ `Filename must end with ${expectedExtension} for ${format} format`
1729
+ );
1665
1730
  }
1666
1731
  }
1667
1732
  const figmaService = new FigmaService({
@@ -1670,27 +1735,38 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1670
1735
  useOAuth: useOAuth && !!oauthToken
1671
1736
  });
1672
1737
  try {
1673
- Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
1738
+ Logger.log(
1739
+ `Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`
1740
+ );
1674
1741
  const imageNode = {
1675
1742
  nodeId,
1676
1743
  fileName: fileName || `temp.${format}`
1677
1744
  };
1678
- const results = await figmaService.downloadImages(fileKey, localPath || "", [imageNode], {
1679
- pngScale: format === "png" ? pngScale : void 0,
1680
- returnBuffer
1681
- });
1745
+ const results = await figmaService.downloadImages(
1746
+ fileKey,
1747
+ localPath || "",
1748
+ [imageNode],
1749
+ {
1750
+ pngScale: format === "png" ? pngScale : void 0,
1751
+ returnBuffer
1752
+ }
1753
+ );
1682
1754
  if (results.length === 0) {
1683
1755
  throw new Error(`Failed to download image for frame ${nodeId}`);
1684
1756
  }
1685
1757
  if (returnBuffer) {
1686
1758
  Logger.log(`Successfully downloaded frame image as buffer`);
1687
1759
  } else {
1688
- Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1760
+ Logger.log(
1761
+ `Successfully downloaded frame image to: ${results[0].filePath}`
1762
+ );
1689
1763
  }
1690
1764
  return results[0];
1691
1765
  } catch (error) {
1692
1766
  Logger.error(`Error downloading frame image from ${fileKey}:`, error);
1693
- throw new Error(`Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`);
1767
+ throw new Error(
1768
+ `Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`
1769
+ );
1694
1770
  }
1695
1771
  }
1696
1772
  function getImageNodeInfo(metadata) {
@@ -1712,7 +1788,9 @@ function enrichMetadataWithImages(metadata, imagePaths, options = {}) {
1712
1788
  let downloadResults;
1713
1789
  if (Array.isArray(imagePaths)) {
1714
1790
  if (imagePaths.length !== metadata.images.length) {
1715
- throw new Error(`Number of image paths (${imagePaths.length}) must match number of images (${metadata.images.length})`);
1791
+ throw new Error(
1792
+ `Number of image paths (${imagePaths.length}) must match number of images (${metadata.images.length})`
1793
+ );
1716
1794
  }
1717
1795
  downloadResults = imagePaths.map((filePath, index) => ({
1718
1796
  filePath,
@@ -1724,16 +1802,23 @@ function enrichMetadataWithImages(metadata, imagePaths, options = {}) {
1724
1802
  downloadResults = imageAssets.map((asset) => {
1725
1803
  const filePath = imagePaths[asset.id];
1726
1804
  if (!filePath) {
1727
- throw new Error(`No image path provided for node ID: ${asset.id}`);
1805
+ return null;
1728
1806
  }
1729
- const imageMetadata = metadata.images.find((img) => img.nodeId === asset.id);
1807
+ const imageMetadata = metadata.images.find(
1808
+ (img) => img.nodeId === asset.id
1809
+ );
1730
1810
  return {
1731
1811
  filePath,
1732
- finalDimensions: imageMetadata?.finalDimensions || { width: 0, height: 0 },
1812
+ finalDimensions: imageMetadata?.finalDimensions || {
1813
+ width: 0,
1814
+ height: 0
1815
+ },
1733
1816
  wasCropped: imageMetadata?.wasCropped || false,
1734
1817
  cssVariables: imageMetadata?.cssVariables
1735
1818
  };
1736
- });
1819
+ }).filter(
1820
+ (result) => result !== null
1821
+ );
1737
1822
  }
1738
1823
  const enrichedNodes = enrichNodesWithImages(
1739
1824
  metadata.nodes,
package/dist/lib.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface FigmaMetadataOptions {
14
14
  /** Local path for downloaded images (required if downloadImages is true) */
15
15
  localPath?: string;
16
16
  /** Image format for downloads (defaults to 'png') */
17
- imageFormat?: 'png' | 'svg';
17
+ imageFormat?: "png" | "svg";
18
18
  /** Export scale for PNG images (defaults to 2) */
19
19
  pngScale?: number;
20
20
  /**
@@ -85,7 +85,7 @@ export interface FigmaFrameImageOptions {
85
85
  /** The filename for the downloaded image (must end with .png or .svg, optional if returnBuffer is true) */
86
86
  fileName?: string;
87
87
  /** Image format to download (defaults to 'png') */
88
- format?: 'png' | 'svg';
88
+ format?: "png" | "svg";
89
89
  /** Enable JSON debug log files (defaults to false) */
90
90
  enableLogging?: boolean;
91
91
  /** Return image as ArrayBuffer instead of saving to disk (defaults to false) */
@@ -99,10 +99,10 @@ export interface FigmaFrameImageOptions {
99
99
  * @returns Promise resolving to the extracted metadata
100
100
  */
101
101
  export declare function getFigmaMetadata(figmaUrl: string, options: FigmaMetadataOptions & {
102
- outputFormat: 'json';
102
+ outputFormat: "json";
103
103
  }): Promise<string>;
104
104
  export declare function getFigmaMetadata(figmaUrl: string, options: FigmaMetadataOptions & {
105
- outputFormat: 'yaml';
105
+ outputFormat: "yaml";
106
106
  }): Promise<string>;
107
107
  export declare function getFigmaMetadata(figmaUrl: string, options?: FigmaMetadataOptions): Promise<FigmaMetadataResult>;
108
108
  /**
@@ -155,6 +155,7 @@ export declare function getImageNodeInfo(metadata: FigmaMetadataResult): Array<{
155
155
  * Enrich metadata with saved image file paths
156
156
  *
157
157
  * Use this function after saving images from buffers to disk to add file path information to the metadata.
158
+ * When using the object format, missing image paths for node IDs are skipped rather than throwing errors.
158
159
  *
159
160
  * @param metadata - The metadata result from getFigmaMetadata
160
161
  * @param imagePaths - Array of file paths (ordered) OR object mapping node IDs to paths/URLs
@@ -167,6 +168,7 @@ export declare function getImageNodeInfo(metadata: FigmaMetadataResult): Array<{
167
168
  * const enriched = enrichMetadataWithImages(metadata, ['/path/to/img1.png', '/path/to/img2.png']);
168
169
  *
169
170
  * // Object format (keyed by node ID) - useful after uploading to CDN
171
+ * // Missing node IDs are automatically skipped
170
172
  * const enriched = enrichMetadataWithImages(metadata, {
171
173
  * '123:456': 'https://cdn.example.com/icon.png',
172
174
  * '789:012': 'https://cdn.example.com/logo.png'
@@ -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.12",
3
+ "version": "1.0.14",
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",