figma-metadata-extractor 1.0.11 → 1.0.13

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/dist/index.cjs CHANGED
@@ -289,15 +289,20 @@ class FigmaService {
289
289
  */
290
290
  filterValidImages(images) {
291
291
  if (!images) return {};
292
- return Object.fromEntries(Object.entries(images).filter(([, value]) => !!value));
292
+ return Object.fromEntries(
293
+ Object.entries(images).filter(([, value]) => !!value)
294
+ );
293
295
  }
294
296
  async request(endpoint) {
295
297
  try {
296
298
  Logger.log(`Calling ${this.baseUrl}${endpoint}`);
297
299
  const headers = this.getAuthHeaders();
298
- return await fetchWithRetry(`${this.baseUrl}${endpoint}`, {
299
- headers
300
- });
300
+ return await fetchWithRetry(
301
+ `${this.baseUrl}${endpoint}`,
302
+ {
303
+ headers
304
+ }
305
+ );
301
306
  } catch (error) {
302
307
  const errorMessage = error instanceof Error ? error.message : String(error);
303
308
  throw new Error(
@@ -372,7 +377,9 @@ class FigmaService {
372
377
  const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
373
378
  resolvedPath = path.resolve(sanitizedPath);
374
379
  if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
375
- throw new Error("Invalid path specified. Directory traversal is not allowed.");
380
+ throw new Error(
381
+ "Invalid path specified. Directory traversal is not allowed."
382
+ );
376
383
  }
377
384
  }
378
385
  const downloadPromises = [];
@@ -384,25 +391,45 @@ class FigmaService {
384
391
  );
385
392
  if (imageFills.length > 0) {
386
393
  const fillUrls = await this.getImageFillUrls(fileKey);
387
- const fillDownloads = imageFills.map(({ imageRef, fileName, needsCropping, cropTransform, requiresImageDimensions }) => {
388
- const imageUrl = fillUrls[imageRef];
389
- return imageUrl ? downloadAndProcessImage(
394
+ const fillDownloads = imageFills.map(
395
+ ({
396
+ imageRef,
390
397
  fileName,
391
- resolvedPath,
392
- imageUrl,
393
398
  needsCropping,
394
399
  cropTransform,
395
- requiresImageDimensions,
396
- returnBuffer
397
- ) : null;
398
- }).filter((promise) => promise !== null);
400
+ requiresImageDimensions
401
+ }) => {
402
+ const imageUrl = fillUrls[imageRef];
403
+ if (!imageUrl) {
404
+ Logger.log(
405
+ `Skipping image fill with missing URL for imageRef: ${imageRef}`
406
+ );
407
+ return null;
408
+ }
409
+ return downloadAndProcessImage(
410
+ fileName,
411
+ resolvedPath,
412
+ imageUrl,
413
+ needsCropping,
414
+ cropTransform,
415
+ requiresImageDimensions,
416
+ returnBuffer
417
+ );
418
+ }
419
+ ).filter(
420
+ (promise) => promise !== null
421
+ );
399
422
  if (fillDownloads.length > 0) {
400
423
  downloadPromises.push(Promise.all(fillDownloads));
401
424
  }
402
425
  }
403
426
  if (renderNodes.length > 0) {
404
- const pngNodes = renderNodes.filter((node) => !node.fileName.toLowerCase().endsWith(".svg"));
405
- const svgNodes = renderNodes.filter((node) => node.fileName.toLowerCase().endsWith(".svg"));
427
+ const pngNodes = renderNodes.filter(
428
+ (node) => !node.fileName.toLowerCase().endsWith(".svg")
429
+ );
430
+ const svgNodes = renderNodes.filter(
431
+ (node) => node.fileName.toLowerCase().endsWith(".svg")
432
+ );
406
433
  if (pngNodes.length > 0) {
407
434
  const pngUrls = await this.getNodeRenderUrls(
408
435
  fileKey,
@@ -410,18 +437,36 @@ class FigmaService {
410
437
  "png",
411
438
  { pngScale }
412
439
  );
413
- const pngDownloads = pngNodes.map(({ nodeId, fileName, needsCropping, cropTransform, requiresImageDimensions }) => {
414
- const imageUrl = pngUrls[nodeId];
415
- return imageUrl ? downloadAndProcessImage(
440
+ const pngDownloads = pngNodes.map(
441
+ ({
442
+ nodeId,
416
443
  fileName,
417
- resolvedPath,
418
- imageUrl,
419
444
  needsCropping,
420
445
  cropTransform,
421
- requiresImageDimensions,
422
- returnBuffer
423
- ).then((result) => ({ ...result, nodeId })) : null;
424
- }).filter((promise) => promise !== null);
446
+ requiresImageDimensions
447
+ }) => {
448
+ const imageUrl = pngUrls[nodeId];
449
+ if (!imageUrl) {
450
+ Logger.log(
451
+ `Skipping PNG render with missing URL for nodeId: ${nodeId}`
452
+ );
453
+ return null;
454
+ }
455
+ return downloadAndProcessImage(
456
+ fileName,
457
+ resolvedPath,
458
+ imageUrl,
459
+ needsCropping,
460
+ cropTransform,
461
+ requiresImageDimensions,
462
+ returnBuffer
463
+ ).then(
464
+ (result) => ({ ...result, nodeId })
465
+ );
466
+ }
467
+ ).filter(
468
+ (promise) => promise !== null
469
+ );
425
470
  if (pngDownloads.length > 0) {
426
471
  downloadPromises.push(Promise.all(pngDownloads));
427
472
  }
@@ -433,18 +478,36 @@ class FigmaService {
433
478
  "svg",
434
479
  { svgOptions }
435
480
  );
436
- const svgDownloads = svgNodes.map(({ nodeId, fileName, needsCropping, cropTransform, requiresImageDimensions }) => {
437
- const imageUrl = svgUrls[nodeId];
438
- return imageUrl ? downloadAndProcessImage(
481
+ const svgDownloads = svgNodes.map(
482
+ ({
483
+ nodeId,
439
484
  fileName,
440
- resolvedPath,
441
- imageUrl,
442
485
  needsCropping,
443
486
  cropTransform,
444
- requiresImageDimensions,
445
- returnBuffer
446
- ).then((result) => ({ ...result, nodeId })) : null;
447
- }).filter((promise) => promise !== null);
487
+ requiresImageDimensions
488
+ }) => {
489
+ const imageUrl = svgUrls[nodeId];
490
+ if (!imageUrl) {
491
+ Logger.log(
492
+ `Skipping SVG render with missing URL for nodeId: ${nodeId}`
493
+ );
494
+ return null;
495
+ }
496
+ return downloadAndProcessImage(
497
+ fileName,
498
+ resolvedPath,
499
+ imageUrl,
500
+ needsCropping,
501
+ cropTransform,
502
+ requiresImageDimensions,
503
+ returnBuffer
504
+ ).then(
505
+ (result) => ({ ...result, nodeId })
506
+ );
507
+ }
508
+ ).filter(
509
+ (promise) => promise !== null
510
+ );
448
511
  if (svgDownloads.length > 0) {
449
512
  downloadPromises.push(Promise.all(svgDownloads));
450
513
  }
@@ -458,7 +521,9 @@ class FigmaService {
458
521
  */
459
522
  async getRawFile(fileKey, depth) {
460
523
  const endpoint = `/files/${fileKey}${depth ? `?depth=${depth}` : ""}`;
461
- Logger.log(`Retrieving raw Figma file: ${fileKey} (depth: ${depth ?? "default"})`);
524
+ Logger.log(
525
+ `Retrieving raw Figma file: ${fileKey} (depth: ${depth ?? "default"})`
526
+ );
462
527
  const response = await this.request(endpoint);
463
528
  writeLogs("figma-raw.json", response);
464
529
  return response;
@@ -1472,14 +1537,25 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1472
1537
  );
1473
1538
  let rawApiResponse;
1474
1539
  if (nodeId) {
1475
- rawApiResponse = await figmaService.getRawNode(fileKey, nodeId, depth || void 0);
1540
+ rawApiResponse = await figmaService.getRawNode(
1541
+ fileKey,
1542
+ nodeId,
1543
+ depth || void 0
1544
+ );
1476
1545
  } else {
1477
- rawApiResponse = await figmaService.getRawFile(fileKey, depth || void 0);
1546
+ rawApiResponse = await figmaService.getRawFile(
1547
+ fileKey,
1548
+ depth || void 0
1549
+ );
1478
1550
  }
1479
- const simplifiedDesign = simplifyRawFigmaObject(rawApiResponse, allExtractors, {
1480
- maxDepth: depth || void 0,
1481
- afterChildren: collapseSvgContainers
1482
- });
1551
+ const simplifiedDesign = simplifyRawFigmaObject(
1552
+ rawApiResponse,
1553
+ allExtractors,
1554
+ {
1555
+ maxDepth: depth || void 0,
1556
+ afterChildren: collapseSvgContainers
1557
+ }
1558
+ );
1483
1559
  Logger.log(
1484
1560
  `Successfully extracted data: ${simplifiedDesign.nodes.length} nodes, ${Object.keys(simplifiedDesign.globalVars?.styles || {}).length} styles`
1485
1561
  );
@@ -1491,7 +1567,9 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1491
1567
  };
1492
1568
  if (downloadImages) {
1493
1569
  if (!returnBuffer && !localPath) {
1494
- throw new Error("localPath is required when downloadImages is true and returnBuffer is false");
1570
+ throw new Error(
1571
+ "localPath is required when downloadImages is true and returnBuffer is false"
1572
+ );
1495
1573
  }
1496
1574
  Logger.log("Discovering and downloading image assets...");
1497
1575
  const imageAssets = findImageAssets(nodes, globalVars);
@@ -1512,10 +1590,20 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1512
1590
  );
1513
1591
  if (returnBuffer) {
1514
1592
  result.images = downloadResults;
1515
- Logger.log(`Successfully downloaded ${downloadResults.length} images as buffers`);
1593
+ Logger.log(
1594
+ `Successfully downloaded ${downloadResults.length} images as buffers`
1595
+ );
1516
1596
  } else {
1517
- result.nodes = enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths, localPath);
1518
- Logger.log(`Successfully downloaded and enriched ${downloadResults.length} images`);
1597
+ result.nodes = enrichNodesWithImages(
1598
+ nodes,
1599
+ imageAssets,
1600
+ downloadResults,
1601
+ useRelativePaths,
1602
+ localPath
1603
+ );
1604
+ Logger.log(
1605
+ `Successfully downloaded and enriched ${downloadResults.length} images`
1606
+ );
1519
1607
  }
1520
1608
  }
1521
1609
  }
@@ -1533,7 +1621,15 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1533
1621
  }
1534
1622
  }
1535
1623
  async function downloadFigmaImages(figmaUrl, nodes, options) {
1536
- const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false, returnBuffer = false } = options;
1624
+ const {
1625
+ apiKey,
1626
+ oauthToken,
1627
+ useOAuth = false,
1628
+ pngScale = 2,
1629
+ localPath,
1630
+ enableLogging = false,
1631
+ returnBuffer = false
1632
+ } = options;
1537
1633
  Logger.enableLogging = enableLogging;
1538
1634
  if (!apiKey && !oauthToken) {
1539
1635
  throw new Error("Either apiKey or oauthToken is required");
@@ -1556,14 +1652,21 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
1556
1652
  ...node,
1557
1653
  nodeId: node.nodeId.replace(/-/g, ":")
1558
1654
  }));
