figma-metadata-extractor 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ npm install figma-metadata-extractor
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { getFigmaMetadata, downloadFigmaImages } from 'figma-metadata-extractor';
14
+ import { getFigmaMetadata, downloadFigmaImages, downloadFigmaFrameImage } from 'figma-metadata-extractor';
15
15
 
16
16
  // Extract metadata from a Figma file
17
17
  const metadata = await getFigmaMetadata(
@@ -45,6 +45,20 @@ const images = await downloadFigmaImages(
45
45
  );
46
46
 
47
47
  console.log(images); // Array of download results
48
+
49
+ // Download a single frame image from a Figma URL
50
+ const frameImage = await downloadFigmaFrameImage(
51
+ 'https://figma.com/file/ABC123/My-Design?node-id=1234-5678',
52
+ {
53
+ apiKey: 'your-figma-api-key',
54
+ localPath: './assets/frames',
55
+ fileName: 'my-frame.png',
56
+ format: 'png', // or 'svg'
57
+ pngScale: 2
58
+ }
59
+ );
60
+
61
+ console.log(frameImage.filePath); // Path to downloaded image
48
62
  ```
49
63
 
50
64
  ## API Reference
@@ -90,6 +104,25 @@ Downloads SVG and PNG images from a Figma file.
90
104
 
91
105
  **Returns:** Promise<FigmaImageResult[]>
92
106
 
107
+ ### `downloadFigmaFrameImage(figmaUrl, options)`
108
+
109
+ Downloads a single frame image from a Figma URL that contains a node-id parameter.
110
+
111
+ **Parameters:**
112
+ - `figmaUrl` (string): The Figma URL with node-id parameter (e.g., `https://figma.com/file/ABC123/My-Design?node-id=1234-5678`)
113
+ - `options` (FigmaFrameImageOptions): Configuration options
114
+
115
+ **Options:**
116
+ - `apiKey?: string` - Figma API key (Personal Access Token)
117
+ - `oauthToken?: string` - Figma OAuth Bearer token
118
+ - `useOAuth?: boolean` - Whether to use OAuth instead of API key
119
+ - `localPath: string` - Absolute path to save the image
120
+ - `fileName: string` - Local filename (must end with .png or .svg)
121
+ - `format?: 'png' | 'svg'` - Image format to download (default: 'png')
122
+ - `pngScale?: number` - Export scale for PNG images (default: 2)
123
+
124
+ **Returns:** Promise<FigmaImageResult>
125
+
93
126
  ## Authentication
94
127
 
95
128
  You need either a Figma API key or OAuth token:
@@ -104,6 +137,50 @@ You need either a Figma API key or OAuth token:
104
137
  2. Use the bearer token in the `oauthToken` option
105
138
  3. Set `useOAuth: true`
106
139
 
140
+ ## Usage Examples
141
+
142
+ ### Download Frame Image from Figma URL
143
+
144
+ The easiest way to download a frame image is to copy the Figma URL directly from your browser when viewing a specific frame:
145
+
146
+ ```typescript
147
+ import { downloadFigmaFrameImage } from 'figma-metadata-extractor';
148
+
149
+ // Copy this URL from Figma when viewing a frame
150
+ const figmaUrl = 'https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678&t=xyz123';
151
+
152
+ const result = await downloadFigmaFrameImage(figmaUrl, {
153
+ apiKey: 'your-figma-api-key',
154
+ localPath: './downloads',
155
+ fileName: 'my-frame.png',
156
+ format: 'png',
157
+ pngScale: 2 // High resolution
158
+ });
159
+
160
+ console.log(`Downloaded to: ${result.filePath}`);
161
+ console.log(`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`);
162
+ ```
163
+
164
+ ### Download Multiple Frame Images
165
+
166
+ ```typescript
167
+ import { downloadFigmaImages } from 'figma-metadata-extractor';
168
+
169
+ // For multiple frames, use the batch download function
170
+ const results = await downloadFigmaImages(
171
+ 'https://figma.com/file/ABC123/My-Design',
172
+ [
173
+ { nodeId: '1234:5678', fileName: 'frame1.png' },
174
+ { nodeId: '9876:5432', fileName: 'frame2.svg' },
175
+ { nodeId: '1111:2222', fileName: 'frame3.png' }
176
+ ],
177
+ {
178
+ apiKey: 'your-figma-api-key',
179
+ localPath: './frames'
180
+ }
181
+ );
182
+ ```
183
+
107
184
  ## Advanced Usage
108
185
 
109
186
  The library also exports the underlying extractor system for custom processing:
@@ -0,0 +1,56 @@
1
+ import type { ExtractorFn, SimplifiedNode } from "./types.js";
2
+ import type { Node as FigmaDocumentNode } from "@figma/rest-api-spec";
3
+ /**
4
+ * Extracts layout-related properties from a node.
5
+ */
6
+ export declare const layoutExtractor: ExtractorFn;
7
+ /**
8
+ * Extracts text content and text styling from a node.
9
+ */
10
+ export declare const textExtractor: ExtractorFn;
11
+ /**
12
+ * Extracts visual appearance properties (fills, strokes, effects, opacity, border radius).
13
+ */
14
+ export declare const visualsExtractor: ExtractorFn;
15
+ /**
16
+ * Extracts component-related properties from INSTANCE nodes.
17
+ */
18
+ export declare const componentExtractor: ExtractorFn;
19
+ /**
20
+ * All extractors - replicates the current parseNode behavior.
21
+ */
22
+ export declare const allExtractors: ExtractorFn[];
23
+ /**
24
+ * Layout and text only - useful for content analysis and layout planning.
25
+ */
26
+ export declare const layoutAndText: ExtractorFn[];
27
+ /**
28
+ * Text content only - useful for content audits and copy extraction.
29
+ */
30
+ export declare const contentOnly: ExtractorFn[];
31
+ /**
32
+ * Visuals only - useful for design system analysis and style extraction.
33
+ */
34
+ export declare const visualsOnly: ExtractorFn[];
35
+ /**
36
+ * Layout only - useful for structure analysis.
37
+ */
38
+ export declare const layoutOnly: ExtractorFn[];
39
+ /**
40
+ * Node types that can be exported as SVG images.
41
+ * When a FRAME, GROUP, or INSTANCE contains only these types, we can collapse it to IMAGE-SVG.
42
+ * Note: FRAME/GROUP/INSTANCE are NOT included here—they're only eligible if collapsed to IMAGE-SVG.
43
+ */
44
+ export declare const SVG_ELIGIBLE_TYPES: Set<string>;
45
+ /**
46
+ * afterChildren callback that collapses SVG-heavy containers to IMAGE-SVG.
47
+ *
48
+ * If a FRAME, GROUP, or INSTANCE contains only SVG-eligible children, the parent
49
+ * is marked as IMAGE-SVG and children are omitted, reducing payload size.
50
+ *
51
+ * @param node - Original Figma node
52
+ * @param result - SimplifiedNode being built
53
+ * @param children - Processed children
54
+ * @returns Children to include (empty array if collapsed)
55
+ */
56
+ export declare function collapseSvgContainers(node: FigmaDocumentNode, result: SimplifiedNode, children: SimplifiedNode[]): SimplifiedNode[];
@@ -0,0 +1,6 @@
1
+ import type { GetFileResponse, GetFileNodesResponse } from "@figma/rest-api-spec";
2
+ import type { ExtractorFn, TraversalOptions, SimplifiedDesign } from "./types.js";
3
+ /**
4
+ * Extract a complete SimplifiedDesign from raw Figma API response using extractors.
5
+ */
6
+ export declare function simplifyRawFigmaObject(apiResponse: GetFileResponse | GetFileNodesResponse, nodeExtractors: ExtractorFn[], options?: TraversalOptions): SimplifiedDesign;
@@ -0,0 +1,4 @@
1
+ export type { ExtractorFn, TraversalContext, TraversalOptions, GlobalVars, StyleTypes, } from "./types.js";
2
+ export { extractFromDesign } from "./node-walker.js";
3
+ export { simplifyRawFigmaObject } from "./design-extractor.js";
4
+ export { layoutExtractor, textExtractor, visualsExtractor, componentExtractor, allExtractors, layoutAndText, contentOnly, visualsOnly, layoutOnly, collapseSvgContainers, SVG_ELIGIBLE_TYPES, } from "./built-in.js";
@@ -0,0 +1,15 @@
1
+ import type { Node as FigmaDocumentNode } from "@figma/rest-api-spec";
2
+ import type { ExtractorFn, TraversalOptions, GlobalVars, SimplifiedNode } from "./types.js";
3
+ /**
4
+ * Extract data from Figma nodes using a flexible, single-pass approach.
5
+ *
6
+ * @param nodes - The Figma nodes to process
7
+ * @param extractors - Array of extractor functions to apply during traversal
8
+ * @param options - Traversal options (filtering, depth limits, etc.)
9
+ * @param globalVars - Global variables for style deduplication
10
+ * @returns Object containing processed nodes and updated global variables
11
+ */
12
+ export declare function extractFromDesign(nodes: FigmaDocumentNode[], extractors: ExtractorFn[], options?: TraversalOptions, globalVars?: GlobalVars): {
13
+ nodes: SimplifiedNode[];
14
+ globalVars: GlobalVars;
15
+ };
@@ -0,0 +1,72 @@
1
+ import type { Node as FigmaDocumentNode, Style } from "@figma/rest-api-spec";
2
+ import type { SimplifiedTextStyle } from "~/transformers/text.js";
3
+ import type { SimplifiedLayout } from "~/transformers/layout.js";
4
+ import type { SimplifiedFill, SimplifiedStroke } from "~/transformers/style.js";
5
+ import type { SimplifiedEffects } from "~/transformers/effects.js";
6
+ import type { ComponentProperties, SimplifiedComponentDefinition, SimplifiedComponentSetDefinition } from "~/transformers/component.js";
7
+ export type StyleTypes = SimplifiedTextStyle | SimplifiedFill[] | SimplifiedLayout | SimplifiedStroke | SimplifiedEffects | string;
8
+ export type GlobalVars = {
9
+ styles: Record<string, StyleTypes>;
10
+ };
11
+ export interface TraversalContext {
12
+ globalVars: GlobalVars & {
13
+ extraStyles?: Record<string, Style>;
14
+ };
15
+ currentDepth: number;
16
+ parent?: FigmaDocumentNode;
17
+ }
18
+ export interface TraversalOptions {
19
+ maxDepth?: number;
20
+ nodeFilter?: (node: FigmaDocumentNode) => boolean;
21
+ /**
22
+ * Called after children are processed, allowing modification of the parent node
23
+ * and control over which children to include in the output.
24
+ *
25
+ * @param node - Original Figma node
26
+ * @param result - SimplifiedNode being built (can be mutated)
27
+ * @param children - Processed children
28
+ * @returns Children to include (return empty array to omit children)
29
+ */
30
+ afterChildren?: (node: FigmaDocumentNode, result: SimplifiedNode, children: SimplifiedNode[]) => SimplifiedNode[];
31
+ }
32
+ /**
33
+ * An extractor function that can modify a SimplifiedNode during traversal.
34
+ *
35
+ * @param node - The current Figma node being processed
36
+ * @param result - SimplifiedNode object being built—this can be mutated inside the extractor
37
+ * @param context - Traversal context including globalVars and parent info. This can also be mutated inside the extractor.
38
+ */
39
+ export type ExtractorFn = (node: FigmaDocumentNode, result: SimplifiedNode, context: TraversalContext) => void;
40
+ export interface SimplifiedDesign {
41
+ name: string;
42
+ nodes: SimplifiedNode[];
43
+ components: Record<string, SimplifiedComponentDefinition>;
44
+ componentSets: Record<string, SimplifiedComponentSetDefinition>;
45
+ globalVars: GlobalVars;
46
+ }
47
+ export interface SimplifiedNode {
48
+ id: string;
49
+ name: string;
50
+ type: string;
51
+ text?: string;
52
+ textStyle?: string;
53
+ fills?: string;
54
+ styles?: string;
55
+ strokes?: string;
56
+ strokeWeight?: string;
57
+ strokeDashes?: number[];
58
+ strokeWeights?: string;
59
+ effects?: string;
60
+ opacity?: number;
61
+ borderRadius?: string;
62
+ layout?: string;
63
+ componentId?: string;
64
+ componentProperties?: ComponentProperties[];
65
+ children?: SimplifiedNode[];
66
+ }
67
+ export interface BoundingBox {
68
+ x: number;
69
+ y: number;
70
+ width: number;
71
+ height: number;
72
+ }
package/dist/index.cjs CHANGED
@@ -1446,10 +1446,62 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
1446
1446
  throw new Error(`Failed to download images: ${error instanceof Error ? error.message : String(error)}`);
1447
1447
  }
