figma-preview-mcp 0.2.0
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 +220 -0
- package/dist/ast-enricher.d.ts +80 -0
- package/dist/ast-enricher.d.ts.map +1 -0
- package/dist/ast-enricher.js +318 -0
- package/dist/ast-enricher.js.map +1 -0
- package/dist/ast-module-splitter.d.ts +106 -0
- package/dist/ast-module-splitter.d.ts.map +1 -0
- package/dist/ast-module-splitter.js +325 -0
- package/dist/ast-module-splitter.js.map +1 -0
- package/dist/bounds-cache.d.ts +48 -0
- package/dist/bounds-cache.d.ts.map +1 -0
- package/dist/bounds-cache.js +71 -0
- package/dist/bounds-cache.js.map +1 -0
- package/dist/image-downloader.d.ts +20 -0
- package/dist/image-downloader.d.ts.map +1 -0
- package/dist/image-downloader.js +248 -0
- package/dist/image-downloader.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1554 -0
- package/dist/index.js.map +1 -0
- package/dist/layout-inference.d.ts +98 -0
- package/dist/layout-inference.d.ts.map +1 -0
- package/dist/layout-inference.js +486 -0
- package/dist/layout-inference.js.map +1 -0
- package/dist/layout-verifier.d.ts +91 -0
- package/dist/layout-verifier.d.ts.map +1 -0
- package/dist/layout-verifier.js +318 -0
- package/dist/layout-verifier.js.map +1 -0
- package/dist/parser.d.ts +120 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +310 -0
- package/dist/parser.js.map +1 -0
- package/dist/renderer.d.ts +8 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +639 -0
- package/dist/renderer.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { EnrichedAST, EnrichedASTNode } from './ast-enricher.js';
|
|
2
|
+
export interface ExportAsset {
|
|
3
|
+
nodeId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
bounds: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
suggestedFormat: 'png' | 'svg' | 'jpg';
|
|
10
|
+
}
|
|
11
|
+
export interface ModuleEntry {
|
|
12
|
+
index: number;
|
|
13
|
+
name: string;
|
|
14
|
+
/** nodeId of the first child in this module group */
|
|
15
|
+
nodeId: string;
|
|
16
|
+
file: string;
|
|
17
|
+
nodeCount: number;
|
|
18
|
+
childNodeIds: string[];
|
|
19
|
+
bounds: {
|
|
20
|
+
y: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface SkeletonChild {
|
|
25
|
+
nodeId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
type: string;
|
|
28
|
+
bounds: {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
};
|
|
34
|
+
moduleFile: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ModuleMap {
|
|
37
|
+
fileKey: string;
|
|
38
|
+
rootNodeId: string;
|
|
39
|
+
totalNodes: number;
|
|
40
|
+
splitThreshold: number;
|
|
41
|
+
viewport: {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
};
|
|
45
|
+
skeletonFile: string;
|
|
46
|
+
modules: ModuleEntry[];
|
|
47
|
+
exportAssets: ExportAsset[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Convert a Figma node name to a kebab-case file-safe string.
|
|
51
|
+
* e.g. "Hero Section" → "hero-section", "FeatureCards_v2" → "featurecards-v2"
|
|
52
|
+
*/
|
|
53
|
+
export declare function toKebabCase(name: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Recursively count all nodes in an EnrichedASTNode subtree.
|
|
56
|
+
* Graphic subtrees (flags.isGraphicSubtree) are counted as 1 since their
|
|
57
|
+
* children are already pruned by the enricher.
|
|
58
|
+
*/
|
|
59
|
+
export declare function countSubtreeNodes(node: EnrichedASTNode): number;
|
|
60
|
+
/**
|
|
61
|
+
* Find the effective split point: if the root has a single child container
|
|
62
|
+
* whose subtree exceeds the threshold, "unwrap" one level and split its
|
|
63
|
+
* children instead. Returns the node whose .children should be grouped.
|
|
64
|
+
*/
|
|
65
|
+
export declare function findSplitTarget(root: EnrichedASTNode, threshold: number): EnrichedASTNode;
|
|
66
|
+
/**
|
|
67
|
+
* Split root.children into module groups using greedy sequential grouping.
|
|
68
|
+
*
|
|
69
|
+
* Algorithm:
|
|
70
|
+
* - If root has a single large child, "unwrap" to split at the next level
|
|
71
|
+
* - Iterate children in order
|
|
72
|
+
* - Accumulate into current group until adding next child would exceed threshold
|
|
73
|
+
* - A single child whose subtree count > threshold is recursively split into
|
|
74
|
+
* sub-modules by diving into its children
|
|
75
|
+
* - Returns { splitTarget, modules } where splitTarget is the actual node being split
|
|
76
|
+
*/
|
|
77
|
+
export interface SplitResult {
|
|
78
|
+
splitTarget: EnrichedASTNode;
|
|
79
|
+
modules: EnrichedASTNode[][];
|
|
80
|
+
}
|
|
81
|
+
export declare function splitIntoModules(root: EnrichedASTNode, threshold: number): SplitResult;
|
|
82
|
+
/**
|
|
83
|
+
* Build a skeleton AST: root node with full layout but children replaced
|
|
84
|
+
* by lightweight placeholders that reference their module file.
|
|
85
|
+
*/
|
|
86
|
+
export declare function generateSkeleton(root: EnrichedASTNode, moduleEntries: ModuleEntry[]): Record<string, unknown>;
|
|
87
|
+
/**
|
|
88
|
+
* Traverse all nodes and collect graphic/image nodes for export.
|
|
89
|
+
*/
|
|
90
|
+
export declare function collectExportAssets(root: EnrichedASTNode): ExportAsset[];
|
|
91
|
+
/**
|
|
92
|
+
* Build the module-map data structure.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildModuleMap(ast: EnrichedAST, moduleGroups: EnrichedASTNode[][], exportAssets: ExportAsset[], threshold: number): ModuleMap;
|
|
95
|
+
/**
|
|
96
|
+
* Split an EnrichedAST into modules and write all files to outputDir.
|
|
97
|
+
*
|
|
98
|
+
* Files written:
|
|
99
|
+
* - module-map.json
|
|
100
|
+
* - skeleton.json
|
|
101
|
+
* - module-{index}-{name}.ast.json (one per module)
|
|
102
|
+
*
|
|
103
|
+
* Returns the ModuleMap for the caller to use as tool response.
|
|
104
|
+
*/
|
|
105
|
+
export declare function writeModuleFiles(ast: EnrichedAST, outputDir: string, threshold: number): Promise<ModuleMap>;
|
|
106
|
+
//# sourceMappingURL=ast-module-splitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-module-splitter.d.ts","sourceRoot":"","sources":["../src/ast-module-splitter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAoB,MAAM,mBAAmB,CAAC;AAIxF,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,eAAe,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B;AAID;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAQ/D;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,CAYzF;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,eAAe,CAAC;IAC7B,OAAO,EAAE,eAAe,EAAE,EAAE,CAAC;CAC9B;AA0DD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,eAAe,EACrB,SAAS,EAAE,MAAM,GAChB,WAAW,CA0Cb;AAID;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,WAAW,EAAE,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB;AAID;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,EAAE,CAuBxE;AAcD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,eAAe,EAAE,EAAE,EACjC,YAAY,EAAE,WAAW,EAAE,EAC3B,SAAS,EAAE,MAAM,GAChB,SAAS,CAgCX;AA8BD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,WAAW,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,SAAS,CAAC,CAwEpB"}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Convert a Figma node name to a kebab-case file-safe string.
|
|
6
|
+
* e.g. "Hero Section" → "hero-section", "FeatureCards_v2" → "featurecards-v2"
|
|
7
|
+
*/
|
|
8
|
+
export function toKebabCase(name) {
|
|
9
|
+
return name
|
|
10
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2') // camelCase → camel-Case
|
|
11
|
+
.replace(/[\s_/\\]+/g, '-') // whitespace/underscores/slashes → dash
|
|
12
|
+
.replace(/[^a-zA-Z0-9-]/g, '') // strip non-alphanumeric
|
|
13
|
+
.replace(/-+/g, '-') // collapse multiple dashes
|
|
14
|
+
.replace(/^-|-$/g, '') // trim leading/trailing dashes
|
|
15
|
+
.toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Recursively count all nodes in an EnrichedASTNode subtree.
|
|
19
|
+
* Graphic subtrees (flags.isGraphicSubtree) are counted as 1 since their
|
|
20
|
+
* children are already pruned by the enricher.
|
|
21
|
+
*/
|
|
22
|
+
export function countSubtreeNodes(node) {
|
|
23
|
+
let count = 1;
|
|
24
|
+
if (node.children) {
|
|
25
|
+
for (const child of node.children) {
|
|
26
|
+
count += countSubtreeNodes(child);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
// ─── Core Splitting Algorithm ───────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Find the effective split point: if the root has a single child container
|
|
34
|
+
* whose subtree exceeds the threshold, "unwrap" one level and split its
|
|
35
|
+
* children instead. Returns the node whose .children should be grouped.
|
|
36
|
+
*/
|
|
37
|
+
export function findSplitTarget(root, threshold) {
|
|
38
|
+
let target = root;
|
|
39
|
+
while (target.children &&
|
|
40
|
+
target.children.length === 1 &&
|
|
41
|
+
countSubtreeNodes(target.children[0]) > threshold &&
|
|
42
|
+
target.children[0].children &&
|
|
43
|
+
target.children[0].children.length > 1) {
|
|
44
|
+
target = target.children[0];
|
|
45
|
+
}
|
|
46
|
+
return target;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Recursively split a single oversized node into smaller groups.
|
|
50
|
+
* Dives into its children until groups fit within the threshold.
|
|
51
|
+
*/
|
|
52
|
+
function splitOversizedNode(node, threshold) {
|
|
53
|
+
// Try to unwrap to find a level with multiple children
|
|
54
|
+
const target = findSplitTarget(node, threshold);
|
|
55
|
+
const children = target.children ?? [];
|
|
56
|
+
// If no children or only one child that can't be further split, return as-is
|
|
57
|
+
if (children.length <= 1) {
|
|
58
|
+
return [[node]];
|
|
59
|
+
}
|
|
60
|
+
const subModules = [];
|
|
61
|
+
let currentGroup = [];
|
|
62
|
+
let currentCount = 0;
|
|
63
|
+
for (const child of children) {
|
|
64
|
+
const childCount = countSubtreeNodes(child);
|
|
65
|
+
if (childCount > threshold) {
|
|
66
|
+
// Flush current group first
|
|
67
|
+
if (currentGroup.length > 0) {
|
|
68
|
+
subModules.push(currentGroup);
|
|
69
|
+
currentGroup = [];
|
|
70
|
+
currentCount = 0;
|
|
71
|
+
}
|
|
72
|
+
// Recursively split this oversized child
|
|
73
|
+
const deeperModules = splitOversizedNode(child, threshold);
|
|
74
|
+
subModules.push(...deeperModules);
|
|
75
|
+
}
|
|
76
|
+
else if (currentCount + childCount > threshold) {
|
|
77
|
+
// Adding this child would exceed threshold: flush and start new group
|
|
78
|
+
if (currentGroup.length > 0) {
|
|
79
|
+
subModules.push(currentGroup);
|
|
80
|
+
}
|
|
81
|
+
currentGroup = [child];
|
|
82
|
+
currentCount = childCount;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Fits in current group
|
|
86
|
+
currentGroup.push(child);
|
|
87
|
+
currentCount += childCount;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Flush remaining
|
|
91
|
+
if (currentGroup.length > 0) {
|
|
92
|
+
subModules.push(currentGroup);
|
|
93
|
+
}
|
|
94
|
+
return subModules;
|
|
95
|
+
}
|
|
96
|
+
export function splitIntoModules(root, threshold) {
|
|
97
|
+
const splitTarget = findSplitTarget(root, threshold);
|
|
98
|
+
const children = splitTarget.children ?? [];
|
|
99
|
+
if (children.length === 0)
|
|
100
|
+
return { splitTarget, modules: [[]] };
|
|
101
|
+
const modules = [];
|
|
102
|
+
let currentGroup = [];
|
|
103
|
+
let currentCount = 0;
|
|
104
|
+
for (const child of children) {
|
|
105
|
+
const childCount = countSubtreeNodes(child);
|
|
106
|
+
if (childCount > threshold) {
|
|
107
|
+
// Flush current group first
|
|
108
|
+
if (currentGroup.length > 0) {
|
|
109
|
+
modules.push(currentGroup);
|
|
110
|
+
currentGroup = [];
|
|
111
|
+
currentCount = 0;
|
|
112
|
+
}
|
|
113
|
+
// Recursively split this oversized child into sub-modules
|
|
114
|
+
const subModules = splitOversizedNode(child, threshold);
|
|
115
|
+
modules.push(...subModules);
|
|
116
|
+
}
|
|
117
|
+
else if (currentCount + childCount > threshold) {
|
|
118
|
+
// Adding this child would exceed threshold: flush and start new group
|
|
119
|
+
if (currentGroup.length > 0) {
|
|
120
|
+
modules.push(currentGroup);
|
|
121
|
+
}
|
|
122
|
+
currentGroup = [child];
|
|
123
|
+
currentCount = childCount;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Fits in current group
|
|
127
|
+
currentGroup.push(child);
|
|
128
|
+
currentCount += childCount;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Flush remaining
|
|
132
|
+
if (currentGroup.length > 0) {
|
|
133
|
+
modules.push(currentGroup);
|
|
134
|
+
}
|
|
135
|
+
return { splitTarget, modules };
|
|
136
|
+
}
|
|
137
|
+
// ─── Skeleton Generation ────────────────────────────────────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* Build a skeleton AST: root node with full layout but children replaced
|
|
140
|
+
* by lightweight placeholders that reference their module file.
|
|
141
|
+
*/
|
|
142
|
+
export function generateSkeleton(root, moduleEntries) {
|
|
143
|
+
// Build a map from childNodeId → moduleFile for quick lookup
|
|
144
|
+
const childToModuleFile = new Map();
|
|
145
|
+
for (const mod of moduleEntries) {
|
|
146
|
+
for (const childId of mod.childNodeIds) {
|
|
147
|
+
childToModuleFile.set(childId, mod.file);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const placeholderChildren = (root.children ?? []).map(child => ({
|
|
151
|
+
nodeId: child.nodeId,
|
|
152
|
+
name: child.name,
|
|
153
|
+
type: child.type,
|
|
154
|
+
bounds: { ...child.bounds },
|
|
155
|
+
moduleFile: childToModuleFile.get(child.nodeId) ?? 'unknown',
|
|
156
|
+
}));
|
|
157
|
+
return {
|
|
158
|
+
nodeId: root.nodeId,
|
|
159
|
+
name: root.name,
|
|
160
|
+
type: root.type,
|
|
161
|
+
bounds: { ...root.bounds },
|
|
162
|
+
layout: { ...root.layout },
|
|
163
|
+
...(root.style ? { style: { ...root.style } } : {}),
|
|
164
|
+
children: placeholderChildren,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// ─── Export Asset Collection ────────────────────────────────────────────────
|
|
168
|
+
/**
|
|
169
|
+
* Traverse all nodes and collect graphic/image nodes for export.
|
|
170
|
+
*/
|
|
171
|
+
export function collectExportAssets(root) {
|
|
172
|
+
const assets = [];
|
|
173
|
+
const seenIds = new Set();
|
|
174
|
+
function walk(node) {
|
|
175
|
+
if ((node.flags?.isGraphicSubtree || node.image) && !seenIds.has(node.nodeId)) {
|
|
176
|
+
seenIds.add(node.nodeId);
|
|
177
|
+
assets.push({
|
|
178
|
+
nodeId: node.nodeId,
|
|
179
|
+
name: node.name,
|
|
180
|
+
bounds: { width: node.bounds.width, height: node.bounds.height },
|
|
181
|
+
suggestedFormat: node.image?.suggestedFormat ?? 'svg',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (node.children) {
|
|
185
|
+
for (const child of node.children) {
|
|
186
|
+
walk(child);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
walk(root);
|
|
191
|
+
return assets;
|
|
192
|
+
}
|
|
193
|
+
// ─── Module Map Builder ─────────────────────────────────────────────────────
|
|
194
|
+
/**
|
|
195
|
+
* Compute the combined bounds (y, height) of a group of children.
|
|
196
|
+
*/
|
|
197
|
+
function computeGroupBounds(children) {
|
|
198
|
+
if (children.length === 0)
|
|
199
|
+
return { y: 0, height: 0 };
|
|
200
|
+
const minY = Math.min(...children.map(c => c.bounds.y));
|
|
201
|
+
const maxBottom = Math.max(...children.map(c => c.bounds.y + c.bounds.height));
|
|
202
|
+
return { y: minY, height: maxBottom - minY };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Build the module-map data structure.
|
|
206
|
+
*/
|
|
207
|
+
export function buildModuleMap(ast, moduleGroups, exportAssets, threshold) {
|
|
208
|
+
const moduleEntries = moduleGroups.map((group, index) => {
|
|
209
|
+
const firstName = group[0]?.name ?? `module-${index}`;
|
|
210
|
+
const kebabName = toKebabCase(firstName);
|
|
211
|
+
const fileName = `module-${index}-${kebabName}.ast.json`;
|
|
212
|
+
let nodeCount = 0;
|
|
213
|
+
for (const child of group) {
|
|
214
|
+
nodeCount += countSubtreeNodes(child);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
index,
|
|
218
|
+
name: firstName,
|
|
219
|
+
nodeId: group[0]?.nodeId ?? '',
|
|
220
|
+
file: fileName,
|
|
221
|
+
nodeCount,
|
|
222
|
+
childNodeIds: group.map(c => c.nodeId),
|
|
223
|
+
bounds: computeGroupBounds(group),
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
fileKey: ast.fileKey,
|
|
228
|
+
rootNodeId: ast.rootNodeId,
|
|
229
|
+
totalNodes: ast.stats.totalNodes,
|
|
230
|
+
splitThreshold: threshold,
|
|
231
|
+
viewport: { ...ast.viewport },
|
|
232
|
+
skeletonFile: 'skeleton.json',
|
|
233
|
+
modules: moduleEntries,
|
|
234
|
+
exportAssets,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// ─── Build stats for a sub-AST ──────────────────────────────────────────────
|
|
238
|
+
function buildSubStats(nodes) {
|
|
239
|
+
const stats = {
|
|
240
|
+
totalNodes: 0,
|
|
241
|
+
figmaLayoutNodes: 0,
|
|
242
|
+
inferredLayoutNodes: 0,
|
|
243
|
+
absoluteNodes: 0,
|
|
244
|
+
needsDetailFetch: 0,
|
|
245
|
+
};
|
|
246
|
+
function walk(node) {
|
|
247
|
+
stats.totalNodes++;
|
|
248
|
+
if (node.layout.source === 'figma')
|
|
249
|
+
stats.figmaLayoutNodes++;
|
|
250
|
+
else if (node.layout.source === 'inferred')
|
|
251
|
+
stats.inferredLayoutNodes++;
|
|
252
|
+
else if (node.layout.source === 'absolute')
|
|
253
|
+
stats.absoluteNodes++;
|
|
254
|
+
if (node.flags?.needsDetailFetch)
|
|
255
|
+
stats.needsDetailFetch++;
|
|
256
|
+
if (node.children)
|
|
257
|
+
node.children.forEach(walk);
|
|
258
|
+
}
|
|
259
|
+
for (const node of nodes) {
|
|
260
|
+
walk(node);
|
|
261
|
+
}
|
|
262
|
+
return stats;
|
|
263
|
+
}
|
|
264
|
+
// ─── File Writing (Main Entry Point) ────────────────────────────────────────
|
|
265
|
+
/**
|
|
266
|
+
* Split an EnrichedAST into modules and write all files to outputDir.
|
|
267
|
+
*
|
|
268
|
+
* Files written:
|
|
269
|
+
* - module-map.json
|
|
270
|
+
* - skeleton.json
|
|
271
|
+
* - module-{index}-{name}.ast.json (one per module)
|
|
272
|
+
*
|
|
273
|
+
* Returns the ModuleMap for the caller to use as tool response.
|
|
274
|
+
*/
|
|
275
|
+
export async function writeModuleFiles(ast, outputDir, threshold) {
|
|
276
|
+
// 1. Split into module groups (may unwrap single-child containers)
|
|
277
|
+
const { splitTarget, modules: moduleGroups } = splitIntoModules(ast.root, threshold);
|
|
278
|
+
// 2. Collect export assets (from full tree)
|
|
279
|
+
const exportAssets = collectExportAssets(ast.root);
|
|
280
|
+
// 3. Build module map (using splitTarget as the effective root)
|
|
281
|
+
const moduleMap = buildModuleMap(ast, moduleGroups, exportAssets, threshold);
|
|
282
|
+
// 4. Generate skeleton from splitTarget (the node whose children are split)
|
|
283
|
+
const skeleton = generateSkeleton(splitTarget, moduleMap.modules);
|
|
284
|
+
// 5. Create output directory
|
|
285
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
286
|
+
// 6. Write module-map.json
|
|
287
|
+
await fs.promises.writeFile(path.join(outputDir, 'module-map.json'), JSON.stringify(moduleMap, null, 2), 'utf-8');
|
|
288
|
+
// 7. Write skeleton.json
|
|
289
|
+
await fs.promises.writeFile(path.join(outputDir, 'skeleton.json'), JSON.stringify(skeleton, null, 2), 'utf-8');
|
|
290
|
+
// 8. Write each module AST file
|
|
291
|
+
for (let i = 0; i < moduleGroups.length; i++) {
|
|
292
|
+
const group = moduleGroups[i];
|
|
293
|
+
const entry = moduleMap.modules[i];
|
|
294
|
+
const stats = buildSubStats(group);
|
|
295
|
+
// Build a synthetic root node that wraps this module's children
|
|
296
|
+
// Use the splitTarget's layout for the synthetic wrapper
|
|
297
|
+
const syntheticRoot = {
|
|
298
|
+
nodeId: splitTarget.nodeId,
|
|
299
|
+
name: `${splitTarget.name} [module-${i}]`,
|
|
300
|
+
type: splitTarget.type,
|
|
301
|
+
bounds: {
|
|
302
|
+
x: splitTarget.bounds.x,
|
|
303
|
+
y: entry.bounds.y,
|
|
304
|
+
width: splitTarget.bounds.width,
|
|
305
|
+
height: entry.bounds.height,
|
|
306
|
+
},
|
|
307
|
+
layout: { ...splitTarget.layout },
|
|
308
|
+
children: group,
|
|
309
|
+
};
|
|
310
|
+
const moduleAst = {
|
|
311
|
+
version: '1.0',
|
|
312
|
+
fileKey: ast.fileKey,
|
|
313
|
+
rootNodeId: ast.rootNodeId,
|
|
314
|
+
viewport: {
|
|
315
|
+
width: ast.viewport.width,
|
|
316
|
+
height: entry.bounds.height,
|
|
317
|
+
},
|
|
318
|
+
stats,
|
|
319
|
+
root: syntheticRoot,
|
|
320
|
+
};
|
|
321
|
+
await fs.promises.writeFile(path.join(outputDir, entry.file), JSON.stringify(moduleAst, null, 2), 'utf-8');
|
|
322
|
+
}
|
|
323
|
+
return moduleMap;
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=ast-module-splitter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-module-splitter.js","sourceRoot":"","sources":["../src/ast-module-splitter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ParsedFigmaData } from './parser.js';
|
|
2
|
+
/**
|
|
3
|
+
* Bounds data for a single node, relative to the root node origin.
|
|
4
|
+
*/
|
|
5
|
+
export interface NodeBounds {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Metadata for a single node used in diagnostic context.
|
|
13
|
+
*/
|
|
14
|
+
export interface NodeMeta {
|
|
15
|
+
name: string;
|
|
16
|
+
type: string;
|
|
17
|
+
parentId: string | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Cached layout bounds data produced by render_preview,
|
|
21
|
+
* consumed by generate_layout_ast and verify_layout_bounds.
|
|
22
|
+
*/
|
|
23
|
+
export interface LayoutBoundsCache {
|
|
24
|
+
fileKey: string;
|
|
25
|
+
rootNodeId: string;
|
|
26
|
+
rootWidth: number;
|
|
27
|
+
rootHeight: number;
|
|
28
|
+
/** nodeId → absolute bounds relative to root node */
|
|
29
|
+
bounds: Map<string, NodeBounds>;
|
|
30
|
+
/** nodeId → node metadata (name, type, parentId) */
|
|
31
|
+
nodeInfo: Map<string, NodeMeta>;
|
|
32
|
+
/** The full parsed node tree (for AST enrichment) */
|
|
33
|
+
nodeTree: ParsedFigmaData;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build and store a LayoutBoundsCache from a fully processed nodeTree
|
|
37
|
+
* (after injectBounds has been called).
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildBoundsCache(nodeTree: ParsedFigmaData, fileKey: string, rootNodeId: string): LayoutBoundsCache;
|
|
40
|
+
/**
|
|
41
|
+
* Retrieve a cached LayoutBoundsCache, or null if not found.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getBoundsCache(fileKey: string, rootNodeId: string): LayoutBoundsCache | null;
|
|
44
|
+
/**
|
|
45
|
+
* Manually set a LayoutBoundsCache entry (e.g. for testing).
|
|
46
|
+
*/
|
|
47
|
+
export declare function setBoundsCache(entry: LayoutBoundsCache): void;
|
|
48
|
+
//# sourceMappingURL=bounds-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounds-cache.d.ts","sourceRoot":"","sources":["../src/bounds-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAChC,oDAAoD;IACpD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAChC,qDAAqD;IACrD,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAWD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,iBAAiB,CAqDnB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAG5F;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAG7D"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Process-level singleton cache: cacheKey → LayoutBoundsCache
|
|
2
|
+
const cache = new Map();
|
|
3
|
+
function cacheKey(fileKey, rootNodeId) {
|
|
4
|
+
// Normalize ID format (colons)
|
|
5
|
+
const normalizedId = rootNodeId.replace(/-/g, ':');
|
|
6
|
+
return `${fileKey}:${normalizedId}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build and store a LayoutBoundsCache from a fully processed nodeTree
|
|
10
|
+
* (after injectBounds has been called).
|
|
11
|
+
*/
|
|
12
|
+
export function buildBoundsCache(nodeTree, fileKey, rootNodeId) {
|
|
13
|
+
const bounds = new Map();
|
|
14
|
+
const nodeInfo = new Map();
|
|
15
|
+
const rootNode = nodeTree.nodes[0];
|
|
16
|
+
const rootWidth = rootNode?.layout?.width ?? 0;
|
|
17
|
+
const rootHeight = rootNode?.layout?.height ?? 0;
|
|
18
|
+
function walk(node, parentId) {
|
|
19
|
+
const id = node.id;
|
|
20
|
+
// Store bounds (use _absX/_absY if available, fall back to layout x/y)
|
|
21
|
+
bounds.set(id, {
|
|
22
|
+
x: node._absX ?? node.layout?.x ?? 0,
|
|
23
|
+
y: node._absY ?? node.layout?.y ?? 0,
|
|
24
|
+
width: node.layout?.width ?? 0,
|
|
25
|
+
height: node.layout?.height ?? 0,
|
|
26
|
+
});
|
|
27
|
+
// Store metadata
|
|
28
|
+
nodeInfo.set(id, {
|
|
29
|
+
name: node.name,
|
|
30
|
+
type: node.type,
|
|
31
|
+
parentId,
|
|
32
|
+
});
|
|
33
|
+
// Recurse children
|
|
34
|
+
if (node.children) {
|
|
35
|
+
for (const child of node.children) {
|
|
36
|
+
walk(child, id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const rootN of nodeTree.nodes) {
|
|
41
|
+
walk(rootN, null);
|
|
42
|
+
}
|
|
43
|
+
const entry = {
|
|
44
|
+
fileKey,
|
|
45
|
+
rootNodeId: rootNodeId.replace(/-/g, ':'),
|
|
46
|
+
rootWidth,
|
|
47
|
+
rootHeight,
|
|
48
|
+
bounds,
|
|
49
|
+
nodeInfo,
|
|
50
|
+
nodeTree,
|
|
51
|
+
};
|
|
52
|
+
// Store in singleton cache (overwrites existing entry for same key)
|
|
53
|
+
const key = cacheKey(fileKey, rootNodeId);
|
|
54
|
+
cache.set(key, entry);
|
|
55
|
+
return entry;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Retrieve a cached LayoutBoundsCache, or null if not found.
|
|
59
|
+
*/
|
|
60
|
+
export function getBoundsCache(fileKey, rootNodeId) {
|
|
61
|
+
const key = cacheKey(fileKey, rootNodeId);
|
|
62
|
+
return cache.get(key) ?? null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Manually set a LayoutBoundsCache entry (e.g. for testing).
|
|
66
|
+
*/
|
|
67
|
+
export function setBoundsCache(entry) {
|
|
68
|
+
const key = cacheKey(entry.fileKey, entry.rootNodeId);
|
|
69
|
+
cache.set(key, entry);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=bounds-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounds-cache.js","sourceRoot":"","sources":["../src/bounds-cache.ts"],"names":[],"mappings":"AAsCA,8DAA8D;AAC9D,MAAM,KAAK,GAAG,IAAI,GAAG,EAA6B,CAAC;AAEnD,SAAS,QAAQ,CAAC,OAAe,EAAE,UAAkB;IACnD,+BAA+B;IAC/B,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,GAAG,OAAO,IAAI,YAAY,EAAE,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAyB,EACzB,OAAe,EACf,UAAkB;IAElB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC7C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE7C,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,SAAS,GAAG,QAAQ,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,QAAQ,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC,CAAC;IAEjD,SAAS,IAAI,CAAC,IAAe,EAAE,QAAuB;QACpD,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAEnB,uEAAuE;QACvE,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE;YACb,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC;YACpC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC;YACpC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;YAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;SACjC,CAAC,CAAC;QAEH,iBAAiB;QACjB,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,QAAQ;SACT,CAAC,CAAC;QAEH,mBAAmB;QACnB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QACnC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,KAAK,GAAsB;QAC/B,OAAO;QACP,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;QACzC,SAAS;QACT,UAAU;QACV,MAAM;QACN,QAAQ;QACR,QAAQ;KACT,CAAC;IAEF,oEAAoE;IACpE,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC1C,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAEtB,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,UAAkB;IAChE,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC1C,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AAChC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAwB;IACrD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACtD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ParsedFigmaData } from './parser.js';
|
|
2
|
+
export type ImageMap = Map<string, string>;
|
|
3
|
+
/**
|
|
4
|
+
* Download a PNG screenshot of the given node (usually the root/preview node).
|
|
5
|
+
* Returns the local file path, or null on failure.
|
|
6
|
+
* Skips download if the file is already cached.
|
|
7
|
+
*/
|
|
8
|
+
export declare function downloadRootScreenshot(fileKey: string, nodeId: string, token: string): Promise<string | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Fetch absoluteBoundingBox for all given nodeIds from Figma REST API.
|
|
11
|
+
* Returns a map of nodeId -> { x, y, width, height } relative to the root node origin.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fetchAbsoluteBounds(fileKey: string, nodeIds: string[], token: string): Promise<Map<string, {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}>>;
|
|
19
|
+
export declare function downloadImages(data: ParsedFigmaData, fileKey: string, token: string): Promise<ImageMap>;
|
|
20
|
+
//# sourceMappingURL=image-downloader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-downloader.d.ts","sourceRoot":"","sources":["../src/image-downloader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAa,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9D,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAI3C;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+CxB;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EAAE,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAqD/E;AA0FD,wBAAsB,cAAc,CAClC,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,QAAQ,CAAC,CAgDnB"}
|