1559
- const results = await figmaService.downloadImages(fileKey, localPath || "", processedNodes, {
1560
- pngScale,
1561
- returnBuffer
1562
- });
1655
+ const results = await figmaService.downloadImages(
1656
+ fileKey,
1657
+ localPath || "",
1658
+ processedNodes,
1659
+ {
1660
+ pngScale,
1661
+ returnBuffer
1662
+ }
1663
+ );
1563
1664
  return results;
1564
1665
  } catch (error) {
1565
1666
  Logger.error(`Error downloading images from ${fileKey}:`, error);
1566
- throw new Error(`Failed to download images: ${error instanceof Error ? error.message : String(error)}`);
1667
+ throw new Error(
1668
+ `Failed to download images: ${error instanceof Error ? error.message : String(error)}`
1669
+ );
1567
1670
  }
1568
1671
  }
1569
1672
  async function downloadFigmaFrameImage(figmaUrl, options) {
@@ -1583,7 +1686,9 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1583
1686
  throw new Error("Either apiKey or oauthToken is required");
1584
1687
  }
1585
1688
  if (!returnBuffer && (!localPath || !fileName)) {
1586
- throw new Error("localPath and fileName are required when returnBuffer is false");
1689
+ throw new Error(
1690
+ "localPath and fileName are required when returnBuffer is false"
1691
+ );
1587
1692
  }