1448
1448
  }
1449
+ async function downloadFigmaFrameImage(figmaUrl, options) {
1450
+ const {
1451
+ apiKey,
1452
+ oauthToken,
1453
+ useOAuth = false,
1454
+ pngScale = 2,
1455
+ localPath,
1456
+ fileName,
1457
+ format = "png"
1458
+ } = options;
1459
+ if (!apiKey && !oauthToken) {
1460
+ throw new Error("Either apiKey or oauthToken is required");
1461
+ }
1462
+ const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1463
+ if (!urlMatch) {
1464
+ throw new Error("Invalid Figma URL format");
1465
+ }
1466
+ const fileKey = urlMatch[2];
1467
+ const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
1468
+ if (!nodeIdMatch) {
1469
+ 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)");
1470
+ }
1471
+ const nodeId = nodeIdMatch[1].replace(/-/g, ":");
1472
+ const expectedExtension = `.${format}`;
1473
+ if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1474
+ throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1475
+ }
1476
+ const figmaService = new FigmaService({
1477
+ figmaApiKey: apiKey || "",
1478
+ figmaOAuthToken: oauthToken || "",
1479
+ useOAuth: useOAuth && !!oauthToken
1480
+ });
1481
+ try {
1482
+ Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
1483
+ const imageNode = {
1484
+ nodeId,
1485
+ fileName
1486
+ };
1487
+ const results = await figmaService.downloadImages(fileKey, localPath, [imageNode], {
1488
+ pngScale: format === "png" ? pngScale : void 0
1489
+ });
1490
+ if (results.length === 0) {
1491
+ throw new Error(`Failed to download image for frame ${nodeId}`);
1492
+ }
1493
+ Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1494
+ return results[0];
1495
+ } catch (error) {
1496
+ Logger.error(`Error downloading frame image from ${fileKey}:`, error);
1497
+ throw new Error(`Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`);
1498
+ }
1499
+ }
1449
1500
  exports.allExtractors = allExtractors;
