figma-code-agent 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/README.md +133 -0
- package/bin/install.js +328 -0
- package/knowledge/README.md +62 -0
- package/knowledge/css-strategy.md +973 -0
- package/knowledge/design-to-code-assets.md +855 -0
- package/knowledge/design-to-code-layout.md +929 -0
- package/knowledge/design-to-code-semantic.md +1085 -0
- package/knowledge/design-to-code-typography.md +1003 -0
- package/knowledge/design-to-code-visual.md +1145 -0
- package/knowledge/design-tokens-variables.md +1261 -0
- package/knowledge/design-tokens.md +960 -0
- package/knowledge/figma-api-devmode.md +894 -0
- package/knowledge/figma-api-plugin.md +920 -0
- package/knowledge/figma-api-rest.md +742 -0
- package/knowledge/figma-api-variables.md +848 -0
- package/knowledge/figma-api-webhooks.md +876 -0
- package/knowledge/payload-blocks.md +1184 -0
- package/knowledge/payload-figma-mapping.md +1210 -0
- package/knowledge/payload-visual-builder.md +1004 -0
- package/knowledge/plugin-architecture.md +1176 -0
- package/knowledge/plugin-best-practices.md +1206 -0
- package/knowledge/plugin-codegen.md +1313 -0
- package/package.json +31 -0
- package/skills/README.md +103 -0
- package/skills/audit-plugin/SKILL.md +244 -0
- package/skills/build-codegen-plugin/SKILL.md +279 -0
- package/skills/build-importer/SKILL.md +320 -0
- package/skills/build-plugin/SKILL.md +199 -0
- package/skills/build-token-pipeline/SKILL.md +363 -0
- package/skills/ref-html/SKILL.md +290 -0
- package/skills/ref-layout/SKILL.md +150 -0
- package/skills/ref-payload-block/SKILL.md +415 -0
- package/skills/ref-react/SKILL.md +222 -0
- package/skills/ref-tokens/SKILL.md +347 -0
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
# Codegen Plugin Development Patterns
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Production-tested patterns for building Figma codegen plugins — covering the full development lifecycle from manifest setup through code generation, output quality, and Dev Mode integration. This module documents **how to build** codegen plugins that generate clean, production-ready code. For the codegen API reference (types, methods, events), see `figma-api-devmode.md`. For general plugin architecture patterns (IPC, project structure, data flow), see `plugin-architecture.md`.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reference this module when you need to:
|
|
10
|
+
|
|
11
|
+
- Build a codegen plugin that generates code from Figma designs in Dev Mode
|
|
12
|
+
- Implement the `figma.codegen.on('generate', ...)` callback with production patterns
|
|
13
|
+
- Set up codegen preferences (unit conversion, CSS strategy selection)
|
|
14
|
+
- Generate multi-language output (React/TSX, Vue, HTML + CSS, Tailwind)
|
|
15
|
+
- Ensure generated code quality (validation, sanitization, formatting)
|
|
16
|
+
- Decide between building a standard plugin vs codegen plugin vs both
|
|
17
|
+
- Implement responsive code generation from multiple design variants
|
|
18
|
+
- Link generated code back to Figma nodes through Dev Resources
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Content
|
|
23
|
+
|
|
24
|
+
### Codegen Plugin Setup
|
|
25
|
+
|
|
26
|
+
#### Manifest Configuration
|
|
27
|
+
|
|
28
|
+
Codegen plugins use `editorType: ["dev"]` with `capabilities: ["codegen"]`. This makes the plugin available exclusively in Dev Mode's Inspect panel.
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"name": "My Codegen Plugin",
|
|
33
|
+
"id": "YOUR_PLUGIN_ID",
|
|
34
|
+
"api": "1.0.0",
|
|
35
|
+
"main": "code.js",
|
|
36
|
+
"editorType": ["dev"],
|
|
37
|
+
"documentAccess": "dynamic-page",
|
|
38
|
+
"capabilities": ["codegen"],
|
|
39
|
+
"codegenLanguages": [
|
|
40
|
+
{ "label": "React (TSX)", "value": "react" },
|
|
41
|
+
{ "label": "HTML + CSS", "value": "html-css" },
|
|
42
|
+
{ "label": "Tailwind", "value": "tailwind" }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Critical:** Codegen plugins **must** use `editorType: ["dev"]`. Using `["figma"]` creates a standard plugin that runs in design mode, not a codegen plugin. This is the most common setup error.
|
|
48
|
+
|
|
49
|
+
#### @create-figma-plugin Setup for Codegen
|
|
50
|
+
|
|
51
|
+
When using `@create-figma-plugin`, configure the `figma-plugin` section in `package.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"figma-plugin": {
|
|
56
|
+
"name": "My Codegen Plugin",
|
|
57
|
+
"id": "YOUR_PLUGIN_ID",
|
|
58
|
+
"editorType": ["dev"],
|
|
59
|
+
"main": "src/main.ts",
|
|
60
|
+
"documentAccess": "dynamic-page",
|
|
61
|
+
"capabilities": ["codegen"],
|
|
62
|
+
"codegenLanguages": [
|
|
63
|
+
{ "label": "React (TSX)", "value": "react" },
|
|
64
|
+
{ "label": "HTML + CSS", "value": "html-css" }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> **No UI entry:** Codegen plugins typically do not have a `ui` entry because their output renders directly in the Inspect panel. However, if you need a hidden iframe for browser API access (fetch, complex processing) or action preferences, you can add `"ui": "src/ui.tsx"` and use `figma.showUI()` at runtime.
|
|
71
|
+
|
|
72
|
+
#### Build Toolchain
|
|
73
|
+
|
|
74
|
+
The build setup is identical to standard plugins:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "build-figma-plugin --typecheck --minify",
|
|
80
|
+
"watch": "build-figma-plugin --typecheck --watch"
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"@create-figma-plugin/utilities": "^4.0.3"
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"@create-figma-plugin/build": "^4.0.3",
|
|
87
|
+
"@create-figma-plugin/tsconfig": "^4.0.3",
|
|
88
|
+
"@figma/plugin-typings": "1.109.0",
|
|
89
|
+
"typescript": ">=5"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Note that `@create-figma-plugin/ui` and `preact` are only needed if your codegen plugin uses a UI for action preferences. For pure code generation, utilities alone suffice.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### Codegen Plugin Lifecycle
|
|
99
|
+
|
|
100
|
+
#### The Generate Callback
|
|
101
|
+
|
|
102
|
+
The generate callback is the heart of every codegen plugin. It fires whenever a user selects a node in Dev Mode with your plugin's language active:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
figma.codegen.on('generate', async (event: CodegenEvent): Promise<CodegenResult[]> => {
|
|
106
|
+
const { node, language } = event;
|
|
107
|
+
|
|
108
|
+
// Route to language-specific generator
|
|
109
|
+
switch (language) {
|
|
110
|
+
case 'react':
|
|
111
|
+
return generateReact(node);
|
|
112
|
+
case 'html-css':
|
|
113
|
+
return generateHTMLCSS(node);
|
|
114
|
+
case 'tailwind':
|
|
115
|
+
return generateTailwind(node);
|
|
116
|
+
default:
|
|
117
|
+
return [{
|
|
118
|
+
title: 'Unsupported',
|
|
119
|
+
code: `// Language "${language}" not yet supported`,
|
|
120
|
+
language: 'PLAINTEXT',
|
|
121
|
+
}];
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### CodegenEvent and CodegenResult
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
interface CodegenEvent {
|
|
130
|
+
node: SceneNode; // The selected node in Dev Mode
|
|
131
|
+
language: string; // Value from codegenLanguages in manifest
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface CodegenResult {
|
|
135
|
+
title: string; // Section title in Inspect panel
|
|
136
|
+
code: string; // Generated code
|
|
137
|
+
language: 'TYPESCRIPT' | 'CSS' | 'HTML' | 'JSON' | 'PLAINTEXT' | /* ... */;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Return an array of `CodegenResult` to produce multiple code sections. Each result becomes a separate collapsible section in the Inspect panel with syntax highlighting:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
return [
|
|
145
|
+
{ title: 'Component.tsx', code: tsxCode, language: 'TYPESCRIPT' },
|
|
146
|
+
{ title: 'Component.module.css', code: cssCode, language: 'CSS' },
|
|
147
|
+
{ title: 'tokens.css', code: tokensCode, language: 'CSS' },
|
|
148
|
+
];
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### The 3-Second Timeout
|
|
152
|
+
|
|
153
|
+
The generate callback has a **hard 3-second timeout**. If the callback does not return within 3 seconds, Figma cancels it. This constraint shapes the entire architecture:
|
|
154
|
+
|
|
155
|
+
- **Pre-cache data** during `preferenceschange` events, not during generation
|
|
156
|
+
- **Keep generation synchronous** where possible
|
|
157
|
+
- **Limit tree traversal depth** to prevent deep recursion on complex nodes
|
|
158
|
+
- **Avoid network calls** in the generate path (pre-fetch data via action preferences)
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// BAD: Slow generation that may timeout
|
|
162
|
+
figma.codegen.on('generate', async ({ node }) => {
|
|
163
|
+
const allNodes = figma.currentPage.findAll(() => true); // Slow!
|
|
164
|
+
const mappings = await fetchMappings('https://api.example.com/map'); // Network!
|
|
165
|
+
return generateCode(node, allNodes, mappings);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// GOOD: Fast generation with pre-cached data
|
|
169
|
+
let cachedMappings: Record<string, string> = {};
|
|
170
|
+
|
|
171
|
+
figma.codegen.on('preferenceschange', async (event) => {
|
|
172
|
+
if (event.propertyName === 'Component Mapping') {
|
|
173
|
+
figma.showUI(__html__, { width: 500, height: 400 });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
figma.ui.on('message', async (msg) => {
|
|
178
|
+
if (msg.type === 'MAPPINGS_UPDATED') {
|
|
179
|
+
cachedMappings = msg.mappings;
|
|
180
|
+
await figma.clientStorage.setAsync('mappings', cachedMappings);
|
|
181
|
+
figma.ui.hide();
|
|
182
|
+
figma.codegen.refresh();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
figma.codegen.on('generate', ({ node }) => {
|
|
187
|
+
return generateCode(node, cachedMappings); // Fast, no I/O
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### Preferences System
|
|
194
|
+
|
|
195
|
+
Codegen preferences let users customize code output without modifying the plugin. They appear in the Dev Mode Inspect panel alongside the language selector.
|
|
196
|
+
|
|
197
|
+
#### Unit Preferences
|
|
198
|
+
|
|
199
|
+
Allow users to choose CSS units and set a scale factor for conversion:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"itemType": "unit",
|
|
204
|
+
"propertyName": "Size Unit",
|
|
205
|
+
"scaledUnit": "rem",
|
|
206
|
+
"defaultScaleFactor": 16,
|
|
207
|
+
"includedLanguages": ["react", "html-css"]
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Access in the generate callback:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
figma.codegen.on('generate', ({ node }) => {
|
|
215
|
+
const unit = figma.codegen.preferences.unit; // "rem" or "px"
|
|
216
|
+
const scale = figma.codegen.preferences.scaleFactor; // 16
|
|
217
|
+
|
|
218
|
+
function convertSize(px: number): string {
|
|
219
|
+
if (unit === 'rem') return `${(px / scale).toFixed(3).replace(/\.?0+$/, '')}rem`;
|
|
220
|
+
if (unit === 'em') return `${(px / scale).toFixed(3).replace(/\.?0+$/, '')}em`;
|
|
221
|
+
return `${px}px`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const width = convertSize(node.width);
|
|
225
|
+
// ...
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### Select Preferences
|
|
230
|
+
|
|
231
|
+
Dropdown options for code generation strategy:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"itemType": "select",
|
|
236
|
+
"propertyName": "CSS Strategy",
|
|
237
|
+
"options": [
|
|
238
|
+
{ "label": "CSS Modules", "value": "modules", "isDefault": true },
|
|
239
|
+
{ "label": "Tailwind CSS", "value": "tailwind" },
|
|
240
|
+
{ "label": "Styled Components", "value": "styled" },
|
|
241
|
+
{ "label": "Inline Styles", "value": "inline" }
|
|
242
|
+
],
|
|
243
|
+
"includedLanguages": ["react"]
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Access via `figma.codegen.preferences['CSS Strategy']`.
|
|
248
|
+
|
|
249
|
+
Common select preference patterns:
|
|
250
|
+
|
|
251
|
+
| Preference | Options | Use Case |
|
|
252
|
+
|-----------|---------|----------|
|
|
253
|
+
| CSS Strategy | Modules, Tailwind, Styled, Inline | React styling approach |
|
|
254
|
+
| Naming Convention | BEM, camelCase, kebab-case | Class name format |
|
|
255
|
+
| Token Mode | CSS Variables, SCSS Variables, None | Design token output |
|
|
256
|
+
| Export Mode | Component, Page Section, Full Page | Scope of generation |
|
|
257
|
+
| Include Comments | Yes, No | Code documentation |
|
|
258
|
+
|
|
259
|
+
#### Action Preferences
|
|
260
|
+
|
|
261
|
+
For complex configuration that requires a custom UI (component mapping tables, token overrides):
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"itemType": "action",
|
|
266
|
+
"propertyName": "Component Mapping",
|
|
267
|
+
"label": "Configure Mappings"
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
When the user clicks the action button, handle it with the `preferenceschange` event:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
figma.codegen.on('preferenceschange', async (event) => {
|
|
275
|
+
if (event.propertyName === 'Component Mapping') {
|
|
276
|
+
// Show a configuration UI
|
|
277
|
+
figma.showUI(__html__, { width: 500, height: 400 });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
figma.ui.on('message', async (msg) => {
|
|
282
|
+
if (msg.type === 'CONFIG_SAVED') {
|
|
283
|
+
await figma.clientStorage.setAsync('componentConfig', msg.config);
|
|
284
|
+
figma.ui.hide();
|
|
285
|
+
figma.codegen.refresh(); // Force regeneration with new config
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
> **Tip:** Always persist action preference data to `figma.clientStorage` so it survives plugin restarts. Load it once at startup and cache it in memory for the generate callback.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### Code Generation Patterns
|
|
295
|
+
|
|
296
|
+
#### Architecture: Extraction → Generation Pipeline
|
|
297
|
+
|
|
298
|
+
The same three-stage pipeline from `plugin-architecture.md` applies to codegen plugins, but compressed into the 3-second timeout window:
|
|
299
|
+
|
|
300
|
+
1. **Extract** — Read node properties into a JSON-serializable schema
|
|
301
|
+
2. **Generate** — Transform extracted data into code strings
|
|
302
|
+
3. **Return** — Package as `CodegenResult[]`
|
|
303
|
+
|
|
304
|
+
For codegen plugins, extraction and generation typically happen inline within the generate callback. For complex plugins that need the full pipeline, extract on selection change and cache the result.
|
|
305
|
+
|
|
306
|
+
#### React/TSX Generation
|
|
307
|
+
|
|
308
|
+
Generate React components with typed props and CSS Modules:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
function generateReact(node: SceneNode): CodegenResult[] {
|
|
312
|
+
const componentName = toPascalCase(node.name);
|
|
313
|
+
const { jsx, styles, imports } = buildComponent(node);
|
|
314
|
+
|
|
315
|
+
const tsx = [
|
|
316
|
+
...imports,
|
|
317
|
+
`import styles from './${componentName}.module.css';`,
|
|
318
|
+
'',
|
|
319
|
+
`interface ${componentName}Props {`,
|
|
320
|
+
` className?: string;`,
|
|
321
|
+
`}`,
|
|
322
|
+
'',
|
|
323
|
+
`export function ${componentName}({ className }: ${componentName}Props) {`,
|
|
324
|
+
` return (`,
|
|
325
|
+
` ${jsx}`,
|
|
326
|
+
` );`,
|
|
327
|
+
`}`,
|
|
328
|
+
].join('\n');
|
|
329
|
+
|
|
330
|
+
const css = renderStylesheet(styles);
|
|
331
|
+
|
|
332
|
+
return [
|
|
333
|
+
{ title: `${componentName}.tsx`, code: tsx, language: 'TYPESCRIPT' },
|
|
334
|
+
{ title: `${componentName}.module.css`, code: css, language: 'CSS' },
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### HTML + CSS Generation
|
|
340
|
+
|
|
341
|
+
Generate semantic HTML with a separate stylesheet:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
function generateHTMLCSS(node: SceneNode): CodegenResult[] {
|
|
345
|
+
const extracted = extractNode(node);
|
|
346
|
+
const element = generateElement(extracted);
|
|
347
|
+
const html = renderHTML(element);
|
|
348
|
+
const css = renderCSS(collectCSSRules(element));
|
|
349
|
+
|
|
350
|
+
return [
|
|
351
|
+
{ title: 'HTML', code: html, language: 'HTML' },
|
|
352
|
+
{ title: 'CSS', code: css, language: 'CSS' },
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### CSS Generation: Layered Approach
|
|
358
|
+
|
|
359
|
+
Generate CSS in three distinct layers for maintainability:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
function generateCSS(node: SceneNode): string {
|
|
363
|
+
const rules: string[] = [];
|
|
364
|
+
const selector = `.${generateClassName(node.name)}`;
|
|
365
|
+
|
|
366
|
+
// Layer 1: Layout (structural bones)
|
|
367
|
+
const layoutCSS = generateLayoutCSS(node);
|
|
368
|
+
if (layoutCSS) rules.push(layoutCSS);
|
|
369
|
+
|
|
370
|
+
// Layer 2: Visual (design skin)
|
|
371
|
+
const visualCSS = generateVisualCSS(node);
|
|
372
|
+
if (visualCSS) rules.push(visualCSS);
|
|
373
|
+
|
|
374
|
+
// Layer 3: Typography (text styles)
|
|
375
|
+
if (node.type === 'TEXT') {
|
|
376
|
+
const typographyCSS = generateTypographyCSS(node);
|
|
377
|
+
if (typographyCSS) rules.push(typographyCSS);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return `${selector} {\n${rules.join('\n')}\n}`;
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
> **Cross-reference:** See `css-strategy.md` for the full three-layer CSS architecture. See `design-to-code-layout.md`, `design-to-code-visual.md`, and `design-to-code-typography.md` for the specific property mapping rules.
|
|
385
|
+
|
|
386
|
+
#### Layout CSS from Auto Layout
|
|
387
|
+
|
|
388
|
+
Map Figma Auto Layout properties to CSS Flexbox. This is the most critical mapping in design-to-code generation:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
function generateLayoutCSS(node: SceneNode): string {
|
|
392
|
+
if (!('layoutMode' in node) || node.layoutMode === 'NONE') return '';
|
|
393
|
+
|
|
394
|
+
const rules: string[] = [];
|
|
395
|
+
rules.push(' display: flex;');
|
|
396
|
+
rules.push(` flex-direction: ${node.layoutMode === 'HORIZONTAL' ? 'row' : 'column'};`);
|
|
397
|
+
|
|
398
|
+
// Gap
|
|
399
|
+
if (node.itemSpacing > 0) {
|
|
400
|
+
rules.push(` gap: ${node.itemSpacing}px;`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Wrap
|
|
404
|
+
if (node.layoutWrap === 'WRAP') {
|
|
405
|
+
rules.push(' flex-wrap: wrap;');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Primary axis alignment → justify-content
|
|
409
|
+
const justifyMap: Record<string, string> = {
|
|
410
|
+
MIN: 'flex-start', CENTER: 'center',
|
|
411
|
+
MAX: 'flex-end', SPACE_BETWEEN: 'space-between',
|
|
412
|
+
};
|
|
413
|
+
rules.push(` justify-content: ${justifyMap[node.primaryAxisAlignItems]};`);
|
|
414
|
+
|
|
415
|
+
// Cross axis alignment → align-items
|
|
416
|
+
const alignMap: Record<string, string> = {
|
|
417
|
+
MIN: 'flex-start', CENTER: 'center',
|
|
418
|
+
MAX: 'flex-end', BASELINE: 'baseline',
|
|
419
|
+
};
|
|
420
|
+
rules.push(` align-items: ${alignMap[node.counterAxisAlignItems]};`);
|
|
421
|
+
|
|
422
|
+
// Padding
|
|
423
|
+
const { paddingTop: pt, paddingRight: pr, paddingBottom: pb, paddingLeft: pl } = node;
|
|
424
|
+
if (pt || pr || pb || pl) {
|
|
425
|
+
if (pt === pr && pr === pb && pb === pl) {
|
|
426
|
+
rules.push(` padding: ${pt}px;`);
|
|
427
|
+
} else {
|
|
428
|
+
rules.push(` padding: ${pt}px ${pr}px ${pb}px ${pl}px;`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return rules.join('\n');
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
> **Cross-reference:** `design-to-code-layout.md` provides the complete mapping table, including child sizing modes (FIXED → explicit width, HUG → auto, FILL → flex: 1), min/max constraints, and wrap alignment.
|
|
437
|
+
|
|
438
|
+
#### Component Instance Handling
|
|
439
|
+
|
|
440
|
+
When the selected node is a component instance, generate usage code rather than the full component definition:
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
figma.codegen.on('generate', async ({ node, language }) => {
|
|
444
|
+
if (node.type === 'INSTANCE') {
|
|
445
|
+
const mainComponent = await node.getMainComponentAsync();
|
|
446
|
+
if (mainComponent) {
|
|
447
|
+
const name = toPascalCase(mainComponent.name);
|
|
448
|
+
const props = extractComponentProps(node, mainComponent);
|
|
449
|
+
const propsStr = formatProps(props);
|
|
450
|
+
|
|
451
|
+
return [{
|
|
452
|
+
title: 'Usage',
|
|
453
|
+
language: 'TYPESCRIPT',
|
|
454
|
+
code: propsStr
|
|
455
|
+
? `<${name}\n${propsStr}\n/>`
|
|
456
|
+
: `<${name} />`,
|
|
457
|
+
}];
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (node.type === 'COMPONENT') {
|
|
462
|
+
return generateComponentDefinition(node, language);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return generateInlineElement(node, language);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
function extractComponentProps(
|
|
469
|
+
instance: InstanceNode,
|
|
470
|
+
component: ComponentNode
|
|
471
|
+
): Record<string, string> {
|
|
472
|
+
const props: Record<string, string> = {};
|
|
473
|
+
|
|
474
|
+
// Extract variant properties
|
|
475
|
+
if ('variantProperties' in instance && instance.variantProperties) {
|
|
476
|
+
for (const [key, value] of Object.entries(instance.variantProperties)) {
|
|
477
|
+
props[toCamelCase(key)] = value;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Extract text overrides by comparing instance children to main component
|
|
482
|
+
// (implementation depends on component structure)
|
|
483
|
+
|
|
484
|
+
return props;
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Semantic HTML Tag Selection
|
|
489
|
+
|
|
490
|
+
Choose HTML tags based on node name, type, and content — not just `<div>` for everything:
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
function getSemanticTag(node: ExtractedNode, context: SemanticContext): string {
|
|
494
|
+
const name = node.name.toLowerCase();
|
|
495
|
+
|
|
496
|
+
// Text nodes
|
|
497
|
+
if (node.type === 'TEXT') {
|
|
498
|
+
// Heading detection by font size and weight
|
|
499
|
+
if (node.text && node.text.font.size >= 32 && context.canUseH1()) {
|
|
500
|
+
context.useH1();
|
|
501
|
+
return 'h1';
|
|
502
|
+
}
|
|
503
|
+
if (node.text && node.text.font.size >= 24) return 'h2';
|
|
504
|
+
if (node.text && node.text.font.size >= 20) return 'h3';
|
|
505
|
+
return 'p';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Name-based detection
|
|
509
|
+
if (name.includes('header') || name.includes('navbar')) return 'header';
|
|
510
|
+
if (name.includes('footer')) return 'footer';
|
|
511
|
+
if (name.includes('nav') || name.includes('menu')) return 'nav';
|
|
512
|
+
if (name.includes('sidebar') || name.includes('aside')) return 'aside';
|
|
513
|
+
if (name.includes('article') || name.includes('post')) return 'article';
|
|
514
|
+
if (name.includes('section')) return 'section';
|
|
515
|
+
if (name.includes('main') || name.includes('content')) return 'main';
|
|
516
|
+
if (name.includes('button') || name.includes('btn') || name.includes('cta')) return 'button';
|
|
517
|
+
if (name.includes('link')) return 'a';
|
|
518
|
+
if (name.includes('list')) return 'ul';
|
|
519
|
+
if (name.includes('item') && context.isInsideList()) return 'li';
|
|
520
|
+
if (name.includes('image') || name.includes('photo') || name.includes('avatar')) return 'img';
|
|
521
|
+
|
|
522
|
+
return 'div';
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Key principles for semantic tag selection:
|
|
527
|
+
- Only one `<h1>` per page — track with `SemanticContext`
|
|
528
|
+
- Headings must follow hierarchy (`h1` > `h2` > `h3`, never skip levels)
|
|
529
|
+
- No headings inside buttons or links
|
|
530
|
+
- Interactive elements (`button`, `a`) based on name patterns
|
|
531
|
+
|
|
532
|
+
> **Cross-reference:** See `design-to-code-semantic.md` for the complete semantic HTML mapping rules and ARIA attribute patterns.
|
|
533
|
+
|
|
534
|
+
#### BEM Class Name Generation
|
|
535
|
+
|
|
536
|
+
Generate flat BEM class names from Figma layer names:
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
function generateBEMClassName(
|
|
540
|
+
nodeName: string,
|
|
541
|
+
parentClassName: string | null,
|
|
542
|
+
depth: number,
|
|
543
|
+
prefix?: string
|
|
544
|
+
): string {
|
|
545
|
+
const element = sanitizeClassName(nodeName);
|
|
546
|
+
|
|
547
|
+
// Root level → block name
|
|
548
|
+
if (!parentClassName || depth === 0) {
|
|
549
|
+
return prefix ? `${prefix}${element}` : element;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// BEM element: block__element (always flat, never block__el__sub)
|
|
553
|
+
const block = parentClassName.split('__')[0];
|
|
554
|
+
return `${block}__${element}`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function sanitizeClassName(name: string): string {
|
|
558
|
+
return name
|
|
559
|
+
.replace(/^(frame|group|component|instance|vector|text)\s*/i, '')
|
|
560
|
+
.toLowerCase()
|
|
561
|
+
.trim()
|
|
562
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
563
|
+
.replace(/^-+|-+$/g, '')
|
|
564
|
+
.replace(/-+/g, '-')
|
|
565
|
+
.replace(/^[0-9]/, 'el-$&')
|
|
566
|
+
.substring(0, 32)
|
|
567
|
+
|| 'element';
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Deduplicate class names when multiple nodes have the same sanitized name:
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
class ClassNameTracker {
|
|
575
|
+
private used = new Map<string, number>();
|
|
576
|
+
|
|
577
|
+
getUnique(baseName: string): string {
|
|
578
|
+
const count = this.used.get(baseName) || 0;
|
|
579
|
+
this.used.set(baseName, count + 1);
|
|
580
|
+
return count === 0 ? baseName : `${baseName}-${count + 1}`;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Example output:
|
|
586
|
+
```
|
|
587
|
+
card ← root frame
|
|
588
|
+
card__header ← first child "Header"
|
|
589
|
+
card__title ← text inside header
|
|
590
|
+
card__body ← second child "Body"
|
|
591
|
+
card__description ← text inside body
|
|
592
|
+
card__footer ← third child "Footer"
|
|
593
|
+
card__button ← button inside footer
|
|
594
|
+
card__button-2 ← second button (deduped)
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
#### Optional Class Prefix
|
|
598
|
+
|
|
599
|
+
Support an optional prefix for all generated class names to avoid collisions when integrating generated code into existing projects:
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
// With prefix "fr-":
|
|
603
|
+
// fr-card, fr-card__header, fr-card__title
|
|
604
|
+
|
|
605
|
+
const output = await generateOutput(extracted, {
|
|
606
|
+
classPrefix: 'fr-',
|
|
607
|
+
});
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
### Responsive Code Generation
|
|
613
|
+
|
|
614
|
+
#### Multi-Frame Responsive Mode
|
|
615
|
+
|
|
616
|
+
When users select multiple frames representing different breakpoints (mobile, tablet, desktop), generate unified CSS with media queries:
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
┌─────────────┐ ┌──────────────┐ ┌───────────────┐
|
|
620
|
+
│ Mobile Frame │ │ Tablet Frame │ │ Desktop Frame │
|
|
621
|
+
│ (375×812) │ │ (768×1024) │ │ (1440×900) │
|
|
622
|
+
└──────┬───────┘ └───────┬───────┘ └───────┬────────┘
|
|
623
|
+
│ │ │
|
|
624
|
+
└─────────┬─────────┘────────────────────┘
|
|
625
|
+
▼
|
|
626
|
+
┌──────────────────────┐
|
|
627
|
+
│ Unified Responsive │
|
|
628
|
+
│ CSS with @media │
|
|
629
|
+
│ queries │
|
|
630
|
+
└──────────────────────┘
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
The process:
|
|
634
|
+
|
|
635
|
+
1. **Detect breakpoints** from frame names or dimensions
|
|
636
|
+
2. **Extract all frames** independently
|
|
637
|
+
3. **Match elements** across frames by normalized layer name
|
|
638
|
+
4. **Generate base styles** from the smallest breakpoint (mobile-first)
|
|
639
|
+
5. **Generate `@media` overrides** for larger breakpoints with only differing properties
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
// Standard breakpoints (mobile-first)
|
|
643
|
+
const BREAKPOINTS = [
|
|
644
|
+
{ name: 'mobile', minWidth: null, maxWidth: 767 }, // base (no media query)
|
|
645
|
+
{ name: 'tablet', minWidth: 768, maxWidth: 1023 },
|
|
646
|
+
{ name: 'desktop', minWidth: 1024, maxWidth: null },
|
|
647
|
+
];
|
|
648
|
+
|
|
649
|
+
// Detect breakpoint from frame name or width
|
|
650
|
+
function detectBreakpoint(frame: { name: string; width: number }): BreakpointConfig {
|
|
651
|
+
const name = frame.name.toLowerCase();
|
|
652
|
+
for (const bp of BREAKPOINTS) {
|
|
653
|
+
if (name.includes(bp.name)) return bp;
|
|
654
|
+
}
|
|
655
|
+
// Fallback to dimension detection
|
|
656
|
+
if (frame.width < 768) return BREAKPOINTS[0];
|
|
657
|
+
if (frame.width < 1024) return BREAKPOINTS[1];
|
|
658
|
+
return BREAKPOINTS[2];
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
#### Element Matching Across Frames
|
|
663
|
+
|
|
664
|
+
Match elements between breakpoint frames by normalizing layer names (stripping breakpoint suffixes):
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
function normalizeLayerName(name: string): string {
|
|
668
|
+
return name
|
|
669
|
+
.replace(/\s*\[(?:mobile|tablet|desktop)\]\s*/gi, '')
|
|
670
|
+
.replace(/\s*[-–]\s*(?:mobile|tablet|desktop)\s*/gi, '')
|
|
671
|
+
.trim()
|
|
672
|
+
.toLowerCase();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// "Header [mobile]" and "Header [desktop]" both normalize to "header"
|
|
676
|
+
// This enables matching the same element across breakpoint frames
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
#### Generating Responsive CSS
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
function generateResponsiveCSS(
|
|
683
|
+
matched: MatchedElementGroup[],
|
|
684
|
+
breakpoints: BreakpointConfig[]
|
|
685
|
+
): string {
|
|
686
|
+
const output: string[] = [];
|
|
687
|
+
|
|
688
|
+
// Base styles (mobile-first, no media query)
|
|
689
|
+
for (const group of matched) {
|
|
690
|
+
output.push(`${group.selector} {`);
|
|
691
|
+
output.push(renderProperties(group.baseStyles));
|
|
692
|
+
output.push('}');
|
|
693
|
+
output.push('');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Media query overrides for larger breakpoints
|
|
697
|
+
for (const bp of breakpoints) {
|
|
698
|
+
if (bp.minWidth === null) continue; // Skip base
|
|
699
|
+
|
|
700
|
+
const overrides = matched
|
|
701
|
+
.filter(g => g.responsiveStyles.has(bp.name))
|
|
702
|
+
.map(g => ({
|
|
703
|
+
selector: g.selector,
|
|
704
|
+
styles: g.responsiveStyles.get(bp.name)!,
|
|
705
|
+
}))
|
|
706
|
+
.filter(o => Object.keys(o.styles).length > 0);
|
|
707
|
+
|
|
708
|
+
if (overrides.length === 0) continue;
|
|
709
|
+
|
|
710
|
+
output.push(`@media (min-width: ${bp.minWidth}px) {`);
|
|
711
|
+
for (const override of overrides) {
|
|
712
|
+
output.push(` ${override.selector} {`);
|
|
713
|
+
output.push(renderProperties(override.styles, ' '));
|
|
714
|
+
output.push(' }');
|
|
715
|
+
}
|
|
716
|
+
output.push('}');
|
|
717
|
+
output.push('');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return output.join('\n');
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
#### Component Variant-Based Responsive
|
|
725
|
+
|
|
726
|
+
An alternative to multi-frame responsive: detect responsive component sets (COMPONENT_SET with a Device/Breakpoint variant property) and generate media queries from the variant children:
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
async function resolveVariantSets(extracted: ExtractedNode): Promise<Map<string, VariantSet>> {
|
|
730
|
+
const variantSets = new Map<string, VariantSet>();
|
|
731
|
+
|
|
732
|
+
// Find INSTANCE nodes with a responsive property (e.g., "Device")
|
|
733
|
+
function findResponsiveInstances(node: ExtractedNode) {
|
|
734
|
+
if (node.componentRef?.responsiveProperty) {
|
|
735
|
+
// Resolve the parent COMPONENT_SET and extract all variant COMPONENT trees
|
|
736
|
+
// Each variant maps to a breakpoint
|
|
737
|
+
}
|
|
738
|
+
node.children?.forEach(findResponsiveInstances);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
findResponsiveInstances(extracted);
|
|
742
|
+
return variantSets;
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
This enables responsive code generation from a single frame selection when the frame contains responsive component instances.
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
### Code Quality in Generated Output
|
|
751
|
+
|
|
752
|
+
Generated code must be clean enough for developers to use directly. Quality problems in generated output erode trust in the tool.
|
|
753
|
+
|
|
754
|
+
#### HTML Validation
|
|
755
|
+
|
|
756
|
+
Validate tag structure and bracket matching in generated HTML:
|
|
757
|
+
|
|
758
|
+
```ts
|
|
759
|
+
interface ValidationResult {
|
|
760
|
+
valid: boolean;
|
|
761
|
+
errors: ValidationError[];
|
|
762
|
+
sanitizedCode: string;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
interface ValidationError {
|
|
766
|
+
line: number;
|
|
767
|
+
column: number;
|
|
768
|
+
message: string;
|
|
769
|
+
type: 'error' | 'warning';
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function validateHTML(html: string): ValidationResult {
|
|
773
|
+
const errors: ValidationError[] = [];
|
|
774
|
+
let sanitized = html;
|
|
775
|
+
|
|
776
|
+
// 1. Remove JavaScript for security (script tags, inline handlers)
|
|
777
|
+
sanitized = removeJavaScript(sanitized, errors);
|
|
778
|
+
|
|
779
|
+
// 2. Validate tag structure (unclosed tags, mismatched nesting)
|
|
780
|
+
validateTagStructure(sanitized, errors);
|
|
781
|
+
|
|
782
|
+
// 3. Validate bracket matching (< > mismatches)
|
|
783
|
+
validateBrackets(sanitized, errors);
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
valid: errors.filter(e => e.type === 'error').length === 0,
|
|
787
|
+
errors,
|
|
788
|
+
sanitizedCode: sanitized,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
#### CSS Validation
|
|
794
|
+
|
|
795
|
+
Validate property syntax and bracket matching in generated CSS:
|
|
796
|
+
|
|
797
|
+
```ts
|
|
798
|
+
function validateCSS(css: string): ValidationResult {
|
|
799
|
+
const errors: ValidationError[] = [];
|
|
800
|
+
let sanitized = css;
|
|
801
|
+
|
|
802
|
+
// 1. Remove JavaScript expressions (url("javascript:..."), expression())
|
|
803
|
+
sanitized = removeJSFromCSS(sanitized, errors);
|
|
804
|
+
|
|
805
|
+
// 2. Validate bracket matching ({ } balance)
|
|
806
|
+
validateCSSBrackets(sanitized, errors);
|
|
807
|
+
|
|
808
|
+
// 3. Validate property syntax (missing semicolons, missing colons)
|
|
809
|
+
validateCSSProperties(sanitized, errors);
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
valid: errors.filter(e => e.type === 'error').length === 0,
|
|
813
|
+
errors,
|
|
814
|
+
sanitizedCode: sanitized,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
#### JavaScript Removal for Security
|
|
820
|
+
|
|
821
|
+
Generated code should never contain executable JavaScript. Strip it at the validation layer:
|
|
822
|
+
|
|
823
|
+
```ts
|
|
824
|
+
function removeJavaScript(html: string, errors: ValidationError[]): string {
|
|
825
|
+
let result = html;
|
|
826
|
+
|
|
827
|
+
// Remove <script> tags and content
|
|
828
|
+
const scripts = html.match(/<script\b[^>]*>[\s\S]*?<\/script>/gi);
|
|
829
|
+
if (scripts) {
|
|
830
|
+
errors.push({
|
|
831
|
+
line: 0, column: 0,
|
|
832
|
+
message: `Removed ${scripts.length} script tag(s) for security`,
|
|
833
|
+
type: 'warning',
|
|
834
|
+
});
|
|
835
|
+
result = result.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Remove inline event handlers (onclick, onerror, onload, etc.)
|
|
839
|
+
const handlers = html.match(/\s(on\w+)=["'][^"']*["']/gi);
|
|
840
|
+
if (handlers) {
|
|
841
|
+
errors.push({
|
|
842
|
+
line: 0, column: 0,
|
|
843
|
+
message: `Removed ${handlers.length} inline event handler(s)`,
|
|
844
|
+
type: 'warning',
|
|
845
|
+
});
|
|
846
|
+
result = result.replace(/\s(on\w+)=["'][^"']*["']/gi, '');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function removeJSFromCSS(css: string, errors: ValidationError[]): string {
|
|
853
|
+
// Remove expression(), url("javascript:..."), etc.
|
|
854
|
+
return css
|
|
855
|
+
.replace(/expression\s*\([^)]*\)/gi, '/* removed */')
|
|
856
|
+
.replace(/url\s*\(\s*["']?javascript:[^)]*\)/gi, '/* removed */');
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
#### Code Formatting and Indentation
|
|
861
|
+
|
|
862
|
+
Consistent formatting makes generated code feel professional:
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
function renderHTML(element: GeneratedElement, indent: number = 0): string {
|
|
866
|
+
const spaces = ' '.repeat(indent);
|
|
867
|
+
const tag = element.tag;
|
|
868
|
+
let attrs = `class="${element.className}"`;
|
|
869
|
+
|
|
870
|
+
// Add data-figma-id for traceability
|
|
871
|
+
if (element.figmaId) {
|
|
872
|
+
attrs += ` data-figma-id="${element.figmaId}"`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Self-closing tags
|
|
876
|
+
if (tag === 'img') {
|
|
877
|
+
return `${spaces}<${tag} ${attrs} src="${element.attributes?.src || ''}" alt="${element.attributes?.alt || ''}" />`;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Opening tag
|
|
881
|
+
let html = `${spaces}<${tag} ${attrs}>`;
|
|
882
|
+
|
|
883
|
+
// Children
|
|
884
|
+
const hasElementChildren = element.children.some(c => typeof c !== 'string');
|
|
885
|
+
if (hasElementChildren) {
|
|
886
|
+
html += '\n';
|
|
887
|
+
for (const child of element.children) {
|
|
888
|
+
html += typeof child === 'string'
|
|
889
|
+
? `${spaces} ${child}\n`
|
|
890
|
+
: renderHTML(child, indent + 1) + '\n';
|
|
891
|
+
}
|
|
892
|
+
html += `${spaces}</${tag}>`;
|
|
893
|
+
} else {
|
|
894
|
+
// Text-only content inline
|
|
895
|
+
html += element.children.join('') + `</${tag}>`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return html;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function renderCSS(rules: CSSRule[]): string {
|
|
902
|
+
return rules.map(rule => {
|
|
903
|
+
const props = Object.entries(rule.properties)
|
|
904
|
+
.filter(([_, v]) => v !== undefined)
|
|
905
|
+
.map(([key, value]) => {
|
|
906
|
+
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
907
|
+
return ` ${cssKey}: ${value};`;
|
|
908
|
+
})
|
|
909
|
+
.join('\n');
|
|
910
|
+
return `${rule.selector} {\n${props}\n}`;
|
|
911
|
+
}).join('\n\n');
|
|
912
|
+
}
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
#### Node-to-Code Traceability
|
|
916
|
+
|
|
917
|
+
Include `data-figma-id` attributes on generated HTML elements to enable bidirectional references between code and design:
|
|
918
|
+
|
|
919
|
+
```html
|
|
920
|
+
<section class="hero" data-figma-id="1:234">
|
|
921
|
+
<h1 class="hero__title" data-figma-id="1:235">Welcome</h1>
|
|
922
|
+
<p class="hero__description" data-figma-id="1:236">Lorem ipsum</p>
|
|
923
|
+
</section>
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
This enables:
|
|
927
|
+
- Click an element in the code preview → highlight the source Figma node
|
|
928
|
+
- Edit text in code → sync back to the Figma text node
|
|
929
|
+
- Show which Figma layer produced each line of code
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
### Integration with Dev Resources
|
|
934
|
+
|
|
935
|
+
#### Creating Dev Resources from Codegen Plugins
|
|
936
|
+
|
|
937
|
+
After generating code, link it back to the Figma node as a Dev Resource visible to all team members:
|
|
938
|
+
|
|
939
|
+
```ts
|
|
940
|
+
// From the UI thread (using hidden iframe for fetch access)
|
|
941
|
+
async function createDevResource(
|
|
942
|
+
fileKey: string,
|
|
943
|
+
nodeId: string,
|
|
944
|
+
componentName: string,
|
|
945
|
+
repoUrl: string
|
|
946
|
+
) {
|
|
947
|
+
const response = await fetch('https://api.figma.com/v1/dev_resources', {
|
|
948
|
+
method: 'POST',
|
|
949
|
+
headers: {
|
|
950
|
+
'X-Figma-Token': 'YOUR_FIGMA_TOKEN',
|
|
951
|
+
'Content-Type': 'application/json',
|
|
952
|
+
},
|
|
953
|
+
body: JSON.stringify({
|
|
954
|
+
dev_resources: [{
|
|
955
|
+
name: `${componentName} (Source)`,
|
|
956
|
+
url: repoUrl,
|
|
957
|
+
file_key: fileKey,
|
|
958
|
+
node_id: nodeId,
|
|
959
|
+
}],
|
|
960
|
+
}),
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
return response.json();
|
|
964
|
+
}
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
> **Cross-reference:** See `figma-api-devmode.md` for the complete Dev Resources REST API reference (GET, POST, PUT, DELETE).
|
|
968
|
+
|
|
969
|
+
#### Linking Components to Repositories
|
|
970
|
+
|
|
971
|
+
Build a mapping table from Figma component keys to repository paths:
|
|
972
|
+
|
|
973
|
+
```ts
|
|
974
|
+
interface ComponentMapping {
|
|
975
|
+
componentKey: string;
|
|
976
|
+
repoUrl: string;
|
|
977
|
+
importPath: string;
|
|
978
|
+
componentName: string;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Store mappings in clientStorage via action preferences
|
|
982
|
+
const mappings = await figma.clientStorage.getAsync('componentMappings') || [];
|
|
983
|
+
|
|
984
|
+
// Use during generation to emit import statements
|
|
985
|
+
figma.codegen.on('generate', async ({ node }) => {
|
|
986
|
+
if (node.type === 'INSTANCE') {
|
|
987
|
+
const main = await node.getMainComponentAsync();
|
|
988
|
+
if (main) {
|
|
989
|
+
const mapping = mappings.find(m => m.componentKey === main.key);
|
|
990
|
+
if (mapping) {
|
|
991
|
+
return [{
|
|
992
|
+
title: 'Import',
|
|
993
|
+
language: 'TYPESCRIPT',
|
|
994
|
+
code: `import { ${mapping.componentName} } from '${mapping.importPath}';`,
|
|
995
|
+
}];
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// ... fall through to full generation
|
|
1000
|
+
});
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
### Design Token Integration in Codegen
|
|
1006
|
+
|
|
1007
|
+
#### Token-Aware CSS Generation
|
|
1008
|
+
|
|
1009
|
+
When generating CSS, substitute repeated values with CSS custom properties (design tokens):
|
|
1010
|
+
|
|
1011
|
+
```ts
|
|
1012
|
+
// Instead of hardcoding:
|
|
1013
|
+
// color: #0066ff;
|
|
1014
|
+
|
|
1015
|
+
// Generate with token reference:
|
|
1016
|
+
// color: var(--color-primary);
|
|
1017
|
+
|
|
1018
|
+
function generateColorValue(fill: FillData, lookup?: TokenLookup): string {
|
|
1019
|
+
if (fill.type !== 'SOLID') return '';
|
|
1020
|
+
|
|
1021
|
+
// Check for Figma variable binding first
|
|
1022
|
+
if (fill.variable) {
|
|
1023
|
+
const varName = figmaVariableToCssName(fill.variable.name);
|
|
1024
|
+
return fill.variable.isLocal
|
|
1025
|
+
? `var(${varName})`
|
|
1026
|
+
: `var(${varName}, ${fill.color})`;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Check token lookup for promoted values
|
|
1030
|
+
if (lookup) {
|
|
1031
|
+
const token = lookup.colors.get(fill.color);
|
|
1032
|
+
if (token) return `var(${token.cssVariable})`;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return fill.color;
|
|
1036
|
+
}
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
#### Generating tokens.css
|
|
1040
|
+
|
|
1041
|
+
When the codegen plugin promotes design tokens, output them as a separate CSS custom properties file:
|
|
1042
|
+
|
|
1043
|
+
```ts
|
|
1044
|
+
function generateTokensFile(tokens: DesignTokens): CodegenResult {
|
|
1045
|
+
const lines: string[] = [':root {'];
|
|
1046
|
+
|
|
1047
|
+
// Colors
|
|
1048
|
+
for (const color of tokens.colors) {
|
|
1049
|
+
lines.push(` ${color.cssVariable}: ${color.value};`);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Spacing
|
|
1053
|
+
for (const spacing of tokens.spacing) {
|
|
1054
|
+
lines.push(` ${spacing.cssVariable}: ${spacing.value};`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Typography
|
|
1058
|
+
for (const family of tokens.typography.families) {
|
|
1059
|
+
lines.push(` ${family.cssVariable}: ${family.value};`);
|
|
1060
|
+
}
|
|
1061
|
+
for (const size of tokens.typography.sizes) {
|
|
1062
|
+
lines.push(` ${size.cssVariable}: ${size.value};`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
lines.push('}');
|
|
1066
|
+
|
|
1067
|
+
return {
|
|
1068
|
+
title: 'tokens.css',
|
|
1069
|
+
code: lines.join('\n'),
|
|
1070
|
+
language: 'CSS',
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
> **Cross-reference:** See `design-tokens.md` for the full token promotion pipeline and naming conventions. See `design-tokens-variables.md` for Figma Variables → CSS custom property mapping.
|
|
1076
|
+
|
|
1077
|
+
---
|
|
1078
|
+
|
|
1079
|
+
### Standard vs Codegen Plugin Comparison
|
|
1080
|
+
|
|
1081
|
+
#### Decision Guide
|
|
1082
|
+
|
|
1083
|
+
| Consideration | Standard Plugin | Codegen Plugin |
|
|
1084
|
+
|--------------|:--------------:|:--------------:|
|
|
1085
|
+
| **Primary audience** | Designers | Developers |
|
|
1086
|
+
| **Running context** | Design Mode | Dev Mode |
|
|
1087
|
+
| **Document access** | Full read/write | Read-only |
|
|
1088
|
+
| **Output method** | Side effects (create nodes, export files) | Return `CodegenResult[]` |
|
|
1089
|
+
| **UI** | Custom iframe UI | Inspect panel + optional hidden UI |
|
|
1090
|
+
| **Timeout** | None (runs until closed) | 3 seconds per generate call |
|
|
1091
|
+
| **Trigger** | Plugins menu / command | Node selection in Dev Mode |
|
|
1092
|
+
| **Use case** | Export, import, automate, analyze | Generate code for developers |
|
|
1093
|
+
|
|
1094
|
+
**Choose a standard plugin when:**
|
|
1095
|
+
- You need to modify the Figma document (create nodes, update styles)
|
|
1096
|
+
- You need a rich interactive UI (settings panels, previews, editors)
|
|
1097
|
+
- You need to export files or create ZIP bundles
|
|
1098
|
+
- You need long-running operations (batch processing)
|
|
1099
|
+
- Your audience is primarily designers
|
|
1100
|
+
|
|
1101
|
+
**Choose a codegen plugin when:**
|
|
1102
|
+
- Your output is code that developers copy into their codebase
|
|
1103
|
+
- You want code to appear directly in the Inspect panel
|
|
1104
|
+
- You need per-node code generation triggered by selection
|
|
1105
|
+
- Your audience is developers using Dev Mode
|
|
1106
|
+
- You want language-specific output with preference controls
|
|
1107
|
+
|
|
1108
|
+
#### Combining Both in One Project
|
|
1109
|
+
|
|
1110
|
+
A single project can provide both a standard plugin and a codegen plugin by publishing two separate plugins that share code:
|
|
1111
|
+
|
|
1112
|
+
```
|
|
1113
|
+
src/
|
|
1114
|
+
├── shared/
|
|
1115
|
+
│ ├── extraction/ # Shared extraction logic
|
|
1116
|
+
│ ├── generation/ # Shared code generation
|
|
1117
|
+
│ └── types/ # Shared type definitions
|
|
1118
|
+
├── standard-plugin/
|
|
1119
|
+
│ ├── main.ts # Standard plugin entry
|
|
1120
|
+
│ └── ui.tsx # Standard plugin UI
|
|
1121
|
+
└── codegen-plugin/
|
|
1122
|
+
└── main.ts # Codegen plugin entry (no UI)
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
The standard plugin handles rich workflows (multi-frame export, live preview, bidirectional sync), while the codegen plugin provides quick per-node code snippets in Dev Mode. Both share the same extraction and generation logic.
|
|
1126
|
+
|
|
1127
|
+
```json
|
|
1128
|
+
// Standard plugin package.json
|
|
1129
|
+
{
|
|
1130
|
+
"figma-plugin": {
|
|
1131
|
+
"editorType": ["figma"],
|
|
1132
|
+
"main": "src/standard-plugin/main.ts",
|
|
1133
|
+
"ui": "src/standard-plugin/ui.tsx"
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Codegen plugin package.json (separate build)
|
|
1138
|
+
{
|
|
1139
|
+
"figma-plugin": {
|
|
1140
|
+
"editorType": ["dev"],
|
|
1141
|
+
"capabilities": ["codegen"],
|
|
1142
|
+
"main": "src/codegen-plugin/main.ts"
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
> **Note:** These must be separate Figma plugins with separate IDs. A single plugin cannot be both `editorType: ["figma"]` and `editorType: ["dev"]`. However, they can live in the same repository and share source code.
|
|
1148
|
+
|
|
1149
|
+
---
|
|
1150
|
+
|
|
1151
|
+
### Using Hidden iframe for Complex Processing
|
|
1152
|
+
|
|
1153
|
+
Codegen plugins can use a hidden iframe for operations that require browser APIs (fetch, WebAssembly, canvas):
|
|
1154
|
+
|
|
1155
|
+
```ts
|
|
1156
|
+
let nextMessageIdx = 1;
|
|
1157
|
+
const resolvers: Record<number, (result: CodegenResult[]) => void> = {};
|
|
1158
|
+
|
|
1159
|
+
// Show hidden UI at startup
|
|
1160
|
+
figma.showUI('<script>/* processing code */</script>', { visible: false });
|
|
1161
|
+
|
|
1162
|
+
figma.ui.on('message', (msg) => {
|
|
1163
|
+
if (msg.type === 'CODEGEN_RESULT' && resolvers[msg.messageIdx]) {
|
|
1164
|
+
resolvers[msg.messageIdx](msg.results);
|
|
1165
|
+
delete resolvers[msg.messageIdx];
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
figma.codegen.on('generate', async ({ node, language }) => {
|
|
1170
|
+
const idx = nextMessageIdx++;
|
|
1171
|
+
|
|
1172
|
+
return new Promise<CodegenResult[]>((resolve) => {
|
|
1173
|
+
resolvers[idx] = resolve;
|
|
1174
|
+
figma.ui.postMessage({
|
|
1175
|
+
type: 'GENERATE',
|
|
1176
|
+
messageIdx: idx,
|
|
1177
|
+
nodeData: serializeNode(node),
|
|
1178
|
+
language,
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
});
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
Common use cases for hidden iframe in codegen:
|
|
1185
|
+
- **Network requests** — Fetch component mappings from a design system server
|
|
1186
|
+
- **Heavy computation** — Run Prettier/formatting that exceeds main thread budget
|
|
1187
|
+
- **WebAssembly** — Use WASM-based tools for code generation
|
|
1188
|
+
- **Template engines** — Run Handlebars, EJS, or other template engines
|
|
1189
|
+
|
|
1190
|
+
> **Warning:** The generate callback still has a 3-second timeout even when delegating to a hidden iframe. Ensure the iframe responds quickly.
|
|
1191
|
+
|
|
1192
|
+
---
|
|
1193
|
+
|
|
1194
|
+
### Performance Optimization for Codegen
|
|
1195
|
+
|
|
1196
|
+
#### Caching Strategies
|
|
1197
|
+
|
|
1198
|
+
```ts
|
|
1199
|
+
// Cache node data between generate calls (nodes don't change in Dev Mode)
|
|
1200
|
+
const nodeCache = new Map<string, ExtractedNode>();
|
|
1201
|
+
|
|
1202
|
+
figma.codegen.on('generate', ({ node }) => {
|
|
1203
|
+
let extracted = nodeCache.get(node.id);
|
|
1204
|
+
if (!extracted) {
|
|
1205
|
+
extracted = extractNode(node);
|
|
1206
|
+
nodeCache.set(node.id, extracted);
|
|
1207
|
+
}
|
|
1208
|
+
return generateCode(extracted);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// Clear cache on page change
|
|
1212
|
+
figma.on('currentpagechange', () => {
|
|
1213
|
+
nodeCache.clear();
|
|
1214
|
+
});
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
#### Limiting Traversal Depth
|
|
1218
|
+
|
|
1219
|
+
For complex nested designs, limit how deep the generator traverses:
|
|
1220
|
+
|
|
1221
|
+
```ts
|
|
1222
|
+
function extractNode(node: SceneNode, depth: number = 0, maxDepth: number = 15): ExtractedNode {
|
|
1223
|
+
const extracted = { /* ... extract properties ... */ };
|
|
1224
|
+
|
|
1225
|
+
if ('children' in node && depth < maxDepth) {
|
|
1226
|
+
extracted.children = node.children
|
|
1227
|
+
.filter(child => child.visible)
|
|
1228
|
+
.map(child => extractNode(child, depth + 1, maxDepth));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return extracted;
|
|
1232
|
+
}
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
#### Avoiding Expensive Operations
|
|
1236
|
+
|
|
1237
|
+
Operations to avoid in the generate callback:
|
|
1238
|
+
|
|
1239
|
+
| Operation | Cost | Alternative |
|
|
1240
|
+
|-----------|:----:|-------------|
|
|
1241
|
+
| `page.findAll()` | High | Only traverse the selected node's subtree |
|
|
1242
|
+
| `node.exportAsync()` | High | Skip asset export in codegen (return asset references instead) |
|
|
1243
|
+
| `figma.loadFontAsync()` | Medium | Only load if needed, cache loaded fonts |
|
|
1244
|
+
| `figma.getNodeByIdAsync()` | Medium | Use the node directly from the event |
|
|
1245
|
+
| Network requests | Variable | Pre-fetch via action preferences |
|
|
1246
|
+
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
### Error Handling in Codegen Plugins
|
|
1250
|
+
|
|
1251
|
+
#### Graceful Degradation
|
|
1252
|
+
|
|
1253
|
+
Never let the generate callback throw an unhandled error. Always return a meaningful result:
|
|
1254
|
+
|
|
1255
|
+
```ts
|
|
1256
|
+
figma.codegen.on('generate', async ({ node, language }) => {
|
|
1257
|
+
try {
|
|
1258
|
+
return generateCode(node, language);
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1261
|
+
return [{
|
|
1262
|
+
title: 'Generation Error',
|
|
1263
|
+
language: 'PLAINTEXT',
|
|
1264
|
+
code: [
|
|
1265
|
+
`// Code generation failed for node "${node.name}" (${node.type})`,
|
|
1266
|
+
`// Error: ${message}`,
|
|
1267
|
+
`//`,
|
|
1268
|
+
`// Possible causes:`,
|
|
1269
|
+
`// - Node type not yet supported`,
|
|
1270
|
+
`// - Complex nested structure exceeded depth limit`,
|
|
1271
|
+
`// - Missing font data`,
|
|
1272
|
+
].join('\n'),
|
|
1273
|
+
}];
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
#### Unsupported Node Types
|
|
1279
|
+
|
|
1280
|
+
Handle node types your generator does not support with informative messages:
|
|
1281
|
+
|
|
1282
|
+
```ts
|
|
1283
|
+
function generateCode(node: SceneNode, language: string): CodegenResult[] {
|
|
1284
|
+
const supportedTypes = ['FRAME', 'COMPONENT', 'INSTANCE', 'TEXT', 'RECTANGLE', 'ELLIPSE', 'GROUP'];
|
|
1285
|
+
|
|
1286
|
+
if (!supportedTypes.includes(node.type)) {
|
|
1287
|
+
return [{
|
|
1288
|
+
title: 'Info',
|
|
1289
|
+
language: 'PLAINTEXT',
|
|
1290
|
+
code: `// Node type "${node.type}" is not supported for code generation.\n// Select a frame, component, or text node.`,
|
|
1291
|
+
}];
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// ... proceed with generation
|
|
1295
|
+
}
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
---
|
|
1299
|
+
|
|
1300
|
+
## Cross-References
|
|
1301
|
+
|
|
1302
|
+
- **`figma-api-devmode.md`** — Dev Mode codegen API reference (CodegenEvent, CodegenResult, preferences types, Dev Resources REST API). This module builds on that reference with development patterns.
|
|
1303
|
+
- **`figma-api-plugin.md`** — Standard plugin API reference (sandbox model, SceneNode types, IPC messaging). Codegen plugins share the same sandbox model.
|
|
1304
|
+
- **`plugin-architecture.md`** — Production plugin architecture patterns (project setup, IPC design, data flow pipeline). The extraction/generation pipeline applies to both standard and codegen plugins.
|
|
1305
|
+
- **`design-to-code-layout.md`** — Auto Layout to Flexbox mapping rules used in CSS generation.
|
|
1306
|
+
- **`design-to-code-visual.md`** — Visual property mapping rules (fills, strokes, effects) used in CSS generation.
|
|
1307
|
+
- **`design-to-code-typography.md`** — Typography mapping rules used in CSS generation.
|
|
1308
|
+
- **`design-to-code-semantic.md`** — Semantic HTML tag selection and ARIA attribute patterns.
|
|
1309
|
+
- **`design-to-code-assets.md`** — Asset detection, vector container identification, SVG export patterns.
|
|
1310
|
+
- **`css-strategy.md`** — Three-layer CSS architecture (Tailwind + Custom Properties + CSS Modules) for organizing generated CSS output.
|
|
1311
|
+
- **`design-tokens.md`** — Token promotion pipeline and CSS variable naming conventions.
|
|
1312
|
+
- **`design-tokens-variables.md`** — Figma Variables to CSS custom property mapping for token-aware code generation.
|
|
1313
|
+
- **`plugin-best-practices.md`** — Production best practices for error handling, performance, caching, async patterns, and testing. The caching strategies and error handling patterns apply directly to codegen plugins operating under the 3-second timeout constraint.
|