1588
1693
  const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1589
1694
  if (!urlMatch) {
@@ -1592,13 +1697,17 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1592
1697
  const fileKey = urlMatch[2];
1593
1698
  const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
1594
1699
  if (!nodeIdMatch) {
1595
- 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)");
1700
+ throw new Error(
1701
+ "No frame node-id found in URL. Please provide a Figma URL with a node-id parameter (e.g., ?node-id=123-456)"
1702
+ );
1596
1703
  }
1597
1704
  const nodeId = nodeIdMatch[1].replace(/-/g, ":");
1598
1705
  if (fileName) {
1599
1706
  const expectedExtension = `.${format}`;
1600
1707
  if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1601
- throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1708
+ throw new Error(
1709
+ `Filename must end with ${expectedExtension} for ${format} format`
1710
+ );
1602
1711
  }
1603
1712
  }
1604
1713
  const figmaService = new FigmaService({
@@ -1607,27 +1716,38 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1607
1716
  useOAuth: useOAuth && !!oauthToken
1608
1717
  });
1609
1718
  try {
1610
- Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
1719
+ Logger.log(
1720
+ `Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`
1721
+ );
1611
1722
  const imageNode = {
1612
1723
  nodeId,
1613
1724
  fileName: fileName || `temp.${format}`
1614
1725
  };
1615
- const results = await figmaService.downloadImages(fileKey, localPath || "", [imageNode], {
1616
- pngScale: format === "png" ? pngScale : void 0,
1617
- returnBuffer
1618
- });
1726
+ const results = await figmaService.downloadImages(
1727
+ fileKey,
1728
+ localPath || "",
1729
+ [imageNode],
1730
+ {
1731
+ pngScale: format === "png" ? pngScale : void 0,
1732
+ returnBuffer
1733
+ }
1734
+ );
1619
1735
  if (results.length === 0) {
1620
1736
  throw new Error(`Failed to download image for frame ${nodeId}`);
1621
1737
  }
1622
1738
  if (returnBuffer) {
1623
1739
  Logger.log(`Successfully downloaded frame image as buffer`);
1624
1740
  } else {
1625
- Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1741
+ Logger.log(
1742
+ `Successfully downloaded frame image to: ${results[0].filePath}`
1743
+ );
1626
1744
  }
1627
1745
  return results[0];
1628
1746
  } catch (error) {
1629
1747
  Logger.error(`Error downloading frame image from ${fileKey}:`, error);
1630
- throw new Error(`Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`);
1748
+ throw new Error(
1749
+ `Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`
1750
+ );
1631
1751
  }