1450
1501
  exports.collapseSvgContainers = collapseSvgContainers;
1451
1502
  exports.componentExtractor = componentExtractor;
1452
1503
  exports.contentOnly = contentOnly;
1504
+ exports.downloadFigmaFrameImage = downloadFigmaFrameImage;
1453
1505
  exports.downloadFigmaImages = downloadFigmaImages;
1454
1506
  exports.extractFromDesign = extractFromDesign;
1455
1507
  exports.getFigmaMetadata = getFigmaMetadata;
@@ -0,0 +1,4 @@
1
+ export { getFigmaMetadata, downloadFigmaImages, downloadFigmaFrameImage, type FigmaMetadataOptions, type FigmaImageOptions, type FigmaFrameImageOptions, type FigmaImageNode, type FigmaMetadataResult, type FigmaImageResult, } from "./lib.js";
2
+ export type { SimplifiedDesign } from "./extractors/types.js";
3
+ export type { ExtractorFn, TraversalContext, TraversalOptions, GlobalVars, StyleTypes, } from "./extractors/index.js";
4
+ export { extractFromDesign, simplifyRawFigmaObject, layoutExtractor, textExtractor, visualsExtractor, componentExtractor, allExtractors, layoutAndText, contentOnly, visualsOnly, layoutOnly, collapseSvgContainers, } from "./extractors/index.js";
package/dist/index.js CHANGED
@@ -1444,11 +1444,63 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
1444
1444
  throw new Error(`Failed to download images: ${error instanceof Error ? error.message : String(error)}`);
1445
1445
  }
1446
1446
  }
