figma-code-agent-mcp 1.0.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/dist/assetServer.d.ts +34 -0
- package/dist/assetServer.js +168 -0
- package/dist/codeGenerator/componentDetector.d.ts +57 -0
- package/dist/codeGenerator/componentDetector.js +171 -0
- package/dist/codeGenerator/index.d.ts +77 -0
- package/dist/codeGenerator/index.js +184 -0
- package/dist/codeGenerator/jsxGenerator.d.ts +46 -0
- package/dist/codeGenerator/jsxGenerator.js +182 -0
- package/dist/codeGenerator/styleConverter.d.ts +95 -0
- package/dist/codeGenerator/styleConverter.js +306 -0
- package/dist/hints.d.ts +14 -0
- package/dist/hints.js +105 -0
- package/dist/hub.d.ts +9 -0
- package/dist/hub.js +252 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +146 -0
- package/dist/sandbox.d.ts +18 -0
- package/dist/sandbox.js +154 -0
- package/dist/toolRegistry.d.ts +19 -0
- package/dist/toolRegistry.js +729 -0
- package/dist/tools/captureScreenshot.d.ts +28 -0
- package/dist/tools/captureScreenshot.js +31 -0
- package/dist/tools/cloneNode.d.ts +43 -0
- package/dist/tools/cloneNode.js +46 -0
- package/dist/tools/createFrame.d.ts +157 -0
- package/dist/tools/createFrame.js +114 -0
- package/dist/tools/createInstance.d.ts +38 -0
- package/dist/tools/createInstance.js +41 -0
- package/dist/tools/createRectangle.d.ts +108 -0
- package/dist/tools/createRectangle.js +77 -0
- package/dist/tools/createText.d.ts +81 -0
- package/dist/tools/createText.js +67 -0
- package/dist/tools/deleteNode.d.ts +15 -0
- package/dist/tools/deleteNode.js +18 -0
- package/dist/tools/execute.d.ts +17 -0
- package/dist/tools/execute.js +68 -0
- package/dist/tools/getCurrentPage.d.ts +16 -0
- package/dist/tools/getCurrentPage.js +19 -0
- package/dist/tools/getDesignContext.d.ts +42 -0
- package/dist/tools/getDesignContext.js +55 -0
- package/dist/tools/getLocalComponents.d.ts +20 -0
- package/dist/tools/getLocalComponents.js +23 -0
- package/dist/tools/getNode.d.ts +30 -0
- package/dist/tools/getNode.js +33 -0
- package/dist/tools/getSelection.d.ts +10 -0
- package/dist/tools/getSelection.js +13 -0
- package/dist/tools/getStyles.d.ts +17 -0
- package/dist/tools/getStyles.js +20 -0
- package/dist/tools/getVariables.d.ts +26 -0
- package/dist/tools/getVariables.js +29 -0
- package/dist/tools/moveNode.d.ts +28 -0
- package/dist/tools/moveNode.js +31 -0
- package/dist/tools/openInEditor.d.ts +21 -0
- package/dist/tools/openInEditor.js +98 -0
- package/dist/tools/searchNodes.d.ts +30 -0
- package/dist/tools/searchNodes.js +46 -0
- package/dist/tools/searchTools.d.ts +28 -0
- package/dist/tools/searchTools.js +28 -0
- package/dist/tools/swapComponent.d.ts +23 -0
- package/dist/tools/swapComponent.js +26 -0
- package/dist/tools/updateNode.d.ts +194 -0
- package/dist/tools/updateNode.js +163 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +1 -0
- package/dist/websocket.d.ts +30 -0
- package/dist/websocket.js +282 -0
- package/package.json +29 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
declare class AssetServer {
|
|
2
|
+
private server;
|
|
3
|
+
private assets;
|
|
4
|
+
private port;
|
|
5
|
+
private isInitialized;
|
|
6
|
+
initialize(config?: {
|
|
7
|
+
port?: number;
|
|
8
|
+
}): void;
|
|
9
|
+
/**
|
|
10
|
+
* Store an asset and return its URL
|
|
11
|
+
* @param base64Data Base64-encoded image data
|
|
12
|
+
* @param format Image format (png, svg, jpg)
|
|
13
|
+
* @returns URL to access the asset
|
|
14
|
+
*/
|
|
15
|
+
storeAsset(base64Data: string, format: 'png' | 'svg' | 'jpg'): string;
|
|
16
|
+
/**
|
|
17
|
+
* Get the base URL for the asset server
|
|
18
|
+
*/
|
|
19
|
+
getBaseUrl(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Clear all stored assets (useful for cleanup)
|
|
22
|
+
*/
|
|
23
|
+
clearAssets(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Get the number of stored assets
|
|
26
|
+
*/
|
|
27
|
+
getAssetCount(): number;
|
|
28
|
+
/**
|
|
29
|
+
* Close the server
|
|
30
|
+
*/
|
|
31
|
+
close(): void;
|
|
32
|
+
}
|
|
33
|
+
export declare const assetServer: AssetServer;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { figmaWebSocket } from './websocket.js';
|
|
4
|
+
const DEFAULT_PORT = 3845;
|
|
5
|
+
const MAX_ASSET_SIZE = 10 * 1024 * 1024; // 10MB per asset
|
|
6
|
+
const MAX_ASSETS = 100;
|
|
7
|
+
class AssetServer {
|
|
8
|
+
server = null;
|
|
9
|
+
assets = new Map();
|
|
10
|
+
port = DEFAULT_PORT;
|
|
11
|
+
isInitialized = false;
|
|
12
|
+
initialize(config) {
|
|
13
|
+
if (this.isInitialized) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
this.port = config?.port || DEFAULT_PORT;
|
|
17
|
+
this.server = http.createServer((req, res) => {
|
|
18
|
+
// Enable CORS
|
|
19
|
+
req.socket.setTimeout(30000);
|
|
20
|
+
res.setHeader('Access-Control-Allow-Origin', `http://localhost:${this.port}`);
|
|
21
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
22
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
23
|
+
if (req.method === 'OPTIONS') {
|
|
24
|
+
res.writeHead(200);
|
|
25
|
+
res.end();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const url = req.url || '';
|
|
29
|
+
// GET /api/status — health check
|
|
30
|
+
if (req.method === 'GET' && url === '/api/status') {
|
|
31
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
32
|
+
res.end(JSON.stringify({
|
|
33
|
+
server: 'ok',
|
|
34
|
+
figmaConnected: figmaWebSocket.isConnected(),
|
|
35
|
+
editorType: figmaWebSocket.getEditorType(),
|
|
36
|
+
}));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// POST /api/capture — forward create command to Figma
|
|
40
|
+
if (req.method === 'POST' && url === '/api/capture') {
|
|
41
|
+
let body = '';
|
|
42
|
+
let size = 0;
|
|
43
|
+
const MAX_BODY = 5 * 1024 * 1024; // 5MB
|
|
44
|
+
req.on('data', (chunk) => {
|
|
45
|
+
size += chunk.length;
|
|
46
|
+
if (size > MAX_BODY) {
|
|
47
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
48
|
+
res.end(JSON.stringify({ error: 'Payload too large' }));
|
|
49
|
+
req.destroy();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
body += chunk.toString();
|
|
53
|
+
});
|
|
54
|
+
req.on('end', async () => {
|
|
55
|
+
try {
|
|
56
|
+
const payload = JSON.parse(body);
|
|
57
|
+
const result = await figmaWebSocket.sendCommand('create', payload);
|
|
58
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
59
|
+
res.end(JSON.stringify({ success: true, result }));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
63
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
64
|
+
res.end(JSON.stringify({ success: false, error: message }));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// GET /assets/* — existing asset serving
|
|
70
|
+
if (req.method === 'GET') {
|
|
71
|
+
const match = url.match(/^\/assets\/([a-f0-9-]+)\.(png|svg|jpg)$/i);
|
|
72
|
+
if (!match) {
|
|
73
|
+
res.writeHead(404);
|
|
74
|
+
res.end('Not Found');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const assetId = match[1];
|
|
78
|
+
const asset = this.assets.get(assetId);
|
|
79
|
+
if (!asset) {
|
|
80
|
+
res.writeHead(404);
|
|
81
|
+
res.end('Asset Not Found');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
res.writeHead(200, {
|
|
85
|
+
'Content-Type': asset.mimeType,
|
|
86
|
+
'Content-Length': asset.data.length,
|
|
87
|
+
'Cache-Control': 'public, max-age=3600',
|
|
88
|
+
});
|
|
89
|
+
res.end(asset.data);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.writeHead(404);
|
|
93
|
+
res.end('Not Found');
|
|
94
|
+
});
|
|
95
|
+
this.server.on('error', (error) => {
|
|
96
|
+
if (error.code === 'EADDRINUSE') {
|
|
97
|
+
console.error(`[AssetServer] Port ${this.port} is already in use. Assets will not be served.`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.error('[AssetServer] Error:', error);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
this.server.listen(this.port, '127.0.0.1', () => {
|
|
104
|
+
console.error(`[AssetServer] Listening on 127.0.0.1:${this.port}`);
|
|
105
|
+
});
|
|
106
|
+
this.isInitialized = true;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Store an asset and return its URL
|
|
110
|
+
* @param base64Data Base64-encoded image data
|
|
111
|
+
* @param format Image format (png, svg, jpg)
|
|
112
|
+
* @returns URL to access the asset
|
|
113
|
+
*/
|
|
114
|
+
storeAsset(base64Data, format) {
|
|
115
|
+
if (this.assets.size >= MAX_ASSETS) {
|
|
116
|
+
// Evict oldest asset
|
|
117
|
+
const firstKey = this.assets.keys().next().value;
|
|
118
|
+
if (firstKey)
|
|
119
|
+
this.assets.delete(firstKey);
|
|
120
|
+
}
|
|
121
|
+
const id = uuidv4();
|
|
122
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
123
|
+
if (buffer.length > MAX_ASSET_SIZE) {
|
|
124
|
+
throw new Error(`Asset exceeds maximum size of ${MAX_ASSET_SIZE / 1024 / 1024}MB`);
|
|
125
|
+
}
|
|
126
|
+
const mimeTypes = {
|
|
127
|
+
png: 'image/png',
|
|
128
|
+
svg: 'image/svg+xml',
|
|
129
|
+
jpg: 'image/jpeg',
|
|
130
|
+
};
|
|
131
|
+
this.assets.set(id, {
|
|
132
|
+
data: buffer,
|
|
133
|
+
mimeType: mimeTypes[format] || 'application/octet-stream',
|
|
134
|
+
});
|
|
135
|
+
return `${this.getBaseUrl()}/assets/${id}.${format}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the base URL for the asset server
|
|
139
|
+
*/
|
|
140
|
+
getBaseUrl() {
|
|
141
|
+
return `http://localhost:${this.port}`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Clear all stored assets (useful for cleanup)
|
|
145
|
+
*/
|
|
146
|
+
clearAssets() {
|
|
147
|
+
this.assets.clear();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get the number of stored assets
|
|
151
|
+
*/
|
|
152
|
+
getAssetCount() {
|
|
153
|
+
return this.assets.size;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Close the server
|
|
157
|
+
*/
|
|
158
|
+
close() {
|
|
159
|
+
if (this.server) {
|
|
160
|
+
this.server.close();
|
|
161
|
+
this.server = null;
|
|
162
|
+
this.isInitialized = false;
|
|
163
|
+
this.assets.clear();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Singleton instance
|
|
168
|
+
export const assetServer = new AssetServer();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects Figma components and generates TypeScript props from variants
|
|
3
|
+
*/
|
|
4
|
+
import { FigmaNode } from './jsxGenerator.js';
|
|
5
|
+
export interface VariantProperty {
|
|
6
|
+
name: string;
|
|
7
|
+
values: string[];
|
|
8
|
+
propName: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ComponentInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
componentName: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
documentationLinks?: Array<{
|
|
15
|
+
uri: string;
|
|
16
|
+
}>;
|
|
17
|
+
variantProperties?: VariantProperty[];
|
|
18
|
+
propsInterface?: string;
|
|
19
|
+
defaultProps?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
export interface DetectedComponent {
|
|
22
|
+
info: ComponentInfo;
|
|
23
|
+
instances: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Detect all component instances in the node tree
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectComponentInstances(node: FigmaNode): Map<string, DetectedComponent>;
|
|
29
|
+
/**
|
|
30
|
+
* Generate TypeScript props interface for a component
|
|
31
|
+
*/
|
|
32
|
+
export declare function generatePropsInterface(component: ComponentInfo): string;
|
|
33
|
+
/**
|
|
34
|
+
* Generate a stub component with variant support
|
|
35
|
+
*/
|
|
36
|
+
export declare function generateComponentStub(component: ComponentInfo): string;
|
|
37
|
+
export interface NestedComponentImplementation {
|
|
38
|
+
name: string;
|
|
39
|
+
componentName: string;
|
|
40
|
+
code: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
documentationLinks?: Array<{
|
|
43
|
+
uri: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate implementations for all detected components
|
|
48
|
+
*/
|
|
49
|
+
export declare function generateNestedComponentImplementations(components: Map<string, DetectedComponent>): NestedComponentImplementation[];
|
|
50
|
+
/**
|
|
51
|
+
* Collect component information for the output metadata
|
|
52
|
+
*/
|
|
53
|
+
export declare function collectComponentMetadata(components: Map<string, DetectedComponent>): ComponentInfo[];
|
|
54
|
+
/**
|
|
55
|
+
* Extract variant properties from a COMPONENT_SET node
|
|
56
|
+
*/
|
|
57
|
+
export declare function extractVariantPropertiesFromSet(node: FigmaNode): Record<string, string[]> | undefined;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects Figma components and generates TypeScript props from variants
|
|
3
|
+
*/
|
|
4
|
+
import { toComponentName, toVariableName } from './jsxGenerator.js';
|
|
5
|
+
/**
|
|
6
|
+
* Detect all component instances in the node tree
|
|
7
|
+
*/
|
|
8
|
+
export function detectComponentInstances(node) {
|
|
9
|
+
const components = new Map();
|
|
10
|
+
function traverse(n) {
|
|
11
|
+
if (n.type === 'INSTANCE' && n.componentName) {
|
|
12
|
+
const key = n.componentName;
|
|
13
|
+
if (!components.has(key)) {
|
|
14
|
+
components.set(key, {
|
|
15
|
+
info: {
|
|
16
|
+
name: n.componentName,
|
|
17
|
+
componentName: toComponentName(n.componentName),
|
|
18
|
+
description: n.componentDescription,
|
|
19
|
+
documentationLinks: n.componentDocumentationLinks,
|
|
20
|
+
variantProperties: [],
|
|
21
|
+
},
|
|
22
|
+
instances: [],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const component = components.get(key);
|
|
26
|
+
component.instances.push(n.id);
|
|
27
|
+
// Collect variant properties from this instance
|
|
28
|
+
if (n.variantProperties) {
|
|
29
|
+
for (const [propName, value] of Object.entries(n.variantProperties)) {
|
|
30
|
+
let variantProp = component.info.variantProperties?.find((p) => p.name === propName);
|
|
31
|
+
if (!variantProp) {
|
|
32
|
+
variantProp = {
|
|
33
|
+
name: propName,
|
|
34
|
+
values: [],
|
|
35
|
+
propName: toVariableName(propName),
|
|
36
|
+
};
|
|
37
|
+
component.info.variantProperties?.push(variantProp);
|
|
38
|
+
}
|
|
39
|
+
if (!variantProp.values.includes(value)) {
|
|
40
|
+
variantProp.values.push(value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Recurse into children
|
|
46
|
+
if (n.children) {
|
|
47
|
+
for (const child of n.children) {
|
|
48
|
+
traverse(child);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
traverse(node);
|
|
53
|
+
return components;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generate TypeScript props interface for a component
|
|
57
|
+
*/
|
|
58
|
+
export function generatePropsInterface(component) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
const typeName = `${component.componentName}Props`;
|
|
61
|
+
lines.push(`type ${typeName} = {`);
|
|
62
|
+
lines.push(' className?: string;');
|
|
63
|
+
if (component.variantProperties && component.variantProperties.length > 0) {
|
|
64
|
+
for (const prop of component.variantProperties) {
|
|
65
|
+
const values = prop.values.map((v) => `"${v}"`).join(' | ');
|
|
66
|
+
lines.push(` ${prop.propName}?: ${values};`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
lines.push('};');
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate a stub component with variant support
|
|
74
|
+
*/
|
|
75
|
+
export function generateComponentStub(component) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
const typeName = `${component.componentName}Props`;
|
|
78
|
+
// Props interface
|
|
79
|
+
lines.push(generatePropsInterface(component));
|
|
80
|
+
lines.push('');
|
|
81
|
+
// Component function
|
|
82
|
+
const defaultValues = [];
|
|
83
|
+
if (component.variantProperties && component.variantProperties.length > 0) {
|
|
84
|
+
for (const prop of component.variantProperties) {
|
|
85
|
+
if (prop.values.length > 0) {
|
|
86
|
+
defaultValues.push(`${prop.propName} = "${prop.values[0]}"`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const propsDestructure = defaultValues.length > 0
|
|
91
|
+
? `{ className, ${defaultValues.join(', ')} }`
|
|
92
|
+
: '{ className }';
|
|
93
|
+
lines.push(`function ${component.componentName}(${propsDestructure}: ${typeName}) {`);
|
|
94
|
+
// Generate variant-based className logic
|
|
95
|
+
if (component.variantProperties && component.variantProperties.length > 0) {
|
|
96
|
+
lines.push(' // Variant-based styling');
|
|
97
|
+
lines.push(' const baseClasses = ""; // Add base styles');
|
|
98
|
+
for (const prop of component.variantProperties) {
|
|
99
|
+
const varName = prop.propName + 'Classes';
|
|
100
|
+
lines.push(` const ${varName} = {`);
|
|
101
|
+
for (const value of prop.values) {
|
|
102
|
+
lines.push(` "${value}": "", // Add ${prop.name}=${value} styles`);
|
|
103
|
+
}
|
|
104
|
+
lines.push(' };');
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
lines.push(' return (');
|
|
109
|
+
if (component.variantProperties && component.variantProperties.length > 0) {
|
|
110
|
+
const classExpr = component.variantProperties
|
|
111
|
+
.map(p => `\${${p.propName}Classes[${p.propName}]}`)
|
|
112
|
+
.join(' ');
|
|
113
|
+
lines.push(` <div className={\`\${baseClasses} ${classExpr} \${className ?? ""}\`}>`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
lines.push(` <div className={className ?? ""}>`);
|
|
117
|
+
}
|
|
118
|
+
lines.push(` {/* ${component.name} content */}`);
|
|
119
|
+
lines.push(' </div>');
|
|
120
|
+
lines.push(' );');
|
|
121
|
+
lines.push('}');
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Generate implementations for all detected components
|
|
126
|
+
*/
|
|
127
|
+
export function generateNestedComponentImplementations(components) {
|
|
128
|
+
const implementations = [];
|
|
129
|
+
for (const [, component] of components) {
|
|
130
|
+
const code = generateComponentStub(component.info);
|
|
131
|
+
implementations.push({
|
|
132
|
+
name: component.info.name,
|
|
133
|
+
componentName: component.info.componentName,
|
|
134
|
+
code,
|
|
135
|
+
description: component.info.description,
|
|
136
|
+
documentationLinks: component.info.documentationLinks,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return implementations;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Collect component information for the output metadata
|
|
143
|
+
*/
|
|
144
|
+
export function collectComponentMetadata(components) {
|
|
145
|
+
const result = [];
|
|
146
|
+
for (const [, component] of components) {
|
|
147
|
+
const info = { ...component.info };
|
|
148
|
+
// Generate the props interface
|
|
149
|
+
info.propsInterface = generatePropsInterface(info);
|
|
150
|
+
// Set default props
|
|
151
|
+
if (info.variantProperties && info.variantProperties.length > 0) {
|
|
152
|
+
info.defaultProps = {};
|
|
153
|
+
for (const prop of info.variantProperties) {
|
|
154
|
+
if (prop.values.length > 0) {
|
|
155
|
+
info.defaultProps[prop.propName] = prop.values[0];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
result.push(info);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Extract variant properties from a COMPONENT_SET node
|
|
165
|
+
*/
|
|
166
|
+
export function extractVariantPropertiesFromSet(node) {
|
|
167
|
+
if (node.type !== 'COMPONENT_SET') {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
return node.variantGroupProperties;
|
|
171
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Generator - Main orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Combines style conversion, JSX generation, and component detection
|
|
5
|
+
* to produce React + Tailwind code from Figma designs.
|
|
6
|
+
*/
|
|
7
|
+
import { FigmaNode } from './jsxGenerator.js';
|
|
8
|
+
import { ComponentInfo } from './componentDetector.js';
|
|
9
|
+
export interface DesignContextInput {
|
|
10
|
+
node: FigmaNode;
|
|
11
|
+
images?: Record<string, {
|
|
12
|
+
data: string;
|
|
13
|
+
format: string;
|
|
14
|
+
}>;
|
|
15
|
+
tokens?: {
|
|
16
|
+
colors?: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
value: string;
|
|
19
|
+
opacity?: number;
|
|
20
|
+
}>;
|
|
21
|
+
typography?: Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
fontFamily: string;
|
|
24
|
+
fontStyle: string;
|
|
25
|
+
fontSize: number;
|
|
26
|
+
lineHeight?: unknown;
|
|
27
|
+
letterSpacing?: unknown;
|
|
28
|
+
}>;
|
|
29
|
+
effects?: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
effects: unknown[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface GenerateOptions {
|
|
36
|
+
generateNestedComponents?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface NestedComponentImpl {
|
|
39
|
+
name: string;
|
|
40
|
+
code: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
documentationLinks?: Array<{
|
|
43
|
+
uri: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
export interface DesignContextOutput {
|
|
47
|
+
code: string;
|
|
48
|
+
componentName: string;
|
|
49
|
+
components: ComponentInfo[];
|
|
50
|
+
nestedComponents?: NestedComponentImpl[];
|
|
51
|
+
tokens: {
|
|
52
|
+
colors: Array<{
|
|
53
|
+
name: string;
|
|
54
|
+
value: string;
|
|
55
|
+
}>;
|
|
56
|
+
typography: Array<{
|
|
57
|
+
name: string;
|
|
58
|
+
fontFamily: string;
|
|
59
|
+
fontSize: number;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
assets: Array<{
|
|
63
|
+
nodeId: string;
|
|
64
|
+
url: string;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Main function to generate code from Figma design context
|
|
69
|
+
*/
|
|
70
|
+
export declare function generateDesignContext(input: DesignContextInput, options?: GenerateOptions): DesignContextOutput;
|
|
71
|
+
/**
|
|
72
|
+
* Format the output for the MCP tool response
|
|
73
|
+
*/
|
|
74
|
+
export declare function formatDesignContextResponse(output: DesignContextOutput, nodeId: string): string;
|
|
75
|
+
export { FigmaNode } from './jsxGenerator.js';
|
|
76
|
+
export { ComponentInfo } from './componentDetector.js';
|
|
77
|
+
export { rgbToHex } from './styleConverter.js';
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Generator - Main orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Combines style conversion, JSX generation, and component detection
|
|
5
|
+
* to produce React + Tailwind code from Figma designs.
|
|
6
|
+
*/
|
|
7
|
+
import { assetServer } from '../assetServer.js';
|
|
8
|
+
import { generateComponent, toComponentName } from './jsxGenerator.js';
|
|
9
|
+
import { detectComponentInstances, collectComponentMetadata, generateNestedComponentImplementations, } from './componentDetector.js';
|
|
10
|
+
/**
|
|
11
|
+
* Process images and store them in the asset server
|
|
12
|
+
*/
|
|
13
|
+
function processImages(images) {
|
|
14
|
+
const imageUrls = new Map();
|
|
15
|
+
if (!images) {
|
|
16
|
+
return imageUrls;
|
|
17
|
+
}
|
|
18
|
+
for (const [nodeId, imageData] of Object.entries(images)) {
|
|
19
|
+
const format = imageData.format;
|
|
20
|
+
const url = assetServer.storeAsset(imageData.data, format);
|
|
21
|
+
imageUrls.set(nodeId, url);
|
|
22
|
+
}
|
|
23
|
+
return imageUrls;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Process design tokens from Figma
|
|
27
|
+
*/
|
|
28
|
+
function processTokens(tokens) {
|
|
29
|
+
const result = {
|
|
30
|
+
colors: [],
|
|
31
|
+
typography: [],
|
|
32
|
+
};
|
|
33
|
+
if (!tokens) {
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
// Process colors
|
|
37
|
+
if (tokens.colors) {
|
|
38
|
+
for (const color of tokens.colors) {
|
|
39
|
+
result.colors.push({
|
|
40
|
+
name: color.name,
|
|
41
|
+
value: color.value,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Process typography
|
|
46
|
+
if (tokens.typography) {
|
|
47
|
+
for (const typo of tokens.typography) {
|
|
48
|
+
result.typography.push({
|
|
49
|
+
name: typo.name,
|
|
50
|
+
fontFamily: typo.fontFamily,
|
|
51
|
+
fontSize: typo.fontSize,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Main function to generate code from Figma design context
|
|
59
|
+
*/
|
|
60
|
+
export function generateDesignContext(input, options = {}) {
|
|
61
|
+
const { node, images, tokens } = input;
|
|
62
|
+
const { generateNestedComponents = true } = options;
|
|
63
|
+
// Process and store images
|
|
64
|
+
const imageUrls = processImages(images);
|
|
65
|
+
// Detect component instances
|
|
66
|
+
const detectedComponents = detectComponentInstances(node);
|
|
67
|
+
const componentMetadata = collectComponentMetadata(detectedComponents);
|
|
68
|
+
// Generate the main component
|
|
69
|
+
const componentName = toComponentName(node.name) || 'Component';
|
|
70
|
+
const generatedComponent = generateComponent(node, imageUrls, componentName);
|
|
71
|
+
// Generate nested component implementations if requested
|
|
72
|
+
let nestedComponents;
|
|
73
|
+
if (generateNestedComponents && detectedComponents.size > 0) {
|
|
74
|
+
const implementations = generateNestedComponentImplementations(detectedComponents);
|
|
75
|
+
nestedComponents = implementations.map(impl => ({
|
|
76
|
+
name: impl.componentName,
|
|
77
|
+
code: impl.code,
|
|
78
|
+
description: impl.description,
|
|
79
|
+
documentationLinks: impl.documentationLinks,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
// Process tokens
|
|
83
|
+
const processedTokens = processTokens(tokens);
|
|
84
|
+
// Build assets list
|
|
85
|
+
const assets = [];
|
|
86
|
+
for (const [nodeId, url] of imageUrls) {
|
|
87
|
+
assets.push({ nodeId, url });
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
code: generatedComponent.code,
|
|
91
|
+
componentName: generatedComponent.name,
|
|
92
|
+
components: componentMetadata,
|
|
93
|
+
nestedComponents,
|
|
94
|
+
tokens: processedTokens,
|
|
95
|
+
assets,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Format the output for the MCP tool response
|
|
100
|
+
*/
|
|
101
|
+
export function formatDesignContextResponse(output, nodeId) {
|
|
102
|
+
const sections = [];
|
|
103
|
+
// Generated React code
|
|
104
|
+
sections.push('## Generated React + Tailwind Code\n');
|
|
105
|
+
sections.push('```tsx');
|
|
106
|
+
sections.push(output.code);
|
|
107
|
+
sections.push('```\n');
|
|
108
|
+
// Nested component implementations
|
|
109
|
+
if (output.nestedComponents && output.nestedComponents.length > 0) {
|
|
110
|
+
sections.push('## Nested Component Implementations\n');
|
|
111
|
+
sections.push('The following components are used in this design:\n');
|
|
112
|
+
for (const comp of output.nestedComponents) {
|
|
113
|
+
sections.push(`### ${comp.name}`);
|
|
114
|
+
if (comp.description) {
|
|
115
|
+
sections.push(`\n${comp.description}\n`);
|
|
116
|
+
}
|
|
117
|
+
if (comp.documentationLinks && comp.documentationLinks.length > 0) {
|
|
118
|
+
sections.push('**Documentation:**');
|
|
119
|
+
for (const link of comp.documentationLinks) {
|
|
120
|
+
sections.push(`- ${link.uri}`);
|
|
121
|
+
}
|
|
122
|
+
sections.push('');
|
|
123
|
+
}
|
|
124
|
+
sections.push('```tsx');
|
|
125
|
+
sections.push(comp.code);
|
|
126
|
+
sections.push('```\n');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Component prop types (for components without full implementations)
|
|
130
|
+
if (output.components.length > 0) {
|
|
131
|
+
const componentsWithoutImpl = output.components.filter(c => !output.nestedComponents?.some(nc => nc.name === c.componentName));
|
|
132
|
+
if (componentsWithoutImpl.length > 0) {
|
|
133
|
+
sections.push('## Component Types\n');
|
|
134
|
+
for (const comp of componentsWithoutImpl) {
|
|
135
|
+
sections.push(`### ${comp.componentName}`);
|
|
136
|
+
if (comp.description) {
|
|
137
|
+
sections.push(`\n${comp.description}`);
|
|
138
|
+
}
|
|
139
|
+
if (comp.documentationLinks && comp.documentationLinks.length > 0) {
|
|
140
|
+
sections.push('\n**Documentation:**');
|
|
141
|
+
for (const link of comp.documentationLinks) {
|
|
142
|
+
sections.push(`- ${link.uri}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (comp.propsInterface) {
|
|
146
|
+
sections.push('\n```typescript');
|
|
147
|
+
sections.push(comp.propsInterface);
|
|
148
|
+
sections.push('```');
|
|
149
|
+
}
|
|
150
|
+
sections.push('');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Design tokens summary (inline format like figma-desktop)
|
|
155
|
+
if (output.tokens.colors.length > 0 || output.tokens.typography.length > 0) {
|
|
156
|
+
sections.push('## Design Tokens\n');
|
|
157
|
+
if (output.tokens.colors.length > 0) {
|
|
158
|
+
const colorList = output.tokens.colors
|
|
159
|
+
.map(c => `${c.name}: ${c.value}`)
|
|
160
|
+
.join(', ');
|
|
161
|
+
sections.push(`**Colors:** ${colorList}\n`);
|
|
162
|
+
}
|
|
163
|
+
if (output.tokens.typography.length > 0) {
|
|
164
|
+
const typoList = output.tokens.typography
|
|
165
|
+
.map(t => `${t.name}: ${t.fontFamily} ${t.fontSize}px`)
|
|
166
|
+
.join(', ');
|
|
167
|
+
sections.push(`**Typography:** ${typoList}\n`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Assets
|
|
171
|
+
if (output.assets.length > 0) {
|
|
172
|
+
sections.push('## Assets\n');
|
|
173
|
+
sections.push('The following images are available:');
|
|
174
|
+
for (const asset of output.assets) {
|
|
175
|
+
sections.push(`- Node ${asset.nodeId}: ${asset.url}`);
|
|
176
|
+
}
|
|
177
|
+
sections.push('');
|
|
178
|
+
}
|
|
179
|
+
// Screenshot reminder
|
|
180
|
+
sections.push('---');
|
|
181
|
+
sections.push(`💡 **Tip:** Use \`capture_screenshot\` with nodeId "${nodeId}" to see the visual design for better context.`);
|
|
182
|
+
return sections.join('\n');
|
|
183
|
+
}
|
|
184
|
+
export { rgbToHex } from './styleConverter.js';
|