1632
1752
  }
1633
1753
  function getImageNodeInfo(metadata) {
@@ -1649,7 +1769,9 @@ function enrichMetadataWithImages(metadata, imagePaths, options = {}) {
1649
1769
  let downloadResults;
1650
1770
  if (Array.isArray(imagePaths)) {
1651
1771
  if (imagePaths.length !== metadata.images.length) {
1652
- throw new Error(`Number of image paths (${imagePaths.length}) must match number of images (${metadata.images.length})`);
1772
+ throw new Error(
1773
+ `Number of image paths (${imagePaths.length}) must match number of images (${metadata.images.length})`
1774
+ );
1653
1775
  }
1654
1776
  downloadResults = imagePaths.map((filePath, index) => ({
1655
1777
  filePath,
@@ -1661,16 +1783,23 @@ function enrichMetadataWithImages(metadata, imagePaths, options = {}) {
1661
1783
  downloadResults = imageAssets.map((asset) => {
1662
1784
  const filePath = imagePaths[asset.id];
1663
1785
  if (!filePath) {
1664
- throw new Error(`No image path provided for node ID: ${asset.id}`);
1786
+ return null;
1665
1787
  }
1666
- const imageMetadata = metadata.images.find((img) => img.nodeId === asset.id);
1788
+ const imageMetadata = metadata.images.find(
1789
+ (img) => img.nodeId === asset.id
1790
+ );
1667
1791
  return {
1668
1792
  filePath,
1669
- finalDimensions: imageMetadata?.finalDimensions || { width: 0, height: 0 },
1793
+ finalDimensions: imageMetadata?.finalDimensions || {
1794
+ width: 0,
1795
+ height: 0
1796
+ },
1670
1797
  wasCropped: imageMetadata?.wasCropped || false,
1671
1798
  cssVariables: imageMetadata?.cssVariables
1672
1799
  };
1673
- });
1800
+ }).filter(
1801
+ (result) => result !== null
1802
+ );
1674
1803
  }
1675
1804
  const enrichedNodes = enrichNodesWithImages(
1676
1805
  metadata.nodes,
package/dist/index.js CHANGED
@@ -287,15 +287,20 @@ class FigmaService {
287
287
  */
288
288
  filterValidImages(images) {
289
289
  if (!images) return {};
290
- return Object.fromEntries(Object.entries(images).filter(([, value]) => !!value));
290
+ return Object.fromEntries(
291
+ Object.entries(images).filter(([, value]) => !!value)
292
+ );
291
293
  }
292
294
  async request(endpoint) {
293
295
  try {
294
296
  Logger.log(`Calling ${this.baseUrl}${endpoint}`);
295
297
  const headers = this.getAuthHeaders();
296
- return await fetchWithRetry(`${this.baseUrl}${endpoint}`, {
297
- headers
298
- });
298
+ return await fetchWithRetry(
299
+ `${this.baseUrl}${endpoint}`,
300
+ {
301
+ headers
302
+ }
303
+ );
299
304
  } catch (error) {
300
305
  const errorMessage = error instanceof Error ? error.message : String(error);
301
306
  throw new Error(
@@ -370,7 +375,9 @@ class FigmaService {
370
375
  const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
371
376
  resolvedPath = path.resolve(sanitizedPath);
372
377
  if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
373
- throw new Error("Invalid path specified. Directory traversal is not allowed.");
378
+ throw new Error(
379
+ "Invalid path specified. Directory traversal is not allowed."
380
+ );
374
381
  }
375
382
  }
376
383
  const downloadPromises = [];
@@ -382,25 +389,45 @@ class FigmaService {
382
389
  );
383
390
  if (imageFills.length > 0) {
384
391
  const fillUrls = await this.getImageFillUrls(fileKey);
385
- const fillDownloads = imageFills.map(({ imageRef, fileName, needsCropping, cropTransform, requiresImageDimensions }) => {
386
- const imageUrl = fillUrls[imageRef];
387
- return imageUrl ? downloadAndProcessImage(
392
+ const fillDownloads = imageFills.map(
393
+ ({
394
+ imageRef,
388
395
  fileName,
389
- resolvedPath,
390
- imageUrl,
391
396
  needsCropping,
392
397
  cropTransform,
393
- requiresImageDimensions,
394
- returnBuffer
395
- ) : null;
396
- }).filter((promise) => promise !== null);
398
+ requiresImageDimensions
399
+ }) => {
400
+ const imageUrl = fillUrls[imageRef];
401
+ if (!imageUrl) {
402
+ Logger.log(
403
+ `Skipping image fill with missing URL for imageRef: ${imageRef}`
404
+ );
405
+ return null;
406
+ }
407
+ return downloadAndProcessImage(
408
+ fileName,
409
+ resolvedPath,
410
+ imageUrl,
411
+ needsCropping,
412
+ cropTransform,
413
+ requiresImageDimensions,
414
+ returnBuffer
415
+ );
416
+ }
417
+ ).filter(
418
+ (promise) => promise !== null
419
+ );
397
420
  if (fillDownloads.length > 0) {
398
421
  downloadPromises.push(Promise.all(fillDownloads));
399
422
  }
400
423
  }
401
424
  if (renderNodes.length > 0) {
402
- const pngNodes = renderNodes.filter((node) => !node.fileName.toLowerCase().endsWith(".svg"));
403
- const svgNodes = renderNodes.filter((node) => node.fileName.toLowerCase().endsWith(".svg"));
425
+ const pngNodes = renderNodes.filter(
426
+ (node) => !node.fileName.toLowerCase().endsWith(".svg")
427
+ );
428
+ const svgNodes = renderNodes.filter(
429
+ (node) => node.fileName.toLowerCase().endsWith(".svg")
430
+ );
404
431
  if (pngNodes.length > 0) {
405
432
  const pngUrls = await this.getNodeRenderUrls(
406
433
  fileKey,
@@ -408,18 +435,36 @@ class FigmaService {
408
435
  "png",
409
436
  { pngScale }
410
437
  );
411
- const pngDownloads = pngNodes.map(({ nodeId, fileName, needsCropping, cropTransform, requiresImageDimensions }) => {
412
- const imageUrl = pngUrls[nodeId];
413
- return imageUrl ? downloadAndProcessImage(
438
+ const pngDownloads = pngNodes.map(
439
+ ({
440
+ nodeId,
414
441
  fileName,
415
- resolvedPath,
416
- imageUrl,
417
442
  needsCropping,
418
443
  cropTransform,
419
- requiresImageDimensions,
420
- returnBuffer
421
- ).then((result) => ({ ...result, nodeId })) : null;
422
- }).filter((promise) => promise !== null);
444
+ requiresImageDimensions
445
+ }) => {
446
+ const imageUrl = pngUrls[nodeId];
447
+ if (!imageUrl) {
448
+ Logger.log(
449
+ `Skipping PNG render with missing URL for nodeId: ${nodeId}`
450
+ );
451
+ return null;
452
+ }
453
+ return downloadAndProcessImage(
454
+ fileName,
455
+ resolvedPath,
456
+ imageUrl,
457
+ needsCropping,
458
+ cropTransform,
459
+ requiresImageDimensions,
460
+ returnBuffer
461
+ ).then(
462
+ (result) => ({ ...result, nodeId })
463
+ );
464
+ }
465
+ ).filter(
466
+ (promise) => promise !== null
467
+ );
423
468
  if (pngDownloads.length > 0) {
424
469
  downloadPromises.push(Promise.all(pngDownloads));
425
470
  }
@@ -431,18 +476,36 @@ class FigmaService {
431
476
  "svg",
432
477
  { svgOptions }
433
478
  );
434
- const svgDownloads = svgNodes.map(({ nodeId, fileName, needsCropping, cropTransform, requiresImageDimensions }) => {
435
- const imageUrl = svgUrls[nodeId];
436
- return imageUrl ? downloadAndProcessImage(
479
+ const svgDownloads = svgNodes.map(
480
+ ({
481
+ nodeId,
437
482
  fileName,
438
- resolvedPath,
439
- imageUrl,
440
483
  needsCropping,
441
484
  cropTransform,
442
- requiresImageDimensions,
443
- returnBuffer
444
- ).then((result) => ({ ...result, nodeId })) : null;
445
- }).filter((promise) => promise !== null);
485
+ requiresImageDimensions
486
+ }) => {
487
+ const imageUrl = svgUrls[nodeId];
488
+ if (!imageUrl) {
489
+ Logger.log(
490
+ `Skipping SVG render with missing URL for nodeId: ${nodeId}`
491
+ );
492
+ return null;
493
+ }
494
+ return downloadAndProcessImage(
495
+ fileName,
496
+ resolvedPath,
497
+ imageUrl,
498
+ needsCropping,
499
+ cropTransform,
500
+ requiresImageDimensions,
501
+ returnBuffer
502
+ ).then(
503
+ (result) => ({ ...result, nodeId })
504
+ );
505
+ }
506
+ ).filter(
507
+ (promise) => promise !== null
508
+ );
446
509
  if (svgDownloads.length > 0) {
447
510
  downloadPromises.push(Promise.all(svgDownloads));
448
511
  }
@@ -456,7 +519,9 @@ class FigmaService {
456
519
  */
457
520
  async getRawFile(fileKey, depth) {
458
521
  const endpoint = `/files/${fileKey}${depth ? `?depth=${depth}` : ""}`;
459
- Logger.log(`Retrieving raw Figma file: ${fileKey} (depth: ${depth ?? "default"})`);
522
+ Logger.log(
523
+ `Retrieving raw Figma file: ${fileKey} (depth: ${depth ?? "default"})`
524
+ );
460
525
  const response = await this.request(endpoint);
461
526
  writeLogs("figma-raw.json", response);
462
527
  return response;
@@ -1470,14 +1535,25 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1470
1535
  );
1471
1536
  let rawApiResponse;
1472
1537
  if (nodeId) {
1473
- rawApiResponse = await figmaService.getRawNode(fileKey, nodeId, depth || void 0);
1538
+ rawApiResponse = await figmaService.getRawNode(
1539
+ fileKey,
1540
+ nodeId,
1541
+ depth || void 0
1542
+ );
1474
1543
  } else {
1475
- rawApiResponse = await figmaService.getRawFile(fileKey, depth || void 0);
1544
+ rawApiResponse = await figmaService.getRawFile(
1545
+ fileKey,
1546
+ depth || void 0
1547
+ );
1476
1548
  }
1477
- const simplifiedDesign = simplifyRawFigmaObject(rawApiResponse, allExtractors, {
1478
- maxDepth: depth || void 0,
1479
- afterChildren: collapseSvgContainers
1480
- });
1549
+ const simplifiedDesign = simplifyRawFigmaObject(
1550
+ rawApiResponse,
1551
+ allExtractors,
1552
+ {
1553
+ maxDepth: depth || void 0,
1554
+ afterChildren: collapseSvgContainers
1555
+ }
1556
+ );
1481
1557
  Logger.log(
1482
1558
  `Successfully extracted data: ${simplifiedDesign.nodes.length} nodes, ${Object.keys(simplifiedDesign.globalVars?.styles || {}).length} styles`
1483
1559
  );
@@ -1489,7 +1565,9 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1489
1565
  };
1490
1566
  if (downloadImages) {
1491
1567
  if (!returnBuffer && !localPath) {
1492
- throw new Error("localPath is required when downloadImages is true and returnBuffer is false");
1568
+ throw new Error(
1569
+ "localPath is required when downloadImages is true and returnBuffer is false"
1570
+ );
1493
1571
  }
1494
1572
  Logger.log("Discovering and downloading image assets...");
1495
1573
  const imageAssets = findImageAssets(nodes, globalVars);
@@ -1510,10 +1588,20 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1510
1588
  );
1511
1589
  if (returnBuffer) {
1512
1590
  result.images = downloadResults;
1513
- Logger.log(`Successfully downloaded ${downloadResults.length} images as buffers`);
1591
+ Logger.log(
1592
+ `Successfully downloaded ${downloadResults.length} images as buffers`
1593
+ );
1514
1594
  } else {
1515
- result.nodes = enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths, localPath);
1516
- Logger.log(`Successfully downloaded and enriched ${downloadResults.length} images`);
1595
+ result.nodes = enrichNodesWithImages(
1596
+ nodes,
1597
+ imageAssets,
1598
+ downloadResults,
1599
+ useRelativePaths,
1600
+ localPath
1601
+ );
1602
+ Logger.log(
1603
+ `Successfully downloaded and enriched ${downloadResults.length} images`
1604
+ );
1517
1605
  }
1518
1606
  }
1519
1607
  }
@@ -1531,7 +1619,15 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1531
1619
  }
1532
1620
  }
1533
1621
  async function downloadFigmaImages(figmaUrl, nodes, options) {
1534
- const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false, returnBuffer = false } = options;
1622
+ const {
1623
+ apiKey,
1624
+ oauthToken,
1625
+ useOAuth = false,
1626
+ pngScale = 2,
1627
+ localPath,
1628
+ enableLogging = false,
1629
+ returnBuffer = false
1630
+ } = options;
1535
1631
  Logger.enableLogging = enableLogging;
1536
1632
  if (!apiKey && !oauthToken) {
1537
1633
  throw new Error("Either apiKey or oauthToken is required");
@@ -1554,14 +1650,21 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
1554
1650
  ...node,
1555
1651
  nodeId: node.nodeId.replace(/-/g, ":")
1556
1652
  }));
1557
- const results = await figmaService.downloadImages(fileKey, localPath || "", processedNodes, {
1558
- pngScale,
1559
- returnBuffer
1560
- });
1653
+ const results = await figmaService.downloadImages(
1654
+ fileKey,
1655
+ localPath || "",
1656
+ processedNodes,
1657
+ {
1658
+ pngScale,
1659
+ returnBuffer
1660
+ }
1661
+ );
1561
1662
  return results;
1562
1663
  } catch (error) {
1563
1664
  Logger.error(`Error downloading images from ${fileKey}:`, error);
1564
- throw new Error(`Failed to download images: ${error instanceof Error ? error.message : String(error)}`);
1665
+ throw new Error(
1666
+ `Failed to download images: ${error instanceof Error ? error.message : String(error)}`
1667
+ );
1565
1668
  }
1566
1669
  }
1567
1670
  async function downloadFigmaFrameImage(figmaUrl, options) {
@@ -1581,7 +1684,9 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1581
1684
  throw new Error("Either apiKey or oauthToken is required");
1582
1685
  }
1583
1686
  if (!returnBuffer && (!localPath || !fileName)) {
1584
- throw new Error("localPath and fileName are required when returnBuffer is false");
1687
+ throw new Error(
1688
+ "localPath and fileName are required when returnBuffer is false"
1689
+ );
1585
1690
  }
1586
1691
  const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1587
1692
  if (!urlMatch) {
@@ -1590,13 +1695,17 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1590
1695
  const fileKey = urlMatch[2];
1591
1696
  const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
1592
1697
  if (!nodeIdMatch) {
1593
- 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)");
1698
+ throw new Error(
1699
+ "No frame node-id found in URL. Please provide a Figma URL with a node-id parameter (e.g., ?node-id=123-456)"
1700
+ );
1594
1701
  }
1595
1702
  const nodeId = nodeIdMatch[1].replace(/-/g, ":");
1596
1703
  if (fileName) {
1597
1704
  const expectedExtension = `.${format}`;
1598
1705
  if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1599
- throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1706
+ throw new Error(
1707
+ `Filename must end with ${expectedExtension} for ${format} format`
1708
+ );
1600
1709
  }
1601
1710
  }
1602
1711
  const figmaService = new FigmaService({
@@ -1605,27 +1714,38 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1605
1714
  useOAuth: useOAuth && !!oauthToken
1606
1715
  });
1607
1716
  try {
1608
- Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
1717
+ Logger.log(
1718
+ `Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`
1719
+ );
1609
1720
  const imageNode = {
1610
1721
  nodeId,
1611
1722
  fileName: fileName || `temp.${format}`
1612
1723
  };
1613
- const results = await figmaService.downloadImages(fileKey, localPath || "", [imageNode], {
1614
- pngScale: format === "png" ? pngScale : void 0,
1615
- returnBuffer
1616
- });
1724
+ const results = await figmaService.downloadImages(
1725
+ fileKey,
1726
+ localPath || "",
1727
+ [imageNode],
1728
+ {
1729
+ pngScale: format === "png" ? pngScale : void 0,
1730
+ returnBuffer
1731
+ }
1732
+ );
1617
1733
  if (results.length === 0) {
1618
1734
  throw new Error(`Failed to download image for frame ${nodeId}`);
1619
1735
  }
1620
1736
  if (returnBuffer) {
1621
1737
  Logger.log(`Successfully downloaded frame image as buffer`);
1622
1738
  } else {
1623
- Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1739
+ Logger.log(
1740
+ `Successfully downloaded frame image to: ${results[0].filePath}`
1741
+ );
1624
1742
  }
1625
1743
  return results[0];
1626
1744
  } catch (error) {
1627
1745
  Logger.error(`Error downloading frame image from ${fileKey}:`, error);
1628
- throw new Error(`Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`);
1746
+ throw new Error(
1747
+ `Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`
1748
+ );
1629
1749
  }
1630
1750
  }