1447
+ async function downloadFigmaFrameImage(figmaUrl, options) {
1448
+ const {
1449
+ apiKey,
1450
+ oauthToken,
1451
+ useOAuth = false,
1452
+ pngScale = 2,
1453
+ localPath,
1454
+ fileName,
1455
+ format = "png"
1456
+ } = options;
1457
+ if (!apiKey && !oauthToken) {
1458
+ throw new Error("Either apiKey or oauthToken is required");
1459
+ }
1460
+ const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1461
+ if (!urlMatch) {
1462
+ throw new Error("Invalid Figma URL format");
1463
+ }
1464
+ const fileKey = urlMatch[2];
1465
+ const nodeIdMatch = figmaUrl.match(/node-id=([^&]+)/);
1466
+ if (!nodeIdMatch) {
1467
+ 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)");
1468
+ }
1469
+ const nodeId = nodeIdMatch[1].replace(/-/g, ":");
1470
+ const expectedExtension = `.${format}`;
1471
+ if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1472
+ throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1473
+ }
1474
+ const figmaService = new FigmaService({
1475
+ figmaApiKey: apiKey || "",
1476
+ figmaOAuthToken: oauthToken || "",
1477
+ useOAuth: useOAuth && !!oauthToken
1478
+ });
1479
+ try {
1480
+ Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
1481
+ const imageNode = {
1482
+ nodeId,
1483
+ fileName
1484
+ };
1485
+ const results = await figmaService.downloadImages(fileKey, localPath, [imageNode], {
1486
+ pngScale: format === "png" ? pngScale : void 0
1487
+ });
1488
+ if (results.length === 0) {
1489
+ throw new Error(`Failed to download image for frame ${nodeId}`);
1490
+ }
1491
+ Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1492
+ return results[0];
1493
+ } catch (error) {
1494
+ Logger.error(`Error downloading frame image from ${fileKey}:`, error);
1495
+ throw new Error(`Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`);
1496
+ }
1497
+ }
1447
1498
  export {
1448
1499
  allExtractors,
1449
1500
  collapseSvgContainers,
1450
1501
  componentExtractor,
1451
1502
  contentOnly,
1503
+ downloadFigmaFrameImage,
1452
1504
  downloadFigmaImages,
1453
1505
  extractFromDesign,
1454
1506
  getFigmaMetadata,
package/dist/lib.d.ts ADDED
@@ -0,0 +1,92 @@
1
+ export interface FigmaMetadataOptions {
2
+ /** The Figma API key (Personal Access Token) */
3
+ apiKey?: string;
4
+ /** The Figma OAuth Bearer token */
5
+ oauthToken?: string;
6
+ /** Whether to use OAuth instead of API key */
7
+ useOAuth?: boolean;
8
+ /** Output format for the metadata */
9
+ outputFormat?: "json" | "yaml" | "object";
10
+ /** Maximum depth to traverse the node tree */
11
+ depth?: number;
12
+ }
13
+ export interface FigmaImageOptions {
14
+ /** Export scale for PNG images (defaults to 2) */
15
+ pngScale?: number;
16
+ /** The absolute path to the directory where images should be stored */
17
+ localPath: string;
18
+ }
19
+ export interface FigmaImageNode {
20
+ /** The ID of the Figma node, formatted as '1234:5678' */
21
+ nodeId: string;
22
+ /** If a node has an imageRef fill, include this variable */
23
+ imageRef?: string;
24
+ /** The local filename for saving the image (must end with .png or .svg) */
25
+ fileName: string;
26
+ /** Whether this image needs cropping based on its transform matrix */
27
+ needsCropping?: boolean;
28
+ /** Figma transform matrix for image cropping */
29
+ cropTransform?: number[][];
30
+ /** Whether this image requires dimension information for CSS variables */
31
+ requiresImageDimensions?: boolean;
32
+ /** Suffix to add to filename for unique cropped images */
33
+ filenameSuffix?: string;
34
+ }
35
+ export interface FigmaMetadataResult {
36
+ metadata: any;
37
+ nodes: any[];
38
+ globalVars: any;
39
+ }
40
+ export interface FigmaImageResult {
41
+ filePath: string;
42
+ finalDimensions: {
43
+ width: number;
44
+ height: number;
45
+ };
46
+ wasCropped: boolean;
47
+ cssVariables?: string;
48
+ }
49
+ export interface FigmaFrameImageOptions {
50
+ /** The Figma API key (Personal Access Token) */
51
+ apiKey?: string;
52
+ /** The Figma OAuth Bearer token */
53
+ oauthToken?: string;
54
+ /** Whether to use OAuth instead of API key */
55
+ useOAuth?: boolean;
56
+ /** Export scale for PNG images (defaults to 2) */
57
+ pngScale?: number;
58
+ /** The absolute path to the directory where the image should be stored */
59
+ localPath: string;
60
+ /** The filename for the downloaded image (must end with .png or .svg) */
61
+ fileName: string;
62
+ /** Image format to download (defaults to 'png') */
63
+ format?: 'png' | 'svg';
64
+ }
65
+ /**
66
+ * Extract metadata from a Figma file or specific nodes
67
+ *
68
+ * @param figmaUrl - The Figma file URL (e.g., https://figma.com/file/ABC123/...)
69
+ * @param options - Configuration options including API credentials
70
+ * @returns Promise resolving to the extracted metadata
71
+ */
72
+ export declare function getFigmaMetadata(figmaUrl: string, options?: FigmaMetadataOptions): Promise<FigmaMetadataResult | string>;
73
+ /**
74
+ * Download images from a Figma file
75
+ *
76
+ * @param figmaUrl - The Figma file URL
77
+ * @param nodes - Array of image nodes to download
78
+ * @param options - Configuration options including API credentials and local path
79
+ * @returns Promise resolving to array of download results
80
+ */
81
+ export declare function downloadFigmaImages(figmaUrl: string, nodes: FigmaImageNode[], options: FigmaMetadataOptions & FigmaImageOptions): Promise<FigmaImageResult[]>;
82
+ /**
83
+ * Download a frame image from a Figma URL
84
+ *
85
+ * @param figmaUrl - The Figma URL containing the frame (with node-id parameter)
86
+ * @param options - Configuration options including API credentials, local path, and filename
87
+ * @returns Promise resolving to the download result
88
+ */
89
+ export declare function downloadFigmaFrameImage(figmaUrl: string, options: FigmaFrameImageOptions): Promise<FigmaImageResult>;
90
+ export type { SimplifiedDesign } from "./extractors/types.js";
91
+ export type { ExtractorFn, TraversalContext, TraversalOptions, GlobalVars, StyleTypes, } from "./extractors/index.js";
92
+ export { extractFromDesign, simplifyRawFigmaObject, layoutExtractor, textExtractor, visualsExtractor, componentExtractor, allExtractors, layoutAndText, contentOnly, visualsOnly, layoutOnly, collapseSvgContainers, } from "./extractors/index.js";
@@ -0,0 +1,75 @@
1
+ import type { GetFileResponse, GetFileNodesResponse } from "@figma/rest-api-spec";
2
+ import { type ImageProcessingResult } from "~/utils/image-processing.js";
3
+ export type FigmaAuthOptions = {
4
+ figmaApiKey: string;
5
+ figmaOAuthToken: string;
6
+ useOAuth: boolean;
7
+ };
8
+ type SvgOptions = {
9
+ outlineText: boolean;
10
+ includeId: boolean;
11
+ simplifyStroke: boolean;
12
+ };
13
+ export declare class FigmaService {
14
+ private readonly apiKey;
15
+ private readonly oauthToken;
16
+ private readonly useOAuth;
17
+ private readonly baseUrl;
18
+ constructor({ figmaApiKey, figmaOAuthToken, useOAuth }: FigmaAuthOptions);
19
+ private getAuthHeaders;
20
+ /**
21
+ * Filters out null values from Figma image responses. This ensures we only work with valid image URLs.
22
+ */
23
+ private filterValidImages;
24
+ private request;
25
+ /**
26
+ * Builds URL query parameters for SVG image requests.
27
+ */
28
+ private buildSvgQueryParams;
29
+ /**
30
+ * Gets download URLs for image fills without downloading them.
31
+ *
32
+ * @returns Map of imageRef to download URL
33
+ */
34
+ getImageFillUrls(fileKey: string): Promise<Record<string, string>>;
35
+ /**
36
+ * Gets download URLs for rendered nodes without downloading them.
37
+ *
38
+ * @returns Map of node ID to download URL
39
+ */
40
+ getNodeRenderUrls(fileKey: string, nodeIds: string[], format: "png" | "svg", options?: {
41
+ pngScale?: number;
42
+ svgOptions?: SvgOptions;
43
+ }): Promise<Record<string, string>>;
44
+ /**
45
+ * Download images method with post-processing support for cropping and returning image dimensions.
46
+ *
47
+ * Supports:
48
+ * - Image fills vs rendered nodes (based on imageRef vs nodeId)
49
+ * - PNG vs SVG format (based on filename extension)
50
+ * - Image cropping based on transform matrices
51
+ * - CSS variable generation for image dimensions
52
+ *
53
+ * @returns Array of local file paths for successfully downloaded images
54
+ */
55
+ downloadImages(fileKey: string, localPath: string, items: Array<{
56
+ imageRef?: string;
57
+ nodeId?: string;
58
+ fileName: string;
59
+ needsCropping?: boolean;
60
+ cropTransform?: any;
61
+ requiresImageDimensions?: boolean;
62
+ }>, options?: {
63
+ pngScale?: number;
64
+ svgOptions?: SvgOptions;
65
+ }): Promise<ImageProcessingResult[]>;
66
+ /**
67
+ * Get raw Figma API response for a file (for use with flexible extractors)
68
+ */
69
+ getRawFile(fileKey: string, depth?: number | null): Promise<GetFileResponse>;
70
+ /**
71
+ * Get raw Figma API response for specific nodes (for use with flexible extractors)
72
+ */
73
+ getRawNode(fileKey: string, nodeId: string, depth?: number | null): Promise<GetFileNodesResponse>;
74
+ }
75
+ export {};
@@ -0,0 +1,26 @@
1
+ import type { Component, ComponentPropertyType, ComponentSet } from "@figma/rest-api-spec";
2
+ export interface ComponentProperties {
3
+ name: string;
4
+ value: string;
5
+ type: ComponentPropertyType;
6
+ }
7
+ export interface SimplifiedComponentDefinition {
8
+ id: string;
9
+ key: string;
10
+ name: string;
11
+ componentSetId?: string;
12
+ }
13
+ export interface SimplifiedComponentSetDefinition {
14
+ id: string;
15
+ key: string;
16
+ name: string;
17
+ description?: string;
18
+ }
19
+ /**
20
+ * Remove unnecessary component properties and convert to simplified format.
21
+ */
22
+ export declare function simplifyComponents(aggregatedComponents: Record<string, Component>): Record<string, SimplifiedComponentDefinition>;
23
+ /**
24
+ * Remove unnecessary component set properties and convert to simplified format.
25
+ */
26
+ export declare function simplifyComponentSets(aggregatedComponentSets: Record<string, ComponentSet>): Record<string, SimplifiedComponentSetDefinition>;
@@ -0,0 +1,8 @@
1
+ import type { Node as FigmaDocumentNode } from "@figma/rest-api-spec";
2
+ export type SimplifiedEffects = {
3
+ boxShadow?: string;
4
+ filter?: string;
5
+ backdropFilter?: string;
6
+ textShadow?: string;
7
+ };
8
+ export declare function buildSimplifiedEffects(n: FigmaDocumentNode): SimplifiedEffects;
@@ -0,0 +1,26 @@
1
+ import type { Node as FigmaDocumentNode } from "@figma/rest-api-spec";
2
+ export interface SimplifiedLayout {
3
+ mode: "none" | "row" | "column";
4
+ justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "baseline" | "stretch";
5
+ alignItems?: "flex-start" | "flex-end" | "center" | "space-between" | "baseline" | "stretch";
6
+ alignSelf?: "flex-start" | "flex-end" | "center" | "stretch";
7
+ wrap?: boolean;
8
+ gap?: string;
9
+ locationRelativeToParent?: {
10
+ x: number;
11
+ y: number;
12
+ };
13
+ dimensions?: {
14
+ width?: number;
15
+ height?: number;
16
+ aspectRatio?: number;
17
+ };
18
+ padding?: string;
19
+ sizing?: {
20
+ horizontal?: "fixed" | "fill" | "hug";
21
+ vertical?: "fixed" | "fill" | "hug";
22
+ };
23
+ overflowScroll?: ("x" | "y")[];
24
+ position?: "absolute";
25
+ }
26
+ export declare function buildSimplifiedLayout(n: FigmaDocumentNode, parent?: FigmaDocumentNode): SimplifiedLayout;
@@ -0,0 +1,119 @@
1
+ import type { Node as FigmaDocumentNode, Paint, RGBA, Transform } from "@figma/rest-api-spec";
2
+ export type CSSRGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;
3
+ export type CSSHexColor = `#${string}`;
4
+ export interface ColorValue {
5
+ hex: CSSHexColor;
6
+ opacity: number;
7
+ }
8
+ /**
9
+ * Simplified image fill with CSS properties and processing metadata
10
+ *
11
+ * This type represents an image fill that can be used as either:
12
+ * - background-image (when parent node has children)
13
+ * - <img> tag (when parent node has no children)
14
+ *
15
+ * The CSS properties are mutually exclusive based on usage context.
16
+ */
17
+ export type SimplifiedImageFill = {
18
+ type: "IMAGE";
19
+ imageRef: string;
20
+ scaleMode: "FILL" | "FIT" | "TILE" | "STRETCH";
21
+ /**
22
+ * For TILE mode, the scaling factor relative to original image size
23
+ */
24
+ scalingFactor?: number;
25
+ backgroundSize?: string;
26
+ backgroundRepeat?: string;
27
+ isBackground?: boolean;
28
+ objectFit?: string;
29
+ imageDownloadArguments?: {
30
+ /**
31
+ * Whether image needs cropping based on transform
32
+ */
33
+ needsCropping: boolean;
34
+ /**
35
+ * Whether CSS variables for dimensions are needed to calculate the background size for TILE mode
36
+ *
37
+ * Figma bases scalingFactor on the image's original size. In CSS, background size (as a percentage)
38
+ * is calculated based on the size of the container. We need to pass back the original dimensions
39
+ * after processing to calculate the intended background size when translated to code.
40
+ */
41
+ requiresImageDimensions: boolean;
42
+ /**
43
+ * Figma's transform matrix for Sharp processing
44
+ */
45
+ cropTransform?: Transform;
46
+ /**
47
+ * Suggested filename suffix to make cropped images unique
48
+ * When the same imageRef is used multiple times with different crops,
49
+ * this helps avoid overwriting conflicts
50
+ */
51
+ filenameSuffix?: string;
52
+ };
53
+ };
54
+ export type SimplifiedGradientFill = {
55
+ type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND";
56
+ gradient: string;
57
+ };
58
+ export type SimplifiedPatternFill = {
59
+ type: "PATTERN";
60
+ patternSource: {
61
+ /**
62
+ * Hardcode to expect PNG for now, consider SVG detection in the future.
63
+ *
64
+ * SVG detection is a bit challenging because the nodeId in question isn't
65
+ * guaranteed to be included in the response we're working with. No guaranteed
66
+ * way to look into it and see if it's only composed of vector shapes.
67
+ */
68
+ type: "IMAGE-PNG";
69
+ nodeId: string;
70
+ };
71
+ backgroundRepeat: string;
72
+ backgroundSize: string;
73
+ backgroundPosition: string;
74
+ };
75
+ export type SimplifiedFill = SimplifiedImageFill | SimplifiedGradientFill | SimplifiedPatternFill | CSSRGBAColor | CSSHexColor;
76
+ export type SimplifiedStroke = {
77
+ colors: SimplifiedFill[];
78
+ strokeWeight?: string;
79
+ strokeDashes?: number[];
80
+ strokeWeights?: string;
81
+ };
82
+ /**
83
+ * Build simplified stroke information from a Figma node
84
+ *
85
+ * @param n - The Figma node to extract stroke information from
86
+ * @param hasChildren - Whether the node has children (affects paint processing)
87
+ * @returns Simplified stroke object with colors and properties
88
+ */
89
+ export declare function buildSimplifiedStrokes(n: FigmaDocumentNode, hasChildren?: boolean): SimplifiedStroke;
90
+ /**
91
+ * Convert a Figma paint (solid, image, gradient) to a SimplifiedFill
92
+ * @param raw - The Figma paint to convert
93
+ * @param hasChildren - Whether the node has children (determines CSS properties)
94
+ * @returns The converted SimplifiedFill
95
+ */
96
+ export declare function parsePaint(raw: Paint, hasChildren?: boolean): SimplifiedFill;
97
+ /**
98
+ * Convert hex color value and opacity to rgba format
99
+ * @param hex - Hexadecimal color value (e.g., "#FF0000" or "#F00")
100
+ * @param opacity - Opacity value (0-1)
101
+ * @returns Color string in rgba format
102
+ */
103
+ export declare function hexToRgba(hex: string, opacity?: number): string;
104
+ /**
105
+ * Convert color from RGBA to { hex, opacity }
106
+ *
107
+ * @param color - The color to convert, including alpha channel
108
+ * @param opacity - The opacity of the color, if not included in alpha channel
109
+ * @returns The converted color
110
+ **/
111
+ export declare function convertColor(color: RGBA, opacity?: number): ColorValue;
112
+ /**
113
+ * Convert color from Figma RGBA to rgba(#, #, #, #) CSS format
114
+ *
115
+ * @param color - The color to convert, including alpha channel
116
+ * @param opacity - The opacity of the color, if not included in alpha channel
117
+ * @returns The converted color
118
+ **/
119
+ export declare function formatRGBAColor(color: RGBA, opacity?: number): CSSRGBAColor;
@@ -0,0 +1,30 @@
1
+ import type { Node as FigmaDocumentNode } from "@figma/rest-api-spec";
2
+ export type SimplifiedTextStyle = Partial<{
3
+ fontFamily: string;
4
+ fontWeight: number;
5
+ fontSize: number;
6
+ lineHeight: string;
7
+ letterSpacing: string;
8
+ textCase: string;
9
+ textAlignHorizontal: string;
10
+ textAlignVertical: string;
11
+ }>;
12
+ export declare function isTextNode(n: FigmaDocumentNode): n is Extract<FigmaDocumentNode, {
13
+ type: "TEXT";
14
+ }>;
15
+ export declare function hasTextStyle(n: FigmaDocumentNode): n is FigmaDocumentNode & {
16
+ style: Extract<FigmaDocumentNode, {
17
+ style: any;
18
+ }>["style"];
19
+ };
20
+ export declare function extractNodeText(n: FigmaDocumentNode): string | undefined;
21
+ export declare function extractTextStyle(n: FigmaDocumentNode): Partial<{
22
+ fontFamily: string;
23
+ fontWeight: number;
24
+ fontSize: number;
25
+ lineHeight: string;
26
+ letterSpacing: string;
27
+ textCase: string;
28
+ textAlignHorizontal: string;
29
+ textAlignVertical: string;
30
+ }> | undefined;
@@ -0,0 +1,69 @@
1
+ export type StyleId = `${string}_${string}` & {
2
+ __brand: "StyleId";
3
+ };
4
+ /**
5
+ * Download Figma image and save it locally
6
+ * @param fileName - The filename to save as
7
+ * @param localPath - The local path to save to
8
+ * @param imageUrl - Image URL (images[nodeId])
9
+ * @returns A Promise that resolves to the full file path where the image was saved
10
+ * @throws Error if download fails
11
+ */
12
+ export declare function downloadFigmaImage(fileName: string, localPath: string, imageUrl: string): Promise<string>;
13
+ /**
14
+ * Remove keys with empty arrays or empty objects from an object.
15
+ * @param input - The input object or value.
16
+ * @returns The processed object or the original value.
17
+ */
18
+ export declare function removeEmptyKeys<T>(input: T): T;
19
+ /**
20
+ * Generate a 6-character random variable ID
21
+ * @param prefix - ID prefix
22
+ * @returns A 6-character random ID string with prefix
23
+ */
24
+ export declare function generateVarId(prefix?: string): StyleId;
25
+ /**
26
+ * Generate a CSS shorthand for values that come with top, right, bottom, and left
27
+ *
28
+ * input: { top: 10, right: 10, bottom: 10, left: 10 }
29
+ * output: "10px"
30
+ *
31
+ * input: { top: 10, right: 20, bottom: 10, left: 20 }
32
+ * output: "10px 20px"
33
+ *
34
+ * input: { top: 10, right: 20, bottom: 30, left: 40 }
35
+ * output: "10px 20px 30px 40px"
36
+ *
37
+ * @param values - The values to generate the shorthand for
38
+ * @returns The generated shorthand
39
+ */
40
+ export declare function generateCSSShorthand(values: {
41
+ top: number;
42
+ right: number;
43
+ bottom: number;
44
+ left: number;
45
+ }, { ignoreZero, suffix, }?: {
46
+ /**
47
+ * If true and all values are 0, return undefined. Defaults to true.
48
+ */
49
+ ignoreZero?: boolean;
50
+ /**
51
+ * The suffix to add to the shorthand. Defaults to "px".
52
+ */
53
+ suffix?: string;
54
+ }): string | undefined;
55
+ /**
56
+ * Check if an element is visible
57
+ * @param element - The item to check
58
+ * @returns True if the item is visible, false otherwise
59
+ */
60
+ export declare function isVisible(element: {
61
+ visible?: boolean;
62
+ }): boolean;
63
+ /**
64
+ * Rounds a number to two decimal places, suitable for pixel value processing.
65
+ * @param num The number to be rounded.
66
+ * @returns The rounded number with two decimal places.
67
+ * @throws TypeError If the input is not a valid number
68
+ */
69
+ export declare function pixelRound(num: number): number;
@@ -0,0 +1,12 @@
1
+ type RequestOptions = RequestInit & {
2
+ /**
3
+ * Force format of headers to be a record of strings, e.g. { "Authorization": "Bearer 123" }
4
+ *
5
+ * Avoids complexity of needing to deal with `instanceof Headers`, which is not supported in some environments.
6
+ */
7
+ headers?: Record<string, string>;
8
+ };
9
+ export declare function fetchWithRetry<T extends {
10
+ status?: number;
11
+ }>(url: string, options?: RequestOptions): Promise<T>;
12
+ export {};
@@ -0,0 +1,23 @@
1
+ import type { Rectangle, HasLayoutTrait, StrokeWeights, HasFramePropertiesTrait } from "@figma/rest-api-spec";
2
+ import { isTruthy } from "remeda";
3
+ import type { CSSHexColor, CSSRGBAColor } from "~/transformers/style.js";
4
+ export { isTruthy };
5
+ export declare function hasValue<K extends PropertyKey, T>(key: K, obj: unknown, typeGuard?: (val: unknown) => val is T): obj is Record<K, T>;
6
+ export declare function isFrame(val: unknown): val is HasFramePropertiesTrait;
7
+ export declare function isLayout(val: unknown): val is HasLayoutTrait;
8
+ /**
9
+ * Checks if:
10
+ * 1. A node is a child to an auto layout frame
11
+ * 2. The child adheres to the auto layout rules—i.e. it's not absolutely positioned
12
+ *
13
+ * @param node - The node to check.
14
+ * @param parent - The parent node.
15
+ * @returns True if the node is a child of an auto layout frame, false otherwise.
16
+ */
17
+ export declare function isInAutoLayoutFlow(node: unknown, parent: unknown): boolean;
18
+ export declare function isStrokeWeights(val: unknown): val is StrokeWeights;
19
+ export declare function isRectangle<T, K extends string>(key: K, obj: T): obj is T & {
20
+ [P in K]: Rectangle;
21
+ };
22
+ export declare function isRectangleCornerRadii(val: unknown): val is number[];
23
+ export declare function isCSSColorValue(val: unknown): val is CSSRGBAColor | CSSHexColor;
@@ -0,0 +1,57 @@
1
+ import type { Transform } from "@figma/rest-api-spec";
2
+ /**
3
+ * Apply crop transform to an image based on Figma's transformation matrix
4
+ * @param imagePath - Path to the original image file
5
+ * @param cropTransform - Figma transform matrix [[scaleX, skewX, translateX], [skewY, scaleY, translateY]]
6
+ * @returns Promise<string> - Path to the cropped image
7
+ */
8
+ export declare function applyCropTransform(imagePath: string, cropTransform: Transform): Promise<string>;
9
+ /**
10
+ * Get image dimensions from a file
11
+ * @param imagePath - Path to the image file
12
+ * @returns Promise<{width: number, height: number}>
13
+ */
14
+ export declare function getImageDimensions(imagePath: string): Promise<{
15
+ width: number;
16
+ height: number;
17
+ }>;
18
+ export type ImageProcessingResult = {
19
+ filePath: string;
20
+ originalDimensions: {
21
+ width: number;
22
+ height: number;
23
+ };
24
+ finalDimensions: {
25
+ width: number;
26
+ height: number;
27
+ };
28
+ wasCropped: boolean;
29
+ cropRegion?: {
30
+ left: number;
31
+ top: number;
32
+ width: number;
33
+ height: number;
34
+ };
35
+ cssVariables?: string;
36
+ processingLog: string[];
37
+ };
38
+ /**
39
+ * Enhanced image download with post-processing
40
+ * @param fileName - The filename to save as
41
+ * @param localPath - The local path to save to
42
+ * @param imageUrl - Image URL
43
+ * @param needsCropping - Whether to apply crop transform
44
+ * @param cropTransform - Transform matrix for cropping
45
+ * @param requiresImageDimensions - Whether to generate dimension metadata
46
+ * @returns Promise<ImageProcessingResult> - Detailed processing information
47
+ */
48
+ export declare function downloadAndProcessImage(fileName: string, localPath: string, imageUrl: string, needsCropping?: boolean, cropTransform?: Transform, requiresImageDimensions?: boolean): Promise<ImageProcessingResult>;
49
+ /**
50
+ * Create CSS custom properties for image dimensions
51
+ * @param imagePath - Path to the image file
52
+ * @returns Promise<string> - CSS custom properties
53
+ */
54
+ export declare function generateImageCSSVariables({ width, height, }: {
55
+ width: number;
56
+ height: number;
57
+ }): string;
@@ -0,0 +1,6 @@
1
+ export declare const Logger: {
2
+ isHTTP: boolean;
3
+ log: (...args: any[]) => void;
4
+ error: (...args: any[]) => void;
5
+ };
6
+ export declare function writeLogs(name: string, value: any): void;
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "figma-metadata-extractor",
3
- "version": "1.0.0",
4
- "description": "Extract metadata and download images from Figma files. A standalone library for accessing Figma design data programmatically.",
3
+ "version": "1.0.2",
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",
7
7
  "module": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
+ "types": "./dist/index.d.ts",
11
12
  "import": "./dist/index.js",
12
- "require": "./dist/index.cjs",
13
- "types": "./dist/index.d.ts"
13
+ "require": "./dist/index.cjs"
14
14
  }
15
15
  },
16
16
  "files": [
@@ -19,7 +19,7 @@
19
19
  "example.js"
20
20
  ],
21
21
  "scripts": {
22
- "build": "vite build && tsc --emitDeclarationOnly --outDir dist",
22
+ "build": "vite build && tsc --project tsconfig.declarations.json",
23
23
  "typecheck": "tsc --noEmit",
24
24
  "test": "jest",
25
25
  "dev": "vite build --watch",