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,1176 @@
1
+ # Figma Plugin Architecture Patterns
2
+
3
+ ## Purpose
4
+
5
+ Production-tested patterns for architecting Figma plugins — covering project setup with `@create-figma-plugin`, project structure by concern, type-safe IPC messaging, manifest configuration, UI architecture, and the extraction-generation-export data flow pipeline. This module documents **how to build** production plugins, not the Plugin API itself (see `figma-api-plugin.md` for API reference).
6
+
7
+ ## When to Use
8
+
9
+ Reference this module when you need to:
10
+
11
+ - Set up a new Figma plugin project with `@create-figma-plugin`
12
+ - Structure a plugin codebase by domain concern (extraction, generation, export)
13
+ - Design a type-safe IPC event system between main thread and UI
14
+ - Configure `manifest.json` and `package.json` for `@create-figma-plugin`
15
+ - Build a plugin UI with Preact and `@create-figma-plugin/ui`
16
+ - Implement the extraction → generation → export data flow pipeline
17
+ - Handle errors, progress reporting, and large data transfer across IPC
18
+
19
+ ---
20
+
21
+ ## Content
22
+
23
+ ### Project Setup with @create-figma-plugin
24
+
25
+ The `@create-figma-plugin` toolkit provides a modern build system, TypeScript support, Preact-based UI components, and automatic manifest generation. It is the recommended toolchain for production Figma plugins.
26
+
27
+ #### Scaffolding a New Project
28
+
29
+ ```bash
30
+ # Create a new plugin project
31
+ npx create-figma-plugin
32
+
33
+ # Or initialize manually in an existing directory
34
+ npm init -y
35
+ npm install @create-figma-plugin/ui @create-figma-plugin/utilities preact
36
+ npm install -D @create-figma-plugin/build @create-figma-plugin/tsconfig \
37
+ @figma/plugin-typings typescript
38
+ ```
39
+
40
+ #### package.json Configuration
41
+
42
+ The `@create-figma-plugin` build system reads plugin configuration from the `figma-plugin` section of `package.json`. This replaces manually maintaining `manifest.json` — the build tool generates it automatically.
43
+
44
+ ```json
45
+ {
46
+ "name": "my-figma-plugin",
47
+ "version": "1.0.0",
48
+ "dependencies": {
49
+ "@create-figma-plugin/ui": "^4.0.3",
50
+ "@create-figma-plugin/utilities": "^4.0.3",
51
+ "preact": ">=10"
52
+ },
53
+ "devDependencies": {
54
+ "@create-figma-plugin/build": "^4.0.3",
55
+ "@create-figma-plugin/tsconfig": "^4.0.3",
56
+ "@figma/plugin-typings": "1.109.0",
57
+ "typescript": ">=5"
58
+ },
59
+ "scripts": {
60
+ "build": "build-figma-plugin --typecheck --minify",
61
+ "watch": "build-figma-plugin --typecheck --watch"
62
+ },
63
+ "figma-plugin": {
64
+ "name": "My Plugin",
65
+ "id": "YOUR_PLUGIN_ID",
66
+ "editorType": ["figma"],
67
+ "main": "src/main.ts",
68
+ "ui": "src/ui.tsx",
69
+ "documentAccess": "dynamic-page",
70
+ "networkAccess": {
71
+ "allowedDomains": [
72
+ "https://fonts.googleapis.com",
73
+ "https://fonts.gstatic.com"
74
+ ]
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Key fields in `figma-plugin`:
81
+
82
+ | Field | Purpose | Example |
83
+ |-------|---------|---------|
84
+ | `name` | Plugin display name in Figma | `"My Plugin"` |
85
+ | `id` | Figma-assigned plugin ID (empty during development) | `"1234567890"` |
86
+ | `editorType` | Target editor(s) | `["figma"]`, `["dev"]` |
87
+ | `main` | Main thread entry point (TypeScript source) | `"src/main.ts"` |
88
+ | `ui` | UI entry point (TSX source) | `"src/ui.tsx"` |
89
+ | `documentAccess` | Page loading strategy | `"dynamic-page"` |
90
+ | `networkAccess` | Allowed domains for UI fetch | `{ "allowedDomains": [...] }` |
91
+
92
+ > **Important:** The build tool compiles TypeScript to JavaScript and generates `manifest.json` automatically from `figma-plugin` config. You do not need to maintain `manifest.json` manually. The output goes to `build/main.js` and `build/ui.js`.
93
+
94
+ #### Build Scripts
95
+
96
+ ```json
97
+ {
98
+ "scripts": {
99
+ "build": "build-figma-plugin --typecheck --minify",
100
+ "watch": "build-figma-plugin --typecheck --watch",
101
+ "test": "vitest run",
102
+ "test:watch": "vitest",
103
+ "test:coverage": "vitest run --coverage"
104
+ }
105
+ }
106
+ ```
107
+
108
+ - `--typecheck` — Run TypeScript type checking before build
109
+ - `--minify` — Minify output for production (important for plugin size limits)
110
+ - `--watch` — Rebuild on file changes during development
111
+
112
+ #### TypeScript Configuration
113
+
114
+ Extend the `@create-figma-plugin/tsconfig` base configuration:
115
+
116
+ ```json
117
+ {
118
+ "extends": "@create-figma-plugin/tsconfig/tsconfig.json",
119
+ "compilerOptions": {
120
+ "typeRoots": [
121
+ "node_modules/@figma",
122
+ "node_modules/@types"
123
+ ]
124
+ },
125
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
126
+ }
127
+ ```
128
+
129
+ The base config includes `"jsx": "react"` with `"jsxFactory": "h"` for Preact compatibility and strict TypeScript settings.
130
+
131
+ ---
132
+
133
+ ### Project Structure Patterns
134
+
135
+ Organize plugin code by domain concern. This separation keeps modules focused, testable, and independently maintainable. The structure reflects the natural data flow: extraction → generation → export.
136
+
137
+ ```
138
+ src/
139
+ ├── main.ts # Plugin backend (Figma sandbox)
140
+ ├── ui.tsx # UI entry point (Preact)
141
+ ├── types/
142
+ │ ├── events.ts # IPC event definitions
143
+ │ ├── extracted.ts # Extracted data schema
144
+ │ ├── generated.ts # Generated output types
145
+ │ ├── export.ts # Export bundle types
146
+ │ └── figma.ts # Figma-specific type helpers
147
+ ├── extraction/
148
+ │ ├── traverse.ts # Tree traversal and node extraction
149
+ │ ├── layout.ts # Auto Layout property extraction
150
+ │ ├── visual.ts # Fill, stroke, effect extraction
151
+ │ ├── text.ts # Typography extraction
152
+ │ ├── assets.ts # Asset detection and classification
153
+ │ └── variants.ts # Component variant resolution
154
+ ├── generation/
155
+ │ ├── render.ts # HTML/CSS rendering orchestrator
156
+ │ ├── html.ts # Element tree generation
157
+ │ ├── layout.ts # Layout CSS generation
158
+ │ ├── visual.ts # Visual CSS generation
159
+ │ ├── typography.ts # Typography CSS generation
160
+ │ ├── semantic.ts # Semantic HTML tag selection
161
+ │ ├── classname.ts # BEM class name generation
162
+ │ ├── responsive.ts # Responsive/breakpoint CSS
163
+ │ └── position.ts # Positioning logic
164
+ ├── tokens/
165
+ │ ├── promote.ts # Token promotion (threshold-based)
166
+ │ ├── render.ts # Token CSS variable rendering
167
+ │ ├── lookup.ts # Token lookup table
168
+ │ └── types.ts # Token type definitions
169
+ ├── export/
170
+ │ ├── prepare.ts # Export bundle preparation (main thread)
171
+ │ ├── bundle.ts # Multi-frame bundle assembly
172
+ │ ├── assets.ts # Asset export (images, SVGs)
173
+ │ ├── css.ts # CSS file assembly
174
+ │ ├── html.ts # HTML document assembly
175
+ │ ├── scss.ts # SCSS file assembly
176
+ │ ├── units.ts # Unit conversion (px → rem)
177
+ │ └── zip.ts # ZIP creation (UI thread only)
178
+ ├── components/
179
+ │ ├── LivePreview.tsx # HTML preview in iframe
180
+ │ ├── CodeEditor.tsx # CodeMirror code editor
181
+ │ ├── FileTabs.tsx # File tab navigation
182
+ │ ├── SplitPane.tsx # Resizable split view
183
+ │ ├── FrameSelector.tsx # Frame selection grid
184
+ │ ├── ExportSettings.tsx # Export configuration panel
185
+ │ ├── ExportDrawer.tsx # Export progress drawer
186
+ │ ├── ErrorState.tsx # Error display component
187
+ │ └── ElementSidebar.tsx # Node-to-element mapping UI
188
+ ├── hooks/
189
+ │ ├── useHistory.ts # Undo/redo state management
190
+ │ └── useResize.ts # Window resize handling
191
+ └── utils/
192
+ └── codeValidator.ts # HTML/CSS validation
193
+ ```
194
+
195
+ #### Why This Structure
196
+
197
+ | Directory | Responsibility | IPC Boundary |
198
+ |-----------|---------------|:------------:|
199
+ | `types/` | Shared type definitions used on both sides | Crosses IPC |
200
+ | `extraction/` | Read Figma nodes → JSON-serializable data | Main thread only |
201
+ | `generation/` | Transform extracted data → element tree + CSS | Either side |
202
+ | `tokens/` | Design token promotion and rendering | Either side |
203
+ | `export/` | Bundle assembly, asset export, ZIP creation | Split across boundary |
204
+ | `components/` | Preact UI components | UI thread only |
205
+ | `hooks/` | UI state management hooks | UI thread only |
206
+ | `utils/` | Shared utilities (validation, helpers) | Either side |
207
+
208
+ > **Critical:** Code in `extraction/` can only run on the main thread because it accesses the Figma API (`figma.getNodeByIdAsync`, `node.exportAsync`). Code in `components/` and `hooks/` can only run in the UI iframe. Code in `types/`, `generation/`, and `tokens/` is pure data transformation and can run on either side.
209
+
210
+ ---
211
+
212
+ ### IPC Architecture
213
+
214
+ Communication between the main thread (sandbox) and UI (iframe) is the backbone of any non-trivial Figma plugin. A type-safe event system prevents mismatched payloads and makes the codebase self-documenting.
215
+
216
+ #### Event System with @create-figma-plugin/utilities
217
+
218
+ The `emit`/`on` helpers from `@create-figma-plugin/utilities` abstract raw `postMessage`/`onmessage` into a typed event system:
219
+
220
+ ```ts
221
+ // Main thread (src/main.ts)
222
+ import { showUI, on, emit } from '@create-figma-plugin/utilities';
223
+
224
+ export default function () {
225
+ showUI({ width: 520, height: 900 });
226
+
227
+ on<ExtractFrameHandler>(EVENTS.EXTRACT_FRAME, async (nodeId: string) => {
228
+ const extracted = await extractNodeTree(node);
229
+ emit<FrameExtractedHandler>(EVENTS.FRAME_EXTRACTED, extracted);
230
+ });
231
+ }
232
+ ```
233
+
234
+ ```tsx
235
+ // UI thread (src/ui.tsx)
236
+ import { emit, on } from '@create-figma-plugin/utilities';
237
+
238
+ function Plugin() {
239
+ useEffect(() => {
240
+ on<FrameExtractedHandler>(EVENTS.FRAME_EXTRACTED, (data) => {
241
+ setExtractedData(data);
242
+ });
243
+ }, []);
244
+
245
+ function handleExtract(nodeId: string) {
246
+ emit<ExtractFrameHandler>(EVENTS.EXTRACT_FRAME, nodeId);
247
+ }
248
+ }
249
+ ```
250
+
251
+ #### Type-Safe Event Handler Definitions
252
+
253
+ Define event handler interfaces using `EventHandler` from `@create-figma-plugin/utilities`. This gives compile-time type checking for both emit and on calls:
254
+
255
+ ```ts
256
+ import { EventHandler } from '@create-figma-plugin/utilities';
257
+
258
+ // Event name constants (centralized, typo-proof)
259
+ export const EVENTS = {
260
+ CLOSE: 'CLOSE',
261
+ GET_SELECTION: 'GET_SELECTION',
262
+ SELECTION_DATA: 'SELECTION_DATA',
263
+ SELECTION_CHANGED: 'SELECTION_CHANGED',
264
+ EXTRACT_FRAME: 'EXTRACT_FRAME',
265
+ FRAME_EXTRACTED: 'FRAME_EXTRACTED',
266
+ GENERATE_CODE: 'GENERATE_CODE',
267
+ CODE_GENERATED: 'CODE_GENERATED',
268
+ GENERATE_FILES: 'GENERATE_FILES',
269
+ FILES_GENERATED: 'FILES_GENERATED',
270
+ EXPORT_FRAME: 'EXPORT_FRAME',
271
+ EXPORT_READY: 'EXPORT_READY',
272
+ ERROR: 'ERROR',
273
+ EXTRACTION_PROGRESS: 'EXTRACTION_PROGRESS',
274
+ } as const;
275
+
276
+ // Handler type for each event
277
+ export interface ExtractFrameHandler extends EventHandler {
278
+ name: 'EXTRACT_FRAME';
279
+ handler: (nodeId: string) => void;
280
+ }
281
+
282
+ export interface FrameExtractedHandler extends EventHandler {
283
+ name: 'FRAME_EXTRACTED';
284
+ handler: (data: ExtractedNode) => void;
285
+ }
286
+
287
+ export interface ErrorHandler extends EventHandler {
288
+ name: 'ERROR';
289
+ handler: (error: ErrorData) => void;
290
+ }
291
+ ```
292
+
293
+ #### Event Naming Conventions
294
+
295
+ Use `VERB_NOUN` for requests and `NOUN_VERBED` for responses. This makes it immediately clear which direction data flows:
296
+
297
+ | Request Event | Response Event | Direction |
298
+ |--------------|---------------|-----------|
299
+ | `EXTRACT_FRAME` | `FRAME_EXTRACTED` | UI → Main → UI |
300
+ | `GENERATE_CODE` | `CODE_GENERATED` | UI → Main → UI |
301
+ | `GENERATE_FILES` | `FILES_GENERATED` | UI → Main → UI |
302
+ | `EXPORT_FRAME` | `EXPORT_READY` | UI → Main → UI |
303
+ | `GET_SELECTION` | `SELECTION_DATA` | UI → Main → UI |
304
+ | `GET_THUMBNAILS` | `THUMBNAILS_READY` | UI → Main → UI |
305
+ | `SAVE_SETTINGS` | (fire-and-forget) | UI → Main |
306
+ | `LOAD_SETTINGS` | `SETTINGS_LOADED` | UI → Main → UI |
307
+
308
+ Additional push events (main → UI, no request):
309
+
310
+ | Event | Trigger |
311
+ |-------|---------|
312
+ | `SELECTION_CHANGED` | User changes selection in Figma |
313
+ | `EXTRACTION_PROGRESS` | Long extraction operation progress |
314
+ | `ERROR` | Any error during processing |
315
+
316
+ #### Progress Reporting Pattern
317
+
318
+ For long-running operations (large frame extraction, multi-frame export), report progress through dedicated events:
319
+
320
+ ```ts
321
+ // Main thread
322
+ on<ExtractFrameHandler>(EVENTS.EXTRACT_FRAME, async (nodeId: string) => {
323
+ const extracted = await extractNodeTree(node, {
324
+ onProgress: (current, total) => {
325
+ emit<ExtractionProgressHandler>(EVENTS.EXTRACTION_PROGRESS, {
326
+ current,
327
+ total,
328
+ phase: 'extracting',
329
+ });
330
+ },
331
+ });
332
+ emit<FrameExtractedHandler>(EVENTS.FRAME_EXTRACTED, extracted);
333
+ });
334
+ ```
335
+
336
+ ```ts
337
+ // Progress data structure
338
+ export interface ExtractionProgress {
339
+ current: number;
340
+ total: number;
341
+ phase: 'extracting' | 'generating' | 'exporting';
342
+ }
343
+ ```
344
+
345
+ #### Structured Error Propagation
346
+
347
+ Never let errors silently fail across the IPC boundary. Define error codes and propagate structured errors:
348
+
349
+ ```ts
350
+ // Error code constants
351
+ export const ERROR_CODES = {
352
+ NO_SELECTION: 'NO_SELECTION',
353
+ INVALID_NODE: 'INVALID_NODE',
354
+ EXTRACTION_FAILED: 'EXTRACTION_FAILED',
355
+ GENERATION_FAILED: 'GENERATION_FAILED',
356
+ EXPORT_FAILED: 'EXPORT_FAILED',
357
+ } as const;
358
+
359
+ // Structured error data
360
+ export interface ErrorData {
361
+ code: ErrorCode;
362
+ message: string; // User-facing message
363
+ details?: string; // Technical details (from caught exception)
364
+ }
365
+
366
+ // Helper function for consistent error emission
367
+ function emitError(code: ErrorData['code'], message: string, error?: unknown): void {
368
+ const details = error instanceof Error
369
+ ? error.message
370
+ : error ? String(error) : undefined;
371
+ console.error(`[Error] ${code}: ${message}`, details || '');
372
+ emit<ErrorHandler>(EVENTS.ERROR, { code, message, details });
373
+ }
374
+ ```
375
+
376
+ Usage in every handler:
377
+
378
+ ```ts
379
+ on<ExtractFrameHandler>(EVENTS.EXTRACT_FRAME, async (nodeId: string) => {
380
+ try {
381
+ const node = await figma.getNodeByIdAsync(nodeId);
382
+ if (!node) {
383
+ emitError(ERROR_CODES.INVALID_NODE, 'Node not found. It may have been deleted.');
384
+ return;
385
+ }
386
+ if (!('children' in node)) {
387
+ emitError(ERROR_CODES.INVALID_NODE, 'Selected node is not a frame or group.');
388
+ return;
389
+ }
390
+
391
+ const extracted = await extractNodeTree(node as SceneNode);
392
+ emit<FrameExtractedHandler>(EVENTS.FRAME_EXTRACTED, extracted);
393
+ } catch (error) {
394
+ emitError(ERROR_CODES.EXTRACTION_FAILED, 'Failed to extract frame data.', error);
395
+ }
396
+ });
397
+ ```
398
+
399
+ #### Binary Data Transfer Across IPC
400
+
401
+ The IPC boundary only supports JSON-serializable data. Binary data (images, exported assets) must be converted to base64 strings for transfer:
402
+
403
+ ```ts
404
+ // Main thread: Convert Uint8Array to base64 for message passing
405
+ function uint8ArrayToBase64(bytes: Uint8Array): string {
406
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
407
+ const len = bytes.length;
408
+ const result = new Array(Math.ceil(len / 3) * 4);
409
+ let ri = 0;
410
+
411
+ for (let i = 0; i < len; i += 3) {
412
+ const a = bytes[i];
413
+ const b = bytes[i + 1];
414
+ const c = bytes[i + 2];
415
+ result[ri++] = chars[a >> 2];
416
+ result[ri++] = chars[((a & 3) << 4) | ((b ?? 0) >> 4)];
417
+ result[ri++] = b !== undefined ? chars[((b & 15) << 2) | ((c ?? 0) >> 6)] : '=';
418
+ result[ri++] = c !== undefined ? chars[c & 63] : '=';
419
+ }
420
+
421
+ return result.join('');
422
+ }
423
+ ```
424
+
425
+ > **Why manual base64?** The main thread sandbox does not have `btoa()` or `Buffer`. You must implement base64 encoding manually. The UI iframe has full browser APIs and can decode base64 normally.
426
+
427
+ ---
428
+
429
+ ### Plugin Manifest Configuration
430
+
431
+ For `@create-figma-plugin` projects, the manifest is generated from `package.json`. For non-toolchain projects, you maintain `manifest.json` directly.
432
+
433
+ #### Standard Plugin Manifest
434
+
435
+ ```json
436
+ {
437
+ "api": "1.0.0",
438
+ "editorType": ["figma"],
439
+ "id": "YOUR_PLUGIN_ID",
440
+ "name": "My Plugin",
441
+ "main": "build/main.js",
442
+ "ui": "build/ui.js",
443
+ "documentAccess": "dynamic-page",
444
+ "networkAccess": {
445
+ "allowedDomains": [
446
+ "https://fonts.googleapis.com",
447
+ "https://fonts.gstatic.com"
448
+ ]
449
+ }
450
+ }
451
+ ```
452
+
453
+ #### documentAccess: dynamic-page
454
+
455
+ Always use `"dynamic-page"` for new plugins. This is now required and means:
456
+
457
+ - Pages load on demand (not all at once)
458
+ - Use `figma.getNodeByIdAsync()` for node access (async)
459
+ - Use `figma.loadAllPagesAsync()` if you need cross-page access
460
+ - Reduces memory usage for large files
461
+
462
+ > **Compatibility note:** Older plugins may use `"lazy"` document access. New plugins must use `"dynamic-page"`. See `figma-api-plugin.md` for the full manifest field reference.
463
+
464
+ #### networkAccess Patterns
465
+
466
+ The `networkAccess.allowedDomains` array controls which domains the UI iframe can fetch from:
467
+
468
+ ```json
469
+ { "allowedDomains": ["none"] }
470
+ ```
471
+ No network access (most restrictive, recommended for security).
472
+
473
+ ```json
474
+ { "allowedDomains": ["https://fonts.googleapis.com", "https://fonts.gstatic.com"] }
475
+ ```
476
+ Allow specific domains (font loading, API calls).
477
+
478
+ ```json
479
+ { "allowedDomains": ["*"] }
480
+ ```
481
+ Allow all domains (least restrictive, use only if necessary).
482
+
483
+ > **Security principle:** Request the minimum network access your plugin needs. Figma reviews `networkAccess` during plugin publishing. Explain your domains in the `reasoning` field.
484
+
485
+ ---
486
+
487
+ ### UI Architecture
488
+
489
+ #### Preact vs React
490
+
491
+ Preact is strongly recommended for Figma plugins because of bundle size:
492
+
493
+ | Library | Minified Size | Impact |
494
+ |---------|:------------:|--------|
495
+ | Preact | ~4 KB | Fast load, within Figma limits |
496
+ | React + ReactDOM | ~40 KB | Slower load, eats into size budget |
497
+
498
+ The `@create-figma-plugin/ui` component library is built on Preact and provides Figma-native styled components (Button, Text, Container, SegmentedControl, etc.).
499
+
500
+ #### UI Entry Point Pattern
501
+
502
+ ```tsx
503
+ import { render, Container, Button, Text, VerticalSpace } from '@create-figma-plugin/ui';
504
+ import { emit, on } from '@create-figma-plugin/utilities';
505
+ import { h } from 'preact';
506
+ import { useState, useEffect, useCallback } from 'preact/hooks';
507
+
508
+ function Plugin() {
509
+ const [data, setData] = useState(null);
510
+ const [error, setError] = useState<ErrorData | null>(null);
511
+
512
+ useEffect(() => {
513
+ // Register event listeners on mount
514
+ on<FrameExtractedHandler>(EVENTS.FRAME_EXTRACTED, (extracted) => {
515
+ setData(extracted);
516
+ });
517
+ on<ErrorHandler>(EVENTS.ERROR, (err) => {
518
+ setError(err);
519
+ });
520
+
521
+ // Request initial selection
522
+ emit<GetSelectionHandler>(EVENTS.GET_SELECTION);
523
+ }, []);
524
+
525
+ return (
526
+ <Container space="medium">
527
+ {error && <ErrorState error={error} />}
528
+ {data && <DataView data={data} />}
529
+ </Container>
530
+ );
531
+ }
532
+
533
+ export default render(Plugin);
534
+ ```
535
+
536
+ > **Critical:** The `render()` function from `@create-figma-plugin/ui` is the entry point wrapper. It handles Preact mounting and Figma theme integration. Always use it as the default export.
537
+
538
+ #### CodeMirror Integration for Code Display
539
+
540
+ For plugins that display or edit generated code, CodeMirror provides syntax highlighting and editing:
541
+
542
+ ```tsx
543
+ import { EditorView, basicSetup } from '@codemirror/view';
544
+ import { html } from '@codemirror/lang-html';
545
+ import { css } from '@codemirror/lang-css';
546
+
547
+ function CodeEditor({ content, language, onChange }) {
548
+ const containerRef = useRef<HTMLDivElement>(null);
549
+
550
+ useEffect(() => {
551
+ if (!containerRef.current) return;
552
+
553
+ const extensions = [
554
+ basicSetup,
555
+ language === 'html' ? html() : css(),
556
+ EditorView.updateListener.of((update) => {
557
+ if (update.docChanged) {
558
+ onChange(update.state.doc.toString());
559
+ }
560
+ }),
561
+ ];
562
+
563
+ const view = new EditorView({
564
+ doc: content,
565
+ extensions,
566
+ parent: containerRef.current,
567
+ });
568
+
569
+ return () => view.destroy();
570
+ }, []);
571
+
572
+ return <div ref={containerRef} />;
573
+ }
574
+ ```
575
+
576
+ CodeMirror dependencies for a Figma plugin:
577
+
578
+ ```json
579
+ {
580
+ "@codemirror/commands": "^6.10.1",
581
+ "@codemirror/lang-css": "^6.3.1",
582
+ "@codemirror/lang-html": "^6.4.11",
583
+ "@codemirror/language": "^6.12.1",
584
+ "@codemirror/state": "^6.5.3",
585
+ "@codemirror/view": "^6.39.9"
586
+ }
587
+ ```
588
+
589
+ #### Live Preview Pattern
590
+
591
+ Render generated HTML/CSS in a sandboxed iframe within the plugin UI:
592
+
593
+ ```tsx
594
+ function LivePreview({ html, css, assets }) {
595
+ const iframeRef = useRef<HTMLIFrameElement>(null);
596
+
597
+ useEffect(() => {
598
+ if (!iframeRef.current) return;
599
+
600
+ // Replace asset references with base64 data URLs for preview
601
+ let previewHtml = html;
602
+ for (const asset of assets) {
603
+ previewHtml = previewHtml.replace(
604
+ new RegExp(`assets/${asset.filename}`, 'g'),
605
+ asset.dataUrl
606
+ );
607
+ }
608
+
609
+ const doc = iframeRef.current.contentDocument;
610
+ if (doc) {
611
+ doc.open();
612
+ doc.write(previewHtml);
613
+ doc.close();
614
+ }
615
+ }, [html, css, assets]);
616
+
617
+ return <iframe ref={iframeRef} sandbox="allow-same-origin" />;
618
+ }
619
+ ```
620
+
621
+ #### Undo/Redo with useHistory Hook
622
+
623
+ For editable generated code, provide undo/redo functionality:
624
+
625
+ ```ts
626
+ interface UseHistoryReturn<T> {
627
+ value: T;
628
+ set: (value: T) => void;
629
+ undo: () => void;
630
+ redo: () => void;
631
+ canUndo: boolean;
632
+ canRedo: boolean;
633
+ reset: (value: T) => void;
634
+ }
635
+
636
+ function useHistory<T>(initial: T): UseHistoryReturn<T> {
637
+ const [state, setState] = useState({
638
+ past: [] as T[],
639
+ present: initial,
640
+ future: [] as T[],
641
+ });
642
+
643
+ const set = useCallback((value: T) => {
644
+ setState(prev => ({
645
+ past: [...prev.past, prev.present],
646
+ present: value,
647
+ future: [],
648
+ }));
649
+ }, []);
650
+
651
+ const undo = useCallback(() => {
652
+ setState(prev => {
653
+ if (prev.past.length === 0) return prev;
654
+ const newPast = [...prev.past];
655
+ const newPresent = newPast.pop()!;
656
+ return {
657
+ past: newPast,
658
+ present: newPresent,
659
+ future: [prev.present, ...prev.future],
660
+ };
661
+ });
662
+ }, []);
663
+
664
+ // ... redo is symmetric
665
+
666
+ return {
667
+ value: state.present,
668
+ set, undo, redo,
669
+ canUndo: state.past.length > 0,
670
+ canRedo: state.future.length > 0,
671
+ reset: (value) => setState({ past: [], present: value, future: [] }),
672
+ };
673
+ }
674
+ ```
675
+
676
+ #### Window Resize Pattern
677
+
678
+ Allow the UI to request window resizing through IPC:
679
+
680
+ ```ts
681
+ // Main thread handler
682
+ on<ResizeWindowHandler>(EVENTS.RESIZE_WINDOW, (width: number, height: number) => {
683
+ figma.ui.resize(width, height);
684
+ });
685
+
686
+ // UI-side emit
687
+ emit<ResizeWindowHandler>(EVENTS.RESIZE_WINDOW, 520, 900);
688
+ ```
689
+
690
+ > **Note:** Figma does not expose the parent window dimensions. Use a sensible default size and allow the user or layout to request resizes.
691
+
692
+ ---
693
+
694
+ ### Data Flow Architecture
695
+
696
+ The core of a design-to-code plugin is a three-stage pipeline. Each stage produces a well-defined intermediate format that is JSON-serializable (can cross the IPC boundary).
697
+
698
+ ```
699
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
700
+ │ EXTRACTION │ ──► │ GENERATION │ ──► │ EXPORT │
701
+ │ │ │ │ │ │
702
+ │ Figma Nodes │ │ ExtractedNode │ │ GeneratedOutput│
703
+ │ → ExtractedNode│ │ → HTML + CSS │ │ → Files + ZIP │
704
+ │ │ │ + Tokens │ │ │
705
+ │ (Main thread) │ │ (Either side) │ │ (Split) │
706
+ └──────────────┘ └──────────────┘ └──────────────┘
707
+ ```
708
+
709
+ #### Stage 1: Extraction (Figma Nodes → ExtractedNode)
710
+
711
+ The extraction stage reads Figma's SceneNode tree and produces a JSON-serializable `ExtractedNode` tree. This is the **critical boundary** — everything after this point is pure data transformation with no Figma API dependency.
712
+
713
+ ```ts
714
+ export interface ExtractedNode {
715
+ id: string; // Figma node ID (e.g., "123:456")
716
+ name: string; // Node name from Figma layers panel
717
+ type: string; // Node type (FRAME, TEXT, RECTANGLE, etc.)
718
+ bounds: Bounds; // Position and dimensions
719
+ opacity: number; // Node opacity (0-1)
720
+ visible: boolean; // Visibility state
721
+
722
+ // Domain-specific extracted data
723
+ layout?: LayoutProperties; // Auto Layout → flex properties
724
+ layoutChild?: LayoutChildProperties; // Child sizing within Auto Layout
725
+ fills?: FillData[]; // Background fills
726
+ strokes?: StrokeData[]; // Border strokes
727
+ effects?: EffectData[]; // Shadows, blurs
728
+ cornerRadius?: CornerRadius; // Border radius
729
+ text?: TextData; // Typography and content
730
+ asset?: AssetData; // Image/vector asset metadata
731
+ componentRef?: ComponentReference; // Instance → component link
732
+
733
+ children?: ExtractedNode[]; // Recursive children
734
+ }
735
+ ```
736
+
737
+ Key extraction principles:
738
+
739
+ 1. **Filter invisible nodes** — Skip `visible: false` nodes during traversal (they contribute nothing to rendered output)
740
+ 2. **Depth limiting** — Cap tree depth (30 levels is a safe default) to prevent performance degradation on deeply nested designs
741
+ 3. **Progress reporting** — Emit progress events for large trees so the UI can show a loading indicator
742
+ 4. **Type-safe property access** — Use type guards (`'children' in node`, `node.type === 'TEXT'`) before accessing type-specific properties
743
+
744
+ ```ts
745
+ async function extractNodeTree(
746
+ root: SceneNode,
747
+ options?: { maxDepth?: number; onProgress?: (current: number, total: number) => void }
748
+ ): Promise<ExtractedNode> {
749
+ const state = {
750
+ elementCount: 0,
751
+ maxDepth: options?.maxDepth ?? 30,
752
+ onProgress: options?.onProgress,
753
+ };
754
+
755
+ return extractNode(root, null, 0, state);
756
+ }
757
+ ```
758
+
759
+ Each domain has its own extraction module that runs independently:
760
+
761
+ | Module | Input | Output | What It Extracts |
762
+ |--------|-------|--------|-----------------|
763
+ | `extraction/layout.ts` | FrameNode | `LayoutProperties` | Auto Layout direction, gap, padding, alignment, wrap |
764
+ | `extraction/visual.ts` | SceneNode | `FillData[]`, `StrokeData[]`, `EffectData[]` | Colors, gradients, images, borders, shadows |
765
+ | `extraction/text.ts` | TextNode | `TextData` | Content, font, size, weight, line height, styled segments |
766
+ | `extraction/assets.ts` | SceneNode | `AssetData` | Vector container detection, image hash, export strategy |
767
+ | `extraction/variants.ts` | InstanceNode | `ComponentReference` | Component set ID, variant properties, responsive property |
768
+
769
+ > **Cross-reference:** See `design-to-code-layout.md` for the Auto Layout → Flexbox mapping rules, `design-to-code-visual.md` for fill/stroke/effect extraction, and `design-to-code-typography.md` for text extraction patterns.
770
+
771
+ #### Stage 2: Generation (ExtractedNode → Element Tree + CSS)
772
+
773
+ The generation stage transforms the extracted data into a renderable element tree with CSS styles. This is pure data transformation — no Figma API calls.
774
+
775
+ ```ts
776
+ export interface GeneratedElement {
777
+ tag: string; // Semantic HTML tag
778
+ className: string; // BEM class name
779
+ styles: CSSStyles; // CSS properties
780
+ children: (GeneratedElement | string)[]; // Child elements or text
781
+ attributes?: Record<string, string>; // HTML attributes
782
+ figmaId?: string; // Source Figma node ID
783
+ }
784
+
785
+ export interface GeneratedOutput {
786
+ html: GeneratedElement; // Root element tree
787
+ css: CSSRule[]; // Collected CSS rules
788
+ fonts: string[]; // Google Fonts to link
789
+ tokens?: DesignTokens; // Promoted design tokens
790
+ }
791
+ ```
792
+
793
+ The generation pipeline applies multiple concerns in order:
794
+
795
+ ```ts
796
+ // For each ExtractedNode, generate a GeneratedElement:
797
+ function generateElement(node: ExtractedNode, options: GenerateOptions): GeneratedElement {
798
+ const tag = getSemanticTag(node, context); // semantic.ts
799
+ const className = generateBEMClassName(node.name, parentClass, depth); // classname.ts
800
+ const styles: CSSStyles = {};
801
+
802
+ // Apply styles from each domain
803
+ Object.assign(styles, generateLayoutStyles(node)); // layout.ts
804
+ Object.assign(styles, generateLayoutChildStyles(node)); // layout.ts
805
+ Object.assign(styles, generateVisualStyles(node)); // visual.ts
806
+ Object.assign(styles, generateTypographyStyles(node)); // typography.ts
807
+ Object.assign(styles, generatePositionStyles(node)); // position.ts
808
+
809
+ return { tag, className, styles, children: [...], figmaId: node.id };
810
+ }
811
+ ```
812
+
813
+ **BEM Class Naming:**
814
+
815
+ ```ts
816
+ // Root level → block name
817
+ "card"
818
+
819
+ // First children → block__element
820
+ "card__header", "card__body", "card__footer"
821
+
822
+ // Deeper children → flatten to block__element (never block__element__sub)
823
+ "card__title", "card__icon", "card__description"
824
+ ```
825
+
826
+ Class names are deduplicated with a `ClassNameTracker` that appends numeric suffixes when names collide: `card__item`, `card__item-2`, `card__item-3`.
827
+
828
+ **Node-to-Element Mapping:**
829
+
830
+ The `figmaId` field on `GeneratedElement` enables bidirectional traceability:
831
+
832
+ ```ts
833
+ interface NodeMapping {
834
+ figmaId: string; // Original Figma node ID
835
+ elementPath: string; // CSS selector (e.g., ".card__title")
836
+ nodeType: string; // Figma node type
837
+ nodeName: string; // Figma layer name
838
+ isTextNode: boolean; // Can be edited
839
+ }
840
+ ```
841
+
842
+ This enables features like click-to-select (click HTML element → highlight Figma node) and live text sync (edit text in code → update Figma text node).
843
+
844
+ #### Stage 3: Export (GeneratedOutput → Files + ZIP)
845
+
846
+ The export stage assembles the generated output into downloadable files. This stage is **split across the IPC boundary**:
847
+
848
+ - **Main thread:** Prepare bundle (assemble file contents, export binary assets)
849
+ - **UI thread:** Create ZIP file (requires `jszip` which needs browser APIs)
850
+
851
+ ```ts
852
+ // Main thread: Prepare bundle
853
+ export async function prepareExportBundle(
854
+ node: ExtractedNode,
855
+ options: ExportOptions
856
+ ): Promise<PreparedExportBundle> {
857
+ // 1. Build asset map for image fill → filename resolution
858
+ const assetMap = buildAssetMap(node);
859
+
860
+ // 2. Generate output (HTML/CSS/tokens)
861
+ const output = await generateOutput(node, { assetMap });
862
+
863
+ // 3. Export binary assets (images, SVGs) from Figma
864
+ const assets = await exportAllAssets(node);
865
+
866
+ // 4. Assemble bundle (text files + binary assets)
867
+ const bundle = generateExportBundle([{ output, name: node.name }], options);
868
+ bundle.assets.push(...assets);
869
+
870
+ return { bundle, folderName: node.name, fileCount: bundle.files.length, assetCount: assets.length };
871
+ }
872
+ ```
873
+
874
+ The `ExportBundle` structure:
875
+
876
+ ```ts
877
+ interface ExportBundle {
878
+ files: ExportFile[]; // Text files (HTML, CSS, SCSS)
879
+ assets: AssetFile[]; // Binary assets (PNG, SVG, JPG)
880
+ readme?: string; // Optional build instructions
881
+ }
882
+
883
+ interface ExportFile {
884
+ filename: string; // e.g., "index.html", "styles/styles.css"
885
+ content: string; // File content
886
+ type: 'html' | 'css' | 'scss';
887
+ }
888
+
889
+ interface AssetFile {
890
+ filename: string; // e.g., "assets/hero.png"
891
+ data: Uint8Array; // Binary data
892
+ mimeType: string; // e.g., "image/png"
893
+ }
894
+ ```
895
+
896
+ **Multi-Frame Export:**
897
+
898
+ For exporting multiple frames (e.g., a full page with desktop + tablet + mobile views):
899
+
900
+ 1. Extract each frame independently
901
+ 2. Generate output for each frame with frame-specific naming
902
+ 3. Merge design tokens across frames (dedup by name, keep highest usage count)
903
+ 4. Generate an `index.html` table of contents linking to each frame
904
+ 5. Package all files and shared assets into a single ZIP
905
+
906
+ ```ts
907
+ // Responsive mode: Multiple frames → unified CSS with media queries
908
+ const extractedNodes = await Promise.all(frames.map(async (frame) => {
909
+ const node = await figma.getNodeByIdAsync(frame.nodeId);
910
+ return { node: await extractNodeTree(node), name: frame.frameName };
911
+ }));
912
+
913
+ const prepared = await prepareMultiFrameBundle(extractedNodes, options);
914
+ ```
915
+
916
+ ---
917
+
918
+ ### Selection and Figma Event Handling
919
+
920
+ #### Selection Change Listener
921
+
922
+ Register for selection changes to keep the UI in sync with the user's current selection:
923
+
924
+ ```ts
925
+ // Main thread
926
+ figma.on('selectionchange', () => {
927
+ const selection = figma.currentPage.selection;
928
+ const data = selection.map(extractNodeInfo);
929
+ emit<SelectionChangedHandler>(EVENTS.SELECTION_CHANGED, data);
930
+ });
931
+
932
+ function extractNodeInfo(node: SceneNode): NodeInfo {
933
+ const info: NodeInfo = {
934
+ id: node.id,
935
+ name: node.name,
936
+ type: node.type,
937
+ width: node.width,
938
+ height: node.height,
939
+ x: node.x,
940
+ y: node.y,
941
+ };
942
+
943
+ if ('children' in node) {
944
+ info.childCount = (node as ChildrenMixin & SceneNode).children.length;
945
+ }
946
+
947
+ if ('layoutMode' in node) {
948
+ info.hasAutoLayout = node.layoutMode !== 'NONE';
949
+ }
950
+
951
+ return info;
952
+ }
953
+ ```
954
+
955
+ #### Settings Persistence
956
+
957
+ Use `figma.clientStorage` for user preferences that persist across plugin sessions:
958
+
959
+ ```ts
960
+ // Main thread
961
+ const SETTINGS_KEY = 'pluginSettings';
962
+
963
+ on<SaveSettingsHandler>(EVENTS.SAVE_SETTINGS, async (settings: PluginSettings) => {
964
+ try {
965
+ await figma.clientStorage.setAsync(SETTINGS_KEY, settings);
966
+ } catch (error) {
967
+ console.error('Failed to save settings:', error);
968
+ }
969
+ });
970
+
971
+ on<LoadSettingsHandler>(EVENTS.LOAD_SETTINGS, async () => {
972
+ try {
973
+ const settings = await figma.clientStorage.getAsync(SETTINGS_KEY);
974
+ emit<SettingsLoadedHandler>(EVENTS.SETTINGS_LOADED, settings || null);
975
+ } catch (error) {
976
+ emit<SettingsLoadedHandler>(EVENTS.SETTINGS_LOADED, null);
977
+ }
978
+ });
979
+ ```
980
+
981
+ > **Important:** `clientStorage` is local to the user's machine and plugin ID. It does not sync across devices. Use it for UI preferences, not for shared data.
982
+
983
+ ---
984
+
985
+ ### Bidirectional Sync Patterns
986
+
987
+ Advanced plugins can sync changes between the code editor and the Figma canvas.
988
+
989
+ #### Text Node Sync (Code → Figma)
990
+
991
+ When a user edits text in the generated HTML, propagate the change back to the Figma text node:
992
+
993
+ ```ts
994
+ on<UpdateTextNodeHandler>(EVENTS.UPDATE_TEXT_NODE, async (figmaId: string, newText: string) => {
995
+ try {
996
+ const node = await figma.getNodeByIdAsync(figmaId);
997
+ if (!node || node.type !== 'TEXT') {
998
+ emit<TextNodeUpdatedHandler>(EVENTS.TEXT_NODE_UPDATED, false, figmaId, 'Not a text node');
999
+ return;
1000
+ }
1001
+
1002
+ const textNode = node as TextNode;
1003
+
1004
+ // Load fonts before modifying text (required by Figma API)
1005
+ if (textNode.fontName === figma.mixed) {
1006
+ // Mixed fonts — load each font range
1007
+ const len = textNode.characters.length;
1008
+ const loaded = new Set<string>();
1009
+ for (let i = 0; i < len; i++) {
1010
+ const font = textNode.getRangeFontName(i, i + 1) as FontName;
1011
+ const key = `${font.family}-${font.style}`;
1012
+ if (!loaded.has(key)) {
1013
+ loaded.add(key);
1014
+ await figma.loadFontAsync(font);
1015
+ }
1016
+ }
1017
+ } else {
1018
+ await figma.loadFontAsync(textNode.fontName as FontName);
1019
+ }
1020
+
1021
+ textNode.characters = newText;
1022
+ emit<TextNodeUpdatedHandler>(EVENTS.TEXT_NODE_UPDATED, true, figmaId);
1023
+ } catch (error) {
1024
+ emit<TextNodeUpdatedHandler>(EVENTS.TEXT_NODE_UPDATED, false, figmaId, String(error));
1025
+ }
1026
+ });
1027
+ ```
1028
+
1029
+ #### Layout Property Sync (Code → Figma)
1030
+
1031
+ Sync CSS layout property changes back to Figma Auto Layout properties:
1032
+
1033
+ ```ts
1034
+ on<UpdateLayoutNodeHandler>(EVENTS.UPDATE_LAYOUT_NODE, async (figmaId, property, value) => {
1035
+ const node = await figma.getNodeByIdAsync(figmaId);
1036
+ if (!node || !('layoutMode' in node)) return;
1037
+
1038
+ const frame = node as FrameNode;
1039
+
1040
+ switch (property) {
1041
+ case 'flex-direction':
1042
+ frame.layoutMode = value === 'row' ? 'HORIZONTAL' : 'VERTICAL';
1043
+ break;
1044
+ case 'justify-content':
1045
+ const justifyMap = { 'flex-start': 'MIN', 'center': 'CENTER', 'flex-end': 'MAX', 'space-between': 'SPACE_BETWEEN' };
1046
+ frame.primaryAxisAlignItems = justifyMap[value] || 'MIN';
1047
+ break;
1048
+ case 'align-items':
1049
+ const alignMap = { 'flex-start': 'MIN', 'center': 'CENTER', 'flex-end': 'MAX', 'baseline': 'BASELINE' };
1050
+ frame.counterAxisAlignItems = alignMap[value] || 'MIN';
1051
+ break;
1052
+ case 'gap':
1053
+ frame.itemSpacing = parseInt(value, 10);
1054
+ break;
1055
+ case 'flex-wrap':
1056
+ frame.layoutWrap = value === 'wrap' ? 'WRAP' : 'NO_WRAP';
1057
+ break;
1058
+ // padding-top, padding-right, padding-bottom, padding-left...
1059
+ }
1060
+
1061
+ emit<LayoutNodeUpdatedHandler>(EVENTS.LAYOUT_NODE_UPDATED, true, figmaId);
1062
+ });
1063
+ ```
1064
+
1065
+ > **Key insight:** The `data-figma-id` attribute on generated HTML elements is what connects the rendered output back to the source Figma nodes. This attribute is included during HTML rendering and enables any bidirectional sync feature.
1066
+
1067
+ ---
1068
+
1069
+ ### Performance Patterns
1070
+
1071
+ #### Large Frame Warnings
1072
+
1073
+ Monitor extraction size and warn about performance implications:
1074
+
1075
+ ```ts
1076
+ const elementCount = countElements(extracted);
1077
+ const extractionTime = Date.now() - startTime;
1078
+ console.log(`[Extraction] ${elementCount} elements in ${extractionTime}ms`);
1079
+
1080
+ if (elementCount > 1000) {
1081
+ console.warn(`[Extraction] Large frame (${elementCount} elements). Performance may be affected.`);
1082
+ }
1083
+ ```
1084
+
1085
+ #### Timing Instrumentation
1086
+
1087
+ Log timing for each pipeline stage to identify bottlenecks:
1088
+
1089
+ ```ts
1090
+ const extractStart = Date.now();
1091
+ const extracted = await extractNodeTree(node);
1092
+ const extractTime = Date.now() - extractStart;
1093
+
1094
+ const genStart = Date.now();
1095
+ const output = await generateOutput(extracted, options);
1096
+ const genTime = Date.now() - genStart;
1097
+
1098
+ console.log(
1099
+ `[Pipeline] ${elementCount} elements: ` +
1100
+ `extraction ${extractTime}ms, generation ${genTime}ms, ` +
1101
+ `total ${extractTime + genTime}ms`
1102
+ );
1103
+ ```
1104
+
1105
+ #### Asset Export Optimization
1106
+
1107
+ Asset export (images, SVGs) is the slowest part of the pipeline. Optimize by:
1108
+
1109
+ 1. **Deduplicating assets** by image hash before export
1110
+ 2. **Building an asset map** before generation so code references correct filenames
1111
+ 3. **Logging per-asset timing** for large exports to identify slow assets
1112
+
1113
+ ```ts
1114
+ const assetMap = buildAssetMap(extracted); // Hash → filename mapping
1115
+ setAssetMap(assetMap); // Make available to CSS generation
1116
+ const output = await generateOutput(extracted, { assetMap });
1117
+ const assets = await exportAllAssets(extracted); // Export unique assets only
1118
+ ```
1119
+
1120
+ ---
1121
+
1122
+ ### Testing Patterns
1123
+
1124
+ The extraction and generation modules are pure functions operating on JSON data, making them highly testable:
1125
+
1126
+ ```ts
1127
+ // generation/layout.test.ts
1128
+ import { describe, it, expect } from 'vitest';
1129
+ import { generateLayoutStyles } from './layout';
1130
+
1131
+ describe('generateLayoutStyles', () => {
1132
+ it('generates flex-direction: row for FLEX_ROW mode', () => {
1133
+ const node = {
1134
+ layout: { mode: 'FLEX_ROW', gap: 8, padding: { top: 0, right: 0, bottom: 0, left: 0 } }
1135
+ };
1136
+ const styles = generateLayoutStyles(node);
1137
+ expect(styles.display).toBe('flex');
1138
+ expect(styles.flexDirection).toBe('row');
1139
+ expect(styles.gap).toBe('8px');
1140
+ });
1141
+ });
1142
+ ```
1143
+
1144
+ Test setup in `package.json`:
1145
+
1146
+ ```json
1147
+ {
1148
+ "devDependencies": {
1149
+ "vitest": "^4.0.16",
1150
+ "@vitest/coverage-v8": "^4.0.16"
1151
+ },
1152
+ "scripts": {
1153
+ "test": "vitest run",
1154
+ "test:watch": "vitest",
1155
+ "test:coverage": "vitest run --coverage"
1156
+ }
1157
+ }
1158
+ ```
1159
+
1160
+ > **Principle:** Keep Figma API calls isolated in `extraction/` and `export/assets.ts`. Everything else operates on plain TypeScript types and can be tested without any Figma environment.
1161
+
1162
+ ---
1163
+
1164
+ ## Cross-References
1165
+
1166
+ - **`figma-api-plugin.md`** — Plugin API reference (sandbox model, SceneNode types, manifest fields, API methods). This module builds on that foundation with architecture patterns.
1167
+ - **`figma-api-devmode.md`** — Dev Mode codegen plugin API reference. For codegen-specific architecture, see `plugin-codegen.md`.
1168
+ - **`design-to-code-layout.md`** — Auto Layout → Flexbox mapping rules consumed by `extraction/layout.ts` and `generation/layout.ts`.
1169
+ - **`design-to-code-visual.md`** — Visual property extraction rules consumed by `extraction/visual.ts` and `generation/visual.ts`.
1170
+ - **`design-to-code-typography.md`** — Typography extraction rules consumed by `extraction/text.ts` and `generation/typography.ts`.
1171
+ - **`design-to-code-assets.md`** — Asset detection and export patterns consumed by `extraction/assets.ts` and `export/assets.ts`.
1172
+ - **`design-to-code-semantic.md`** — Semantic HTML tag selection used by `generation/semantic.ts`.
1173
+ - **`css-strategy.md`** — Three-layer CSS architecture (Tailwind + Custom Properties + CSS Modules) for organizing generated CSS.
1174
+ - **`design-tokens.md`** — Token promotion and rendering patterns consumed by `tokens/` modules.
1175
+ - **`plugin-codegen.md`** — Codegen plugin development patterns (Dev Mode integration, generate callback, preferences system).
1176
+ - **`plugin-best-practices.md`** — Production best practices for error handling, performance, memory management, caching, async patterns, testing, and distribution. Complements the architecture patterns in this module with quality and reliability guidance.