1631
1751
  function getImageNodeInfo(metadata) {
@@ -1647,7 +1767,9 @@ function enrichMetadataWithImages(metadata, imagePaths, options = {}) {
1647
1767
  let downloadResults;
1648
1768
  if (Array.isArray(imagePaths)) {
1649
1769
  if (imagePaths.length !== metadata.images.length) {
1650
- throw new Error(`Number of image paths (${imagePaths.length}) must match number of images (${metadata.images.length})`);
1770
+ throw new Error(
1771
+ `Number of image paths (${imagePaths.length}) must match number of images (${metadata.images.length})`
1772
+ );
1651
1773
  }
1652
1774
  downloadResults = imagePaths.map((filePath, index) => ({
1653
1775
  filePath,
@@ -1659,16 +1781,23 @@ function enrichMetadataWithImages(metadata, imagePaths, options = {}) {
1659
1781
  downloadResults = imageAssets.map((asset) => {
1660
1782
  const filePath = imagePaths[asset.id];
1661
1783
  if (!filePath) {
1662
- throw new Error(`No image path provided for node ID: ${asset.id}`);
1784
+ return null;
1663
1785
  }
1664
- const imageMetadata = metadata.images.find((img) => img.nodeId === asset.id);
1786
+ const imageMetadata = metadata.images.find(
1787
+ (img) => img.nodeId === asset.id
1788
+ );
1665
1789
  return {
1666
1790
  filePath,
1667
- finalDimensions: imageMetadata?.finalDimensions || { width: 0, height: 0 },
1791
+ finalDimensions: imageMetadata?.finalDimensions || {
1792
+ width: 0,
1793
+ height: 0
1794
+ },
1668
1795
  wasCropped: imageMetadata?.wasCropped || false,
1669
1796
  cssVariables: imageMetadata?.cssVariables
1670
1797
  };
1671
- });
1798
+ }).filter(
1799
+ (result) => result !== null
1800
+ );
1672
1801
  }
1673
1802
  const enrichedNodes = enrichNodesWithImages(
1674
1803
  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'
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
1
  {
2
- "name": "figma-metadata-extractor",
3
- "version": "1.0.11",
4
- "description": "Extract metadata and download images from Figma files. A standalone library for accessing Figma design data and downloading frame images programmatically.",
5
- "type": "module",
6
- "main": "dist/index.cjs",
7
- "module": "dist/index.js",
8
- "types": "dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
14
- }
15
- },
16
- "files": [
17
- "dist",
18
- "README.md",
19
- "example.js"
20
- ],
21
- "scripts": {
22
- "build": "vite build && tsc --project tsconfig.declarations.json",
23
- "typecheck": "tsc --noEmit",
24
- "test": "jest",
25
- "dev": "vite build --watch",
26
- "lint": "eslint .",
27
- "format": "prettier --write \"src/**/*.ts\"",
28
- "prepack": "npm run build"
29
- },
30
- "engines": {
31
- "node": ">=18.0.0"
32
- },
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/mikmokpok/figma-context-extractor.git"
36
- },
37
- "keywords": [
38
- "figma",
39
- "design",
40
- "metadata",
41
- "extractor",
42
- "typescript",
43
- "api",
44
- "images"
45
- ],
46
- "author": "",
47
- "license": "MIT",
48
- "dependencies": {
49
- "@figma/rest-api-spec": "^0.33.0",
50
- "js-yaml": "^4.1.0",
51
- "remeda": "^2.20.1",
52
- "sharp": "^0.34.3",
53
- "zod": "^3.24.2"
54
- },
55
- "devDependencies": {
56
- "@types/jest": "^29.5.14",
57
- "@types/js-yaml": "^4.0.9",
58
- "@types/node": "^20.17.0",
59
- "@typescript-eslint/eslint-plugin": "^8.24.0",
60
- "@typescript-eslint/parser": "^8.24.0",
61
- "eslint": "^9.20.1",
62
- "eslint-config-prettier": "^10.0.1",
63
- "jest": "^29.7.0",
64
- "prettier": "^3.5.0",
65
- "ts-jest": "^29.2.5",
66
- "typescript": "^5.7.3",
67
- "vite": "^6.4.1"
2
+ "name": "figma-metadata-extractor",
3
+ "version": "1.0.13",
4
+ "description": "Extract metadata and download images from Figma files. A standalone library for accessing Figma design data and downloading frame images programmatically.",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
68
14
  }
69
- }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "example.js"
20
+ ],
21
+ "scripts": {
22
+ "build": "vite build && tsc --project tsconfig.declarations.json",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "jest",
25
+ "dev": "vite build --watch",
26
+ "lint": "eslint .",
27
+ "format": "prettier --write \"src/**/*.ts\"",
28
+ "prepack": "npm run build"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/mikmokpok/figma-context-extractor.git"
36
+ },
37
+ "keywords": [
38
+ "figma",
39
+ "design",
40
+ "metadata",
41
+ "extractor",
42
+ "typescript",
43
+ "api",
44
+ "images"
45
+ ],
46
+ "author": "",
47
+ "license": "MIT",
48
+ "dependencies": {
49
+ "@figma/rest-api-spec": "^0.33.0",
50
+ "js-yaml": "^4.1.0",
51
+ "remeda": "^2.20.1",
52
+ "sharp": "^0.34.3",
53
+ "zod": "^3.24.2"
54
+ },
55
+ "devDependencies": {
56
+ "@types/jest": "^29.5.14",
57
+ "@types/js-yaml": "^4.0.9",
58
+ "@types/node": "^20.17.0",
59
+ "@typescript-eslint/eslint-plugin": "^8.24.0",
60
+ "@typescript-eslint/parser": "^8.24.0",
61
+ "eslint": "^9.20.1",
62
+ "eslint-config-prettier": "^10.0.1",
63
+ "jest": "^29.7.0",
64
+ "prettier": "^3.5.0",
65
+ "ts-jest": "^29.2.5",
66
+ "typescript": "^5.7.3",
67
+ "vite": "^6.4.1"
68
+ }
69
+ }