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.
- package/dist/__vite-browser-external-Dyvby5gX.cjs +1 -0
- package/dist/__vite-browser-external-l0sNRNKZ.js +1 -0
- package/dist/index.cjs +231 -146
- package/dist/index.js +231 -146
- package/dist/lib.d.ts +6 -4
- package/dist/utils/common.d.ts +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
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
|
|
511
|
+
const fillTasks = imageFills.map(
|
|
395
512
|
({
|
|
396
513
|
imageRef,
|
|
397
514
|
fileName,
|
|
@@ -406,7 +523,7 @@ class FigmaService {
|
|
|
406
523
|
);
|
|
407
524
|
return null;
|
|
408
525
|
}
|
|
409
|
-
return downloadAndProcessImage(
|
|
526
|
+
return () => downloadAndProcessImage(
|
|
410
527
|
fileName,
|
|
411
528
|
resolvedPath,
|
|
412
529
|
imageUrl,
|
|
@@ -417,10 +534,10 @@ class FigmaService {
|
|
|
417
534
|
);
|
|
418
535
|
}
|
|
419
536
|
).filter(
|
|
420
|
-
(
|
|
537
|
+
(task) => task !== null
|
|
421
538
|
);
|
|
422
|
-
if (
|
|
423
|
-
downloadPromises.push(
|
|
539
|
+
if (fillTasks.length > 0) {
|
|
540
|
+
downloadPromises.push(runWithConcurrency(fillTasks, CONCURRENCY_LIMIT));
|
|
424
541
|
}
|
|
425
542
|
}
|
|
426
543
|
if (renderNodes.length > 0) {
|
|
@@ -437,7 +554,7 @@ class FigmaService {
|
|
|
437
554
|
"png",
|
|
438
555
|
{ pngScale }
|
|
439
556
|
);
|
|
440
|
-
const
|
|
557
|
+
const pngTasks = pngNodes.map(
|
|
441
558
|
({
|
|
442
559
|
nodeId,
|
|
443
560
|
fileName,
|
|
@@ -452,7 +569,7 @@ class FigmaService {
|
|
|
452
569
|
);
|
|
453
570
|
return null;
|
|
454
571
|
}
|
|
455
|
-
return downloadAndProcessImage(
|
|
572
|
+
return () => downloadAndProcessImage(
|
|
456
573
|
fileName,
|
|
457
574
|
resolvedPath,
|
|
458
575
|
imageUrl,
|
|
@@ -465,10 +582,12 @@ class FigmaService {
|
|
|
465
582
|
);
|
|
466
583
|
}
|
|
467
584
|
).filter(
|
|
468
|
-
(
|
|
585
|
+
(task) => task !== null
|
|
469
586
|
);
|
|
470
|
-
if (
|
|
471
|
-
downloadPromises.push(
|
|
587
|
+
if (pngTasks.length > 0) {
|
|
588
|
+
downloadPromises.push(
|
|
589
|
+
runWithConcurrency(pngTasks, CONCURRENCY_LIMIT)
|
|
590
|
+
);
|
|
472
591
|
}
|
|
473
592
|
}
|
|
474
593
|
if (svgNodes.length > 0) {
|
|
@@ -478,7 +597,7 @@ class FigmaService {
|
|
|
478
597
|
"svg",
|
|
479
598
|
{ svgOptions }
|
|
480
599
|
);
|
|
481
|
-
const
|
|
600
|
+
const svgTasks = svgNodes.map(
|
|
482
601
|
({
|
|
483
602
|
nodeId,
|
|
484
603
|
fileName,
|
|
@@ -493,7 +612,7 @@ class FigmaService {
|
|
|
493
612
|
);
|
|
494
613
|
return null;
|
|
495
614
|
}
|
|
496
|
-
return downloadAndProcessImage(
|
|
615
|
+
return () => downloadAndProcessImage(
|
|
497
616
|
fileName,
|
|
498
617
|
resolvedPath,
|
|
499
618
|
imageUrl,
|
|
@@ -506,10 +625,12 @@ class FigmaService {
|
|
|
506
625
|
);
|
|
507
626
|
}
|
|
508
627
|
).filter(
|
|
509
|
-
(
|
|
628
|
+
(task) => task !== null
|
|
510
629
|
);
|
|
511
|
-
if (
|
|
512
|
-
downloadPromises.push(
|
|
630
|
+
if (svgTasks.length > 0) {
|
|
631
|
+
downloadPromises.push(
|
|
632
|
+
runWithConcurrency(svgTasks, CONCURRENCY_LIMIT)
|
|
633
|
+
);
|
|
513
634
|
}
|
|
514
635
|
}
|
|
515
636
|
}
|
|
@@ -541,106 +662,6 @@ class FigmaService {
|
|
|
541
662
|
return response;
|
|
542
663
|
}
|
|
543
664
|
}
|
|
544
|
-
async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
|
|
545
|
-
try {
|
|
546
|
-
const response = await fetch(imageUrl, {
|
|
547
|
-
method: "GET"
|
|
548
|
-
});
|
|
549
|
-
if (!response.ok) {
|
|
550
|
-
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
551
|
-
}
|
|
552
|
-
if (returnBuffer) {
|
|
553
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
554
|
-
return arrayBuffer;
|
|
555
|
-
}
|
|
556
|
-
if (!fs.existsSync(localPath)) {
|
|
557
|
-
fs.mkdirSync(localPath, { recursive: true });
|
|
558
|
-
}
|
|
559
|
-
const fullPath = path.join(localPath, fileName);
|
|
560
|
-
const reader = response.body?.getReader();
|
|
561
|
-
if (!reader) {
|
|
562
|
-
throw new Error("Failed to get response body");
|
|
563
|
-
}
|
|
564
|
-
const writer = fs.createWriteStream(fullPath);
|
|
565
|
-
return new Promise((resolve, reject) => {
|
|
566
|
-
const processStream = async () => {
|
|
567
|
-
try {
|
|
568
|
-
while (true) {
|
|
569
|
-
const { done, value } = await reader.read();
|
|
570
|
-
if (done) {
|
|
571
|
-
writer.end();
|
|
572
|
-
break;
|
|
573
|
-
}
|
|
574
|
-
writer.write(value);
|
|
575
|
-
}
|
|
576
|
-
} catch (err) {
|
|
577
|
-
writer.end();
|
|
578
|
-
fs.unlink(fullPath, () => {
|
|
579
|
-
});
|
|
580
|
-
reject(err);
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
writer.on("finish", () => {
|
|
584
|
-
resolve(fullPath);
|
|
585
|
-
});
|
|
586
|
-
writer.on("error", (err) => {
|
|
587
|
-
reader.cancel();
|
|
588
|
-
fs.unlink(fullPath, () => {
|
|
589
|
-
});
|
|
590
|
-
reject(new Error(`Failed to write image: ${err.message}`));
|
|
591
|
-
});
|
|
592
|
-
processStream();
|
|
593
|
-
});
|
|
594
|
-
} catch (error) {
|
|
595
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
596
|
-
throw new Error(`Error downloading image: ${errorMessage}`);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
function generateVarId(prefix = "var") {
|
|
600
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
601
|
-
let result = "";
|
|
602
|
-
for (let i = 0; i < 6; i++) {
|
|
603
|
-
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
604
|
-
result += chars[randomIndex];
|
|
605
|
-
}
|
|
606
|
-
return `${prefix}_${result}`;
|
|
607
|
-
}
|
|
608
|
-
function generateCSSShorthand(values, {
|
|
609
|
-
ignoreZero = true,
|
|
610
|
-
suffix = "px"
|
|
611
|
-
} = {}) {
|
|
612
|
-
const { top, right, bottom, left } = values;
|
|
613
|
-
if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) {
|
|
614
|
-
return void 0;
|
|
615
|
-
}
|
|
616
|
-
if (top === right && right === bottom && bottom === left) {
|
|
617
|
-
return `${top}${suffix}`;
|
|
618
|
-
}
|
|
619
|
-
if (right === left) {
|
|
620
|
-
if (top === bottom) {
|
|
621
|
-
return `${top}${suffix} ${right}${suffix}`;
|
|
622
|
-
}
|
|
623
|
-
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`;
|
|
624
|
-
}
|
|
625
|
-
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`;
|
|
626
|
-
}
|
|
627
|
-
function isVisible(element) {
|
|
628
|
-
return element.visible ?? true;
|
|
629
|
-
}
|
|
630
|
-
function pixelRound(num) {
|
|
631
|
-
if (isNaN(num)) {
|
|
632
|
-
throw new TypeError(`Input must be a valid number`);
|
|
633
|
-
}
|
|
634
|
-
return Number(Number(num).toFixed(2));
|
|
635
|
-
}
|
|
636
|
-
const common = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
637
|
-
__proto__: null,
|
|
638
|
-
downloadFigmaImage,
|
|
639
|
-
generateCSSShorthand,
|
|
640
|
-
generateVarId,
|
|
641
|
-
isVisible,
|
|
642
|
-
pixelRound
|
|
643
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
644
665
|
function hasValue(key, obj, typeGuard) {
|
|
645
666
|
const isObject = typeof obj === "object" && obj !== null;
|
|
646
667
|
if (!isObject || !(key in obj)) return false;
|
|
@@ -1537,14 +1558,25 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1537
1558
|
);
|
|
1538
1559
|
let rawApiResponse;
|
|
1539
1560
|
if (nodeId) {
|
|
1540
|
-
rawApiResponse = await figmaService.getRawNode(
|
|
1561
|
+
rawApiResponse = await figmaService.getRawNode(
|
|
1562
|
+
fileKey,
|
|
1563
|
+
nodeId,
|
|
1564
|
+
depth || void 0
|
|
1565
|
+
);
|
|
1541
1566
|
} else {
|
|
1542
|
-
rawApiResponse = await figmaService.getRawFile(
|
|
1567
|
+
rawApiResponse = await figmaService.getRawFile(
|
|
1568
|
+
fileKey,
|
|
1569
|
+
depth || void 0
|
|
1570
|
+
);
|
|
1543
1571
|
}
|
|
1544
|
-
const simplifiedDesign = simplifyRawFigmaObject(
|
|
1545
|
-
|
|
1546
|
-
|
|
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(
|
|
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(
|
|
1614
|
+
Logger.log(
|
|
1615
|
+
`Successfully downloaded ${downloadResults.length} images as buffers`
|
|
1616
|
+
);
|
|
1581
1617
|
} else {
|
|
1582
|
-
result.nodes = enrichNodesWithImages(
|
|
1583
|
-
|
|
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 {
|
|
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(
|
|
1625
|
-
|
|
1626
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1681
|
-
|
|
1682
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1807
|
+
return null;
|
|
1730
1808
|
}
|
|
1731
|
-
const imageMetadata = metadata.images.find(
|
|
1809
|
+
const imageMetadata = metadata.images.find(
|
|
1810
|
+
(img) => img.nodeId === asset.id
|
|
1811
|
+
);
|
|
1732
1812
|
return {
|
|
1733
1813
|
filePath,
|
|
1734
|
-
finalDimensions: imageMetadata?.finalDimensions || {
|
|
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
|
|
509
|
+
const fillTasks = imageFills.map(
|
|
393
510
|
({
|
|
394
511
|
imageRef,
|
|
395
512
|
fileName,
|
|
@@ -404,7 +521,7 @@ class FigmaService {
|
|
|
404
521
|
);
|
|
405
522
|
return null;
|
|
406
523
|
}
|
|
407
|
-
return downloadAndProcessImage(
|
|
524
|
+
return () => downloadAndProcessImage(
|
|
408
525
|
fileName,
|
|
409
526
|
resolvedPath,
|
|
410
527
|
imageUrl,
|
|
@@ -415,10 +532,10 @@ class FigmaService {
|
|
|
415
532
|
);
|
|
416
533
|
}
|
|
417
534
|
).filter(
|
|
418
|
-
(
|
|
535
|
+
(task) => task !== null
|
|
419
536
|
);
|
|
420
|
-
if (
|
|
421
|
-
downloadPromises.push(
|
|
537
|
+
if (fillTasks.length > 0) {
|
|
538
|
+
downloadPromises.push(runWithConcurrency(fillTasks, CONCURRENCY_LIMIT));
|
|
422
539
|
}
|
|
423
540
|
}
|
|
424
541
|
if (renderNodes.length > 0) {
|
|
@@ -435,7 +552,7 @@ class FigmaService {
|
|
|
435
552
|
"png",
|
|
436
553
|
{ pngScale }
|
|
437
554
|
);
|
|
438
|
-
const
|
|
555
|
+
const pngTasks = pngNodes.map(
|
|
439
556
|
({
|
|
440
557
|
nodeId,
|
|
441
558
|
fileName,
|
|
@@ -450,7 +567,7 @@ class FigmaService {
|
|
|
450
567
|
);
|
|
451
568
|
return null;
|
|
452
569
|
}
|
|
453
|
-
return downloadAndProcessImage(
|
|
570
|
+
return () => downloadAndProcessImage(
|
|
454
571
|
fileName,
|
|
455
572
|
resolvedPath,
|
|
456
573
|
imageUrl,
|
|
@@ -463,10 +580,12 @@ class FigmaService {
|
|
|
463
580
|
);
|
|
464
581
|
}
|
|
465
582
|
).filter(
|
|
466
|
-
(
|
|
583
|
+
(task) => task !== null
|
|
467
584
|
);
|
|
468
|
-
if (
|
|
469
|
-
downloadPromises.push(
|
|
585
|
+
if (pngTasks.length > 0) {
|
|
586
|
+
downloadPromises.push(
|
|
587
|
+
runWithConcurrency(pngTasks, CONCURRENCY_LIMIT)
|
|
588
|
+
);
|
|
470
589
|
}
|
|
471
590
|
}
|
|
472
591
|
if (svgNodes.length > 0) {
|
|
@@ -476,7 +595,7 @@ class FigmaService {
|
|
|
476
595
|
"svg",
|
|
477
596
|
{ svgOptions }
|
|
478
597
|
);
|
|
479
|
-
const
|
|
598
|
+
const svgTasks = svgNodes.map(
|
|
480
599
|
({
|
|
481
600
|
nodeId,
|
|
482
601
|
fileName,
|
|
@@ -491,7 +610,7 @@ class FigmaService {
|
|
|
491
610
|
);
|
|
492
611
|
return null;
|
|
493
612
|
}
|
|
494
|
-
return downloadAndProcessImage(
|
|
613
|
+
return () => downloadAndProcessImage(
|
|
495
614
|
fileName,
|
|
496
615
|
resolvedPath,
|
|
497
616
|
imageUrl,
|
|
@@ -504,10 +623,12 @@ class FigmaService {
|
|
|
504
623
|
);
|
|
505
624
|
}
|
|
506
625
|
).filter(
|
|
507
|
-
(
|
|
626
|
+
(task) => task !== null
|
|
508
627
|
);
|
|
509
|
-
if (
|
|
510
|
-
downloadPromises.push(
|
|
628
|
+
if (svgTasks.length > 0) {
|
|
629
|
+
downloadPromises.push(
|
|
630
|
+
runWithConcurrency(svgTasks, CONCURRENCY_LIMIT)
|
|
631
|
+
);
|
|
511
632
|
}
|
|
512
633
|
}
|
|
513
634
|
}
|
|
@@ -539,106 +660,6 @@ class FigmaService {
|
|
|
539
660
|
return response;
|
|
540
661
|
}
|
|
541
662
|
}
|
|
542
|
-
async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
|
|
543
|
-
try {
|
|
544
|
-
const response = await fetch(imageUrl, {
|
|
545
|
-
method: "GET"
|
|
546
|
-
});
|
|
547
|
-
if (!response.ok) {
|
|
548
|
-
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
549
|
-
}
|
|
550
|
-
if (returnBuffer) {
|
|
551
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
552
|
-
return arrayBuffer;
|
|
553
|
-
}
|
|
554
|
-
if (!fs.existsSync(localPath)) {
|
|
555
|
-
fs.mkdirSync(localPath, { recursive: true });
|
|
556
|
-
}
|
|
557
|
-
const fullPath = path.join(localPath, fileName);
|
|
558
|
-
const reader = response.body?.getReader();
|
|
559
|
-
if (!reader) {
|
|
560
|
-
throw new Error("Failed to get response body");
|
|
561
|
-
}
|
|
562
|
-
const writer = fs.createWriteStream(fullPath);
|
|
563
|
-
return new Promise((resolve, reject) => {
|
|
564
|
-
const processStream = async () => {
|
|
565
|
-
try {
|
|
566
|
-
while (true) {
|
|
567
|
-
const { done, value } = await reader.read();
|
|
568
|
-
if (done) {
|
|
569
|
-
writer.end();
|
|
570
|
-
break;
|
|
571
|
-
}
|
|
572
|
-
writer.write(value);
|
|
573
|
-
}
|
|
574
|
-
} catch (err) {
|
|
575
|
-
writer.end();
|
|
576
|
-
fs.unlink(fullPath, () => {
|
|
577
|
-
});
|
|
578
|
-
reject(err);
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
writer.on("finish", () => {
|
|
582
|
-
resolve(fullPath);
|
|
583
|
-
});
|
|
584
|
-
writer.on("error", (err) => {
|
|
585
|
-
reader.cancel();
|
|
586
|
-
fs.unlink(fullPath, () => {
|
|
587
|
-
});
|
|
588
|
-
reject(new Error(`Failed to write image: ${err.message}`));
|
|
589
|
-
});
|
|
590
|
-
processStream();
|
|
591
|
-
});
|
|
592
|
-
} catch (error) {
|
|
593
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
594
|
-
throw new Error(`Error downloading image: ${errorMessage}`);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
function generateVarId(prefix = "var") {
|
|
598
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
599
|
-
let result = "";
|
|
600
|
-
for (let i = 0; i < 6; i++) {
|
|
601
|
-
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
602
|
-
result += chars[randomIndex];
|
|
603
|
-
}
|
|
604
|
-
return `${prefix}_${result}`;
|
|
605
|
-
}
|
|
606
|
-
function generateCSSShorthand(values, {
|
|
607
|
-
ignoreZero = true,
|
|
608
|
-
suffix = "px"
|
|
609
|
-
} = {}) {
|
|
610
|
-
const { top, right, bottom, left } = values;
|
|
611
|
-
if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) {
|
|
612
|
-
return void 0;
|
|
613
|
-
}
|
|
614
|
-
if (top === right && right === bottom && bottom === left) {
|
|
615
|
-
return `${top}${suffix}`;
|
|
616
|
-
}
|
|
617
|
-
if (right === left) {
|
|
618
|
-
if (top === bottom) {
|
|
619
|
-
return `${top}${suffix} ${right}${suffix}`;
|
|
620
|
-
}
|
|
621
|
-
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`;
|
|
622
|
-
}
|
|
623
|
-
return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`;
|
|
624
|
-
}
|
|
625
|
-
function isVisible(element) {
|
|
626
|
-
return element.visible ?? true;
|
|
627
|
-
}
|
|
628
|
-
function pixelRound(num) {
|
|
629
|
-
if (isNaN(num)) {
|
|
630
|
-
throw new TypeError(`Input must be a valid number`);
|
|
631
|
-
}
|
|
632
|
-
return Number(Number(num).toFixed(2));
|
|
633
|
-
}
|
|
634
|
-
const common = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
635
|
-
__proto__: null,
|
|
636
|
-
downloadFigmaImage,
|
|
637
|
-
generateCSSShorthand,
|
|
638
|
-
generateVarId,
|
|
639
|
-
isVisible,
|
|
640
|
-
pixelRound
|
|
641
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
642
663
|
function hasValue(key, obj, typeGuard) {
|
|
643
664
|
const isObject = typeof obj === "object" && obj !== null;
|
|
644
665
|
if (!isObject || !(key in obj)) return false;
|
|
@@ -1535,14 +1556,25 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1535
1556
|
);
|
|
1536
1557
|
let rawApiResponse;
|
|
1537
1558
|
if (nodeId) {
|
|
1538
|
-
rawApiResponse = await figmaService.getRawNode(
|
|
1559
|
+
rawApiResponse = await figmaService.getRawNode(
|
|
1560
|
+
fileKey,
|
|
1561
|
+
nodeId,
|
|
1562
|
+
depth || void 0
|
|
1563
|
+
);
|
|
1539
1564
|
} else {
|
|
1540
|
-
rawApiResponse = await figmaService.getRawFile(
|
|
1565
|
+
rawApiResponse = await figmaService.getRawFile(
|
|
1566
|
+
fileKey,
|
|
1567
|
+
depth || void 0
|
|
1568
|
+
);
|
|
1541
1569
|
}
|
|
1542
|
-
const simplifiedDesign = simplifyRawFigmaObject(
|
|
1543
|
-
|
|
1544
|
-
|
|
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(
|
|
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(
|
|
1612
|
+
Logger.log(
|
|
1613
|
+
`Successfully downloaded ${downloadResults.length} images as buffers`
|
|
1614
|
+
);
|
|
1579
1615
|
} else {
|
|
1580
|
-
result.nodes = enrichNodesWithImages(
|
|
1581
|
-
|
|
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 {
|
|
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(
|
|
1623
|
-
|
|
1624
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1679
|
-
|
|
1680
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1805
|
+
return null;
|
|
1728
1806
|
}
|
|
1729
|
-
const imageMetadata = metadata.images.find(
|
|
1807
|
+
const imageMetadata = metadata.images.find(
|
|
1808
|
+
(img) => img.nodeId === asset.id
|
|
1809
|
+
);
|
|
1730
1810
|
return {
|
|
1731
1811
|
filePath,
|
|
1732
|
-
finalDimensions: imageMetadata?.finalDimensions || {
|
|
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?:
|
|
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?:
|
|
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:
|
|
102
|
+
outputFormat: "json";
|
|
103
103
|
}): Promise<string>;
|
|
104
104
|
export declare function getFigmaMetadata(figmaUrl: string, options: FigmaMetadataOptions & {
|
|
105
|
-
outputFormat:
|
|
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/dist/utils/common.d.ts
CHANGED
|
@@ -68,3 +68,10 @@ export declare function isVisible(element: {
|
|
|
68
68
|
* @throws TypeError If the input is not a valid number
|
|
69
69
|
*/
|
|
70
70
|
export declare function pixelRound(num: number): number;
|
|
71
|
+
/**
|
|
72
|
+
* Run a list of async tasks with a concurrency limit
|
|
73
|
+
* @param tasks - Array of functions that return a Promise
|
|
74
|
+
* @param limit - Maximum number of concurrent tasks
|
|
75
|
+
* @returns Promise resolving to array of results
|
|
76
|
+
*/
|
|
77
|
+
export declare function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: number): Promise<T[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-metadata-extractor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.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",
|