figma-code-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/bin/install.js +328 -0
- package/knowledge/README.md +62 -0
- package/knowledge/css-strategy.md +973 -0
- package/knowledge/design-to-code-assets.md +855 -0
- package/knowledge/design-to-code-layout.md +929 -0
- package/knowledge/design-to-code-semantic.md +1085 -0
- package/knowledge/design-to-code-typography.md +1003 -0
- package/knowledge/design-to-code-visual.md +1145 -0
- package/knowledge/design-tokens-variables.md +1261 -0
- package/knowledge/design-tokens.md +960 -0
- package/knowledge/figma-api-devmode.md +894 -0
- package/knowledge/figma-api-plugin.md +920 -0
- package/knowledge/figma-api-rest.md +742 -0
- package/knowledge/figma-api-variables.md +848 -0
- package/knowledge/figma-api-webhooks.md +876 -0
- package/knowledge/payload-blocks.md +1184 -0
- package/knowledge/payload-figma-mapping.md +1210 -0
- package/knowledge/payload-visual-builder.md +1004 -0
- package/knowledge/plugin-architecture.md +1176 -0
- package/knowledge/plugin-best-practices.md +1206 -0
- package/knowledge/plugin-codegen.md +1313 -0
- package/package.json +31 -0
- package/skills/README.md +103 -0
- package/skills/audit-plugin/SKILL.md +244 -0
- package/skills/build-codegen-plugin/SKILL.md +279 -0
- package/skills/build-importer/SKILL.md +320 -0
- package/skills/build-plugin/SKILL.md +199 -0
- package/skills/build-token-pipeline/SKILL.md +363 -0
- package/skills/ref-html/SKILL.md +290 -0
- package/skills/ref-layout/SKILL.md +150 -0
- package/skills/ref-payload-block/SKILL.md +415 -0
- package/skills/ref-react/SKILL.md +222 -0
- package/skills/ref-tokens/SKILL.md +347 -0
|
@@ -0,0 +1,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.
|