Skip to content

Commit 9966623

Browse files
authored
Collapse containers that only have vector children to better handle SVG image downloads and also make output size smaller. (#250)
* Don't include thumbnail link or last modified date in simplified output. * Collapse containers that only have vector children to better handle SVG image downloads and also make output size smaller.
1 parent 927f2c1 commit 9966623

File tree

7 files changed

+94
-9
lines changed

7 files changed

+94
-9
lines changed

.changeset/evil-tools-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"figma-developer-mcp": patch
3+
---
4+
5+
Collapse containers that only have vector children to better handle SVG image downloads and also make output size smaller.

src/extractors/built-in.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { ExtractorFn, GlobalVars, StyleTypes, TraversalContext } from "./types.js";
1+
import type {
2+
ExtractorFn,
3+
GlobalVars,
4+
StyleTypes,
5+
TraversalContext,
6+
SimplifiedNode,
7+
} from "./types.js";
28
import { buildSimplifiedLayout } from "~/transformers/layout.js";
39
import { buildSimplifiedStrokes, parsePaint } from "~/transformers/style.js";
410
import { buildSimplifiedEffects } from "~/transformers/effects.js";
@@ -196,3 +202,52 @@ export const visualsOnly = [visualsExtractor];
196202
* Layout only - useful for structure analysis.
197203
*/
198204
export const layoutOnly = [layoutExtractor];
205+
206+
// -------------------- AFTER CHILDREN HELPERS --------------------
207+
208+
/**
209+
* Node types that can be exported as SVG images.
210+
* When a FRAME, GROUP, or INSTANCE contains only these types, we can collapse it to IMAGE-SVG.
211+
* Note: FRAME/GROUP/INSTANCE are NOT included here—they're only eligible if collapsed to IMAGE-SVG.
212+
*/
213+
export const SVG_ELIGIBLE_TYPES = new Set([
214+
"IMAGE-SVG", // VECTOR nodes are converted to IMAGE-SVG, or containers that were collapsed
215+
"STAR",
216+
"LINE",
217+
"ELLIPSE",
218+
"REGULAR_POLYGON",
219+
"RECTANGLE",
220+
]);
221+
222+
/**
223+
* afterChildren callback that collapses SVG-heavy containers to IMAGE-SVG.
224+
*
225+
* If a FRAME, GROUP, or INSTANCE contains only SVG-eligible children, the parent
226+
* is marked as IMAGE-SVG and children are omitted, reducing payload size.
227+
*
228+
* @param node - Original Figma node
229+
* @param result - SimplifiedNode being built
230+
* @param children - Processed children
231+
* @returns Children to include (empty array if collapsed)
232+
*/
233+
export function collapseSvgContainers(
234+
node: FigmaDocumentNode,
235+
result: SimplifiedNode,
236+
children: SimplifiedNode[],
237+
): SimplifiedNode[] {
238+
const allChildrenAreSvgEligible = children.every((child) =>
239+
SVG_ELIGIBLE_TYPES.has(child.type),
240+
);
241+
242+
if (
243+
(node.type === "FRAME" || node.type === "GROUP" || node.type === "INSTANCE") &&
244+
allChildrenAreSvgEligible
245+
) {
246+
// Collapse to IMAGE-SVG and omit children
247+
result.type = "IMAGE-SVG";
248+
return [];
249+
}
250+
251+
// Include all children normally
252+
return children;
253+
}

src/extractors/design-extractor.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,11 @@ function parseAPIResponse(data: GetFileResponse | GetFileNodesResponse) {
7676
nodesToParse = data.document.children.filter(isVisible);
7777
}
7878

79-
const { name, lastModified, thumbnailUrl } = data;
79+
const { name } = data;
8080

8181
return {
8282
metadata: {
8383
name,
84-
lastModified,
85-
thumbnailUrl: thumbnailUrl || "",
8684
},
8785
rawNodes: nodesToParse,
8886
extraStyles,

src/extractors/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export { extractFromDesign } from "./node-walker.js";
1313
// Design-level extraction (unified nodes + components)
1414
export { simplifyRawFigmaObject } from "./design-extractor.js";
1515

16-
// Built-in extractors
16+
// Built-in extractors and afterChildren helpers
1717
export {
1818
layoutExtractor,
1919
textExtractor,
@@ -25,4 +25,7 @@ export {
2525
contentOnly,
2626
visualsOnly,
2727
layoutOnly,
28+
// afterChildren helpers
29+
collapseSvgContainers,
30+
SVG_ELIGIBLE_TYPES,
2831
} from "./built-in.js";

src/extractors/node-walker.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,14 @@ function processNodeWithExtractors(
8181
.filter((child): child is SimplifiedNode => child !== null);
8282

8383
if (children.length > 0) {
84-
result.children = children;
84+
// Allow custom logic to modify parent and control which children to include
85+
const childrenToInclude = options.afterChildren
86+
? options.afterChildren(node, result, children)
87+
: children;
88+
89+
if (childrenToInclude.length > 0) {
90+
result.children = childrenToInclude;
91+
}
8592
}
8693
}
8794
}

src/extractors/types.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ export interface TraversalContext {
3030
export interface TraversalOptions {
3131
maxDepth?: number;
3232
nodeFilter?: (node: FigmaDocumentNode) => boolean;
33+
/**
34+
* Called after children are processed, allowing modification of the parent node
35+
* and control over which children to include in the output.
36+
*
37+
* @param node - Original Figma node
38+
* @param result - SimplifiedNode being built (can be mutated)
39+
* @param children - Processed children
40+
* @returns Children to include (return empty array to omit children)
41+
*/
42+
afterChildren?: (
43+
node: FigmaDocumentNode,
44+
result: SimplifiedNode,
45+
children: SimplifiedNode[],
46+
) => SimplifiedNode[];
3347
}
3448

3549
/**
@@ -47,8 +61,6 @@ export type ExtractorFn = (
4761

4862
export interface SimplifiedDesign {
4963
name: string;
50-
lastModified: string;
51-
thumbnailUrl: string;
5264
nodes: SimplifiedNode[];
5365
components: Record<string, SimplifiedComponentDefinition>;
5466
componentSets: Record<string, SimplifiedComponentSetDefinition>;

src/mcp/tools/get-figma-data-tool.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { z } from "zod";
22
import type { GetFileResponse, GetFileNodesResponse } from "@figma/rest-api-spec";
33
import { FigmaService } from "~/services/figma.js";
4-
import { simplifyRawFigmaObject, allExtractors } from "~/extractors/index.js";
4+
import {
5+
simplifyRawFigmaObject,
6+
allExtractors,
7+
collapseSvgContainers,
8+
} from "~/extractors/index.js";
59
import yaml from "js-yaml";
610
import { Logger, writeLogs } from "~/utils/logger.js";
711

@@ -62,6 +66,7 @@ async function getFigmaData(
6266
// Use unified design extraction (handles nodes + components consistently)
6367
const simplifiedDesign = simplifyRawFigmaObject(rawApiResponse, allExtractors, {
6468
maxDepth: depth,
69+
afterChildren: collapseSvgContainers,
6570
});
6671

6772
writeLogs("figma-simplified.json", simplifiedDesign);

0 commit comments

Comments
 (0)