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.
Files changed (34) hide show
  1. package/README.md +133 -0
  2. package/bin/install.js +328 -0
  3. package/knowledge/README.md +62 -0
  4. package/knowledge/css-strategy.md +973 -0
  5. package/knowledge/design-to-code-assets.md +855 -0
  6. package/knowledge/design-to-code-layout.md +929 -0
  7. package/knowledge/design-to-code-semantic.md +1085 -0
  8. package/knowledge/design-to-code-typography.md +1003 -0
  9. package/knowledge/design-to-code-visual.md +1145 -0
  10. package/knowledge/design-tokens-variables.md +1261 -0
  11. package/knowledge/design-tokens.md +960 -0
  12. package/knowledge/figma-api-devmode.md +894 -0
  13. package/knowledge/figma-api-plugin.md +920 -0
  14. package/knowledge/figma-api-rest.md +742 -0
  15. package/knowledge/figma-api-variables.md +848 -0
  16. package/knowledge/figma-api-webhooks.md +876 -0
  17. package/knowledge/payload-blocks.md +1184 -0
  18. package/knowledge/payload-figma-mapping.md +1210 -0
  19. package/knowledge/payload-visual-builder.md +1004 -0
  20. package/knowledge/plugin-architecture.md +1176 -0
  21. package/knowledge/plugin-best-practices.md +1206 -0
  22. package/knowledge/plugin-codegen.md +1313 -0
  23. package/package.json +31 -0
  24. package/skills/README.md +103 -0
  25. package/skills/audit-plugin/SKILL.md +244 -0
  26. package/skills/build-codegen-plugin/SKILL.md +279 -0
  27. package/skills/build-importer/SKILL.md +320 -0
  28. package/skills/build-plugin/SKILL.md +199 -0
  29. package/skills/build-token-pipeline/SKILL.md +363 -0
  30. package/skills/ref-html/SKILL.md +290 -0
  31. package/skills/ref-layout/SKILL.md +150 -0
  32. package/skills/ref-payload-block/SKILL.md +415 -0
  33. package/skills/ref-react/SKILL.md +222 -0
  34. 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.