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,1206 @@
1
+ # Figma Plugin Best Practices
2
+
3
+ ## Purpose
4
+
5
+ Production-hardened best practices for Figma plugin development — covering error handling, performance optimization, memory management, caching strategies, async patterns, testing and debugging, and plugin distribution. These patterns are extracted from a production Figma plugin that extracts designs and generates HTML/CSS code. For plugin architecture patterns (project structure, IPC, data flow), see `plugin-architecture.md`. For the Plugin API reference, see `figma-api-plugin.md`.
6
+
7
+ ## When to Use
8
+
9
+ Reference this module when you need to:
10
+
11
+ - Implement robust error handling with structured error types and IPC propagation
12
+ - Optimize plugin performance for large Figma files (1000+ elements)
13
+ - Manage memory effectively when processing complex node trees
14
+ - Cache Figma API results (variables, fonts, component references) to reduce repeated calls
15
+ - Write correct async code with Figma's async APIs (getNodeByIdAsync, loadFontAsync, exportAsync)
16
+ - Test and debug plugin logic without the Figma runtime environment
17
+ - Prepare a plugin for publishing (permissions, manifest, versioning)
18
+
19
+ ---
20
+
21
+ ## Content
22
+
23
+ ### 1. Error Handling Patterns
24
+
25
+ Reliable error handling is the difference between a plugin that users trust and one they abandon. Every error in a Figma plugin falls into one of three categories: validation errors (bad input), processing errors (extraction/generation failures), and infrastructure errors (Figma API failures).
26
+
27
+ #### Structured Error Types
28
+
29
+ Define error data that the UI can display and act upon. A structured error includes a machine-readable code, a human-readable message, and optional technical details:
30
+
31
+ ```ts
32
+ // Error code enum — one code per failure category
33
+ export const ERROR_CODES = {
34
+ NO_SELECTION: 'NO_SELECTION',
35
+ INVALID_NODE: 'INVALID_NODE',
36
+ EXTRACTION_FAILED: 'EXTRACTION_FAILED',
37
+ GENERATION_FAILED: 'GENERATION_FAILED',
38
+ EXPORT_FAILED: 'EXPORT_FAILED',
39
+ THUMBNAIL_FAILED: 'THUMBNAIL_FAILED',
40
+ } as const;
41
+
42
+ export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
43
+
44
+ // Structured error data for UI display
45
+ export interface ErrorData {
46
+ code: ErrorCode;
47
+ message: string; // User-facing message (non-technical)
48
+ details?: string; // Technical details (from caught exception)
49
+ }
50
+ ```
51
+
52
+ Why codes instead of just messages:
53
+ - The UI can show different recovery actions per error code
54
+ - Error codes are stable across refactors (messages can change)
55
+ - Logging and analytics can aggregate by code
56
+ - Localization can map codes to translated messages
57
+
58
+ #### Error Propagation Across IPC
59
+
60
+ Errors originating in the main thread must be propagated to the UI thread through IPC. Use a centralized `emitError` helper so every error follows the same pattern:
61
+
62
+ ```ts
63
+ import { emit } from '@create-figma-plugin/utilities';
64
+
65
+ function emitError(code: ErrorData['code'], message: string, error?: unknown): void {
66
+ const details = error instanceof Error
67
+ ? error.message
68
+ : error ? String(error) : undefined;
69
+
70
+ // Always log to console for developer debugging
71
+ console.error(`[Error] ${code}: ${message}`, details || '');
72
+
73
+ // Emit structured error to UI
74
+ emit<ErrorHandler>(EVENTS.ERROR, { code, message, details });
75
+ }
76
+ ```
77
+
78
+ Benefits of a centralized helper:
79
+ - Console logging is automatic (developers see errors during development)
80
+ - Error structure is consistent (UI can always expect the same shape)
81
+ - Unknown error types are safely converted to strings
82
+ - The `code` field enables the UI to show contextual recovery suggestions
83
+
84
+ #### Try/Catch Placement
85
+
86
+ Wrap the entire body of each IPC event handler in a single try/catch. Do NOT wrap individual operations within a handler — that fragments error handling and makes it easy to miss paths:
87
+
88
+ ```ts
89
+ // GOOD: Single try/catch per handler
90
+ on<ExtractFrameHandler>(EVENTS.EXTRACT_FRAME, async (nodeId: string) => {
91
+ try {
92
+ const node = await figma.getNodeByIdAsync(nodeId);
93
+ if (!node) {
94
+ emitError(ERROR_CODES.INVALID_NODE, 'Node not found. It may have been deleted.');
95
+ return;
96
+ }
97
+ if (!('children' in node)) {
98
+ emitError(ERROR_CODES.INVALID_NODE, 'Selected node is not a frame or group.');
99
+ return;
100
+ }
101
+
102
+ const extracted = await extractNodeTree(node as SceneNode);
103
+ emit<FrameExtractedHandler>(EVENTS.FRAME_EXTRACTED, extracted);
104
+ } catch (error) {
105
+ emitError(ERROR_CODES.EXTRACTION_FAILED, 'Failed to extract frame data.', error);
106
+ }
107
+ });
108
+
109
+ // BAD: Multiple try/catch blocks per handler
110
+ on<ExtractFrameHandler>(EVENTS.EXTRACT_FRAME, async (nodeId: string) => {
111
+ let node;
112
+ try {
113
+ node = await figma.getNodeByIdAsync(nodeId);
114
+ } catch (e) {
115
+ emitError(ERROR_CODES.INVALID_NODE, 'Node lookup failed.', e);
116
+ return;
117
+ }
118
+ let extracted;
119
+ try {
120
+ extracted = await extractNodeTree(node as SceneNode);
121
+ } catch (e) {
122
+ emitError(ERROR_CODES.EXTRACTION_FAILED, 'Extraction failed.', e);
123
+ return;
124
+ }
125
+ // ... more fragmented try/catch blocks
126
+ });
127
+ ```
128
+
129
+ The exception to this rule is operations where you want to skip a failure and continue (e.g., asset export for individual files where one failure should not block others).
130
+
131
+ #### Graceful Degradation
132
+
133
+ When processing a tree of nodes, skip unsupported or problematic nodes rather than failing the entire operation:
134
+
135
+ ```ts
136
+ // Asset export: skip failures, continue with remaining assets
137
+ for (let i = 0; i < nodesWithAssets.length; i++) {
138
+ const assetNode = nodesWithAssets[i];
139
+ try {
140
+ const data = await exportAsset(assetNode.id, format);
141
+ assets.push({ filename, data, mimeType });
142
+ } catch (error) {
143
+ // Log but continue with other assets
144
+ console.warn(`[Export] Failed to export asset "${assetNode.name}":`, error);
145
+ }
146
+ }
147
+
148
+ // Thumbnail generation: add placeholder on failure
149
+ try {
150
+ const bytes = await node.exportAsync({ format: 'PNG', constraint: { type: 'WIDTH', value: 200 } });
151
+ thumbnails.push({ id: nodeId, name: node.name, thumbnail: base64Data });
152
+ } catch (error) {
153
+ console.error('[Thumbnails] Failed for node:', nodeId, error);
154
+ // Still include the frame with empty thumbnail — UI shows placeholder
155
+ thumbnails.push({ id: nodeId, name: node.name, thumbnail: '' });
156
+ }
157
+ ```
158
+
159
+ #### UI Error Display and Recovery
160
+
161
+ On the UI side, handle error events by showing actionable messages with recovery options:
162
+
163
+ ```tsx
164
+ on<ErrorHandler>(EVENTS.ERROR, (error: ErrorData) => {
165
+ setError(error);
166
+ });
167
+
168
+ function ErrorState({ error, onRetry }: { error: ErrorData; onRetry?: () => void }) {
169
+ const recoveryHint = getRecoveryHint(error.code);
170
+
171
+ return (
172
+ <Container space="medium">
173
+ <Text style="bold">{error.message}</Text>
174
+ {recoveryHint && <Text style="muted">{recoveryHint}</Text>}
175
+ {error.details && <Text style="code">{error.details}</Text>}
176
+ {onRetry && <Button onClick={onRetry}>Retry</Button>}
177
+ </Container>
178
+ );
179
+ }
180
+
181
+ function getRecoveryHint(code: ErrorCode): string | null {
182
+ switch (code) {
183
+ case 'NO_SELECTION':
184
+ return 'Select a frame or component in the Figma canvas.';
185
+ case 'INVALID_NODE':
186
+ return 'Select a frame, component, or group — not a page or individual shape.';
187
+ case 'EXTRACTION_FAILED':
188
+ return 'The design may be too complex. Try selecting a smaller section.';
189
+ case 'EXPORT_FAILED':
190
+ return 'Export failed. Try again or check if any images are missing.';
191
+ default:
192
+ return null;
193
+ }
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ ### 2. Performance Patterns
200
+
201
+ Performance is critical for plugin adoption. A plugin that freezes Figma for 5 seconds on every interaction will be uninstalled immediately. The key insight: the extraction phase (reading Figma nodes) is the bottleneck, not generation (pure data transformation).
202
+
203
+ #### Progress Tracking for Long Operations
204
+
205
+ For any operation that processes more than ~100 nodes, report progress to the UI so users see that the plugin is working:
206
+
207
+ ```ts
208
+ // Extraction statistics for performance monitoring
209
+ export interface ExtractionStats {
210
+ elementCount: number;
211
+ maxDepthReached: number;
212
+ depthLimitHit: boolean;
213
+ extractionTimeMs: number;
214
+ }
215
+
216
+ // Internal mutable state during extraction
217
+ interface ExtractionState {
218
+ elementCount: number;
219
+ maxDepthReached: number;
220
+ depthLimitHit: boolean;
221
+ onProgress?: (current: number, total: number) => void;
222
+ maxDepth: number;
223
+ }
224
+
225
+ // Emit progress every N elements
226
+ if (state.onProgress && state.elementCount % 50 === 0) {
227
+ state.onProgress(state.elementCount, state.elementCount + 100); // Estimate total
228
+ }
229
+ ```
230
+
231
+ Wire progress to the UI through IPC:
232
+
233
+ ```ts
234
+ const extracted = await extractNodeTree(node, {
235
+ onProgress: (current, total) => {
236
+ emit<ExtractionProgressHandler>(EVENTS.EXTRACTION_PROGRESS, {
237
+ current,
238
+ total,
239
+ phase: 'extracting',
240
+ });
241
+ },
242
+ });
243
+ ```
244
+
245
+ #### Pipeline Timing Instrumentation
246
+
247
+ Log timing for each stage of the extraction-generation-export pipeline to identify bottlenecks:
248
+
249
+ ```ts
250
+ // Extraction phase
251
+ const extractStart = Date.now();
252
+ const extracted = await extractNodeTree(node as SceneNode);
253
+ const extractTime = Date.now() - extractStart;
254
+
255
+ // Generation phase
256
+ const genStart = Date.now();
257
+ const output = await generateOutput(extracted, options);
258
+ const genTime = Date.now() - genStart;
259
+
260
+ const elementCount = countElements(extracted);
261
+ console.log(
262
+ `[Pipeline] ${elementCount} elements: ` +
263
+ `extraction ${extractTime}ms, generation ${genTime}ms, ` +
264
+ `total ${extractTime + genTime}ms`
265
+ );
266
+
267
+ // Warn about large frames
268
+ if (elementCount > 1000) {
269
+ console.warn(
270
+ `[Extraction] Large frame detected (${elementCount} elements). ` +
271
+ `Performance may be affected.`
272
+ );
273
+ }
274
+ ```
275
+
276
+ This instrumentation reveals where to invest optimization effort. In practice, extraction typically accounts for 60-80% of total time because it involves async Figma API calls, while generation is pure synchronous computation.
277
+
278
+ #### Depth Limits on Recursive Traversal
279
+
280
+ Cap tree traversal depth to prevent performance degradation on deeply nested designs. Figma designs rarely exceed 15-20 levels of nesting intentionally — deeper nesting usually indicates auto-layout structure rather than meaningful visual hierarchy:
281
+
282
+ ```ts
283
+ const MAX_DEPTH = 30;
284
+
285
+ async function extractNode(
286
+ node: SceneNode,
287
+ parent: SceneNode | null,
288
+ depth: number,
289
+ state: ExtractionState
290
+ ): Promise<ExtractedNode | null> {
291
+ // Check depth limit before recursing
292
+ if ('children' in node) {
293
+ if (depth >= state.maxDepth) {
294
+ state.depthLimitHit = true;
295
+ console.warn(
296
+ `[Extraction] Depth limit (${state.maxDepth}) reached at "${node.name}", ` +
297
+ `truncating ${node.children.length} children`
298
+ );
299
+ return extracted; // Return node without children
300
+ }
301
+
302
+ const visibleChildren = node.children.filter(child => child.visible);
303
+ extracted.children = await Promise.all(
304
+ visibleChildren.map(child => extractNode(child, node, depth + 1, state))
305
+ );
306
+ }
307
+
308
+ return extracted;
309
+ }
310
+ ```
311
+
312
+ A depth of 30 is a safe production default — it accommodates complex designs while preventing runaway recursion on pathological files.
313
+
314
+ #### Batch Sequential Operations
315
+
316
+ When processing children of a node, batch operations sequentially rather than firing all at once. Figma's API can handle some parallelism, but excessive concurrent calls can cause issues:
317
+
318
+ ```ts
319
+ // GOOD: Process children with controlled parallelism
320
+ const visibleChildren = containerNode.children.filter(child => child.visible);
321
+ const childResults = await Promise.all(
322
+ visibleChildren.map(child => extractNode(child, node, depth + 1, state))
323
+ );
324
+
325
+ // GOOD: Sequential for Figma API calls that modify state
326
+ for (const font of fontsToLoad) {
327
+ await figma.loadFontAsync(font);
328
+ }
329
+
330
+ // BAD: Unbounded parallelism on heavy Figma API operations
331
+ const allExports = await Promise.all(
332
+ allNodes.map(node => node.exportAsync({ format: 'PNG' })) // Can overwhelm Figma
333
+ );
334
+ ```
335
+
336
+ For pure extraction (reading properties), parallel `Promise.all` on children is fine. For operations that involve I/O like `exportAsync`, process sequentially or in small batches.
337
+
338
+ #### UI Responsiveness During Extraction
339
+
340
+ The main thread shares execution time with Figma's renderer. Long synchronous operations block the UI. Use progress callbacks and chunked processing to keep the plugin responsive:
341
+
342
+ ```ts
343
+ // Report progress periodically during traversal
344
+ if (state.onProgress && state.elementCount % 50 === 0) {
345
+ state.onProgress(state.elementCount, state.elementCount + 100);
346
+ }
347
+ ```
348
+
349
+ For asset export (the slowest operation), log per-asset timing to help identify slow exports:
350
+
351
+ ```ts
352
+ for (const asset of assets) {
353
+ const assetStartTime = Date.now();
354
+ const base64 = uint8ArrayToBase64(asset.data);
355
+ const assetTime = Date.now() - assetStartTime;
356
+ if (assetTime > 100) {
357
+ console.log(
358
+ `[Export] Base64 conversion: ${asset.filename} ` +
359
+ `(${asset.data.length} bytes) took ${assetTime}ms`
360
+ );
361
+ }
362
+ }
363
+ ```
364
+
365
+ #### Bundle Size Optimization
366
+
367
+ Plugin load time affects perceived performance. The UI iframe must load all JavaScript before the plugin becomes interactive:
368
+
369
+ | Library | Minified Size | Recommendation |
370
+ |---------|:------------:|----------------|
371
+ | Preact | ~4 KB | Use this for plugin UI |
372
+ | React + ReactDOM | ~40 KB | Avoid — 10x larger than Preact |
373
+ | CodeMirror (basic) | ~150 KB | Worth it for code editing features |
374
+ | JSZip | ~100 KB | Load only when export is triggered |
375
+
376
+ Optimization strategies:
377
+ - Use Preact (not React) with `@create-figma-plugin/ui` components
378
+ - Enable `--minify` in the build script (`build-figma-plugin --typecheck --minify`)
379
+ - Tree shake unused modules by using named imports
380
+ - Consider dynamic import for heavy libraries (CodeMirror, JSZip) loaded only when needed
381
+
382
+ > **Cross-reference:** See `plugin-architecture.md` for the Preact vs React comparison and `@create-figma-plugin` build configuration.
383
+
384
+ ---
385
+
386
+ ### 3. Memory Management
387
+
388
+ Figma plugins run in a constrained environment. The main thread sandbox has limited memory, and the IPC bridge can only transfer JSON-serializable data. Memory mismanagement causes crashes on large files.
389
+
390
+ #### JSON-Serializable Intermediate Format
391
+
392
+ The most critical architectural decision in any design-to-code plugin: the extraction stage must produce a **JSON-serializable** intermediate format. You cannot pass Figma `SceneNode` objects across the IPC boundary — they are live references to the document graph, not plain data.
393
+
394
+ ```ts
395
+ // WRONG: Trying to pass Figma nodes to the UI
396
+ figma.ui.postMessage({
397
+ type: 'NODE_DATA',
398
+ node: figma.currentPage.selection[0], // Fails — not serializable
399
+ });
400
+
401
+ // RIGHT: Extract to a plain data structure first
402
+ export interface ExtractedNode {
403
+ id: string;
404
+ name: string;
405
+ type: string;
406
+ bounds: { x: number; y: number; width: number; height: number };
407
+ opacity: number;
408
+ visible: boolean;
409
+ layout?: LayoutProperties;
410
+ fills?: FillData[];
411
+ strokes?: StrokeData[];
412
+ effects?: EffectData[];
413
+ text?: TextData;
414
+ asset?: AssetData;
415
+ children?: ExtractedNode[];
416
+ }
417
+
418
+ // Extract first, then pass across IPC
419
+ const extracted = await extractNodeTree(node);
420
+ emit<FrameExtractedHandler>(EVENTS.FRAME_EXTRACTED, extracted);
421
+ ```
422
+
423
+ This pattern also provides a clean separation between Figma API access (main thread only) and code generation (can run on either side).
424
+
425
+ #### Selective Property Extraction
426
+
427
+ Extract only the properties that your generation pipeline needs. Reading every property on every node wastes time and memory:
428
+
429
+ ```ts
430
+ // GOOD: Extract only what generation needs
431
+ const extracted: ExtractedNode = {
432
+ id: node.id,
433
+ name: node.name,
434
+ type: node.type,
435
+ bounds: {
436
+ x: Math.round(node.x),
437
+ y: Math.round(node.y),
438
+ width: Math.round(node.width),
439
+ height: Math.round(node.height),
440
+ },
441
+ opacity: 'opacity' in node ? node.opacity : 1,
442
+ visible: node.visible,
443
+ };
444
+
445
+ // Only extract layout if the node has Auto Layout
446
+ const layout = await extractLayout(node);
447
+ if (layout) extracted.layout = layout;
448
+
449
+ // Only extract text if it's a TEXT node
450
+ const text = await extractText(node);
451
+ if (text) extracted.text = text;
452
+ ```
453
+
454
+ Each domain extraction module (layout, visual, text, assets) runs independently and only populates its fields when relevant. Nodes that are plain rectangles with no special properties carry minimal extracted data.
455
+
456
+ #### Asset Deduplication by Hash
457
+
458
+ Figma files commonly reuse the same image across multiple nodes (a logo, avatar, pattern). Export each unique image once, not once per usage:
459
+
460
+ ```ts
461
+ // Track already-exported imageHashes to avoid duplicates
462
+ const exportedImageHashes = new Map<string, string>(); // imageHash -> filename
463
+
464
+ for (const assetNode of nodesWithAssets) {
465
+ const imageHash = assetNode.asset!.imageHash;
466
+
467
+ // Skip duplicate image exports — same imageHash means same image
468
+ if (imageHash && exportedImageHashes.has(imageHash)) {
469
+ console.log(`[Export] Skipping duplicate image "${assetNode.name}" (same hash)`);
470
+ continue;
471
+ }
472
+
473
+ const data = await exportAsset(assetNode.id, format);
474
+ assets.push({ filename, data, mimeType });
475
+
476
+ // Track exported hash
477
+ if (imageHash) {
478
+ exportedImageHashes.set(imageHash, filename);
479
+ }
480
+ }
481
+ ```
482
+
483
+ The same deduplication applies to the asset map used by the generation pipeline:
484
+
485
+ ```ts
486
+ // Build a map from node ID and imageHash to asset filename
487
+ export function buildAssetMap(node: ExtractedNode): Map<string, string> {
488
+ const assetMap = new Map<string, string>();
489
+ const imageHashToFilename = new Map<string, string>();
490
+
491
+ for (const assetNode of nodesWithAssets) {
492
+ const imageHash = assetNode.asset!.imageHash;
493
+
494
+ // Reuse existing filename for same hash
495
+ if (imageHash && imageHashToFilename.has(imageHash)) {
496
+ assetMap.set(assetNode.id, imageHashToFilename.get(imageHash)!);
497
+ continue;
498
+ }
499
+
500
+ const filename = filenameTracker.getUnique(baseName, extension);
501
+ assetMap.set(assetNode.id, filename);
502
+
503
+ if (imageHash) {
504
+ assetMap.set(imageHash, filename);
505
+ imageHashToFilename.set(imageHash, filename);
506
+ }
507
+ }
508
+
509
+ return assetMap;
510
+ }
511
+ ```
512
+
513
+ #### Large File Handling
514
+
515
+ For files with hundreds of frames or thousands of nodes, implement safeguards:
516
+
517
+ - **Element count warnings:** Log warnings at thresholds (e.g., 1000+ elements) so developers know to optimize
518
+ - **Depth limiting:** Cap recursion depth to prevent stack overflow (see Performance Patterns above)
519
+ - **Pagination:** For multi-frame export, process frames sequentially rather than loading all into memory at once
520
+ - **Asset budget:** Log asset count and total size estimates before starting export
521
+
522
+ ```ts
523
+ // Pre-flight check before starting heavy export
524
+ const elementCount = countElements(extracted);
525
+ let assetCount = 0;
526
+ const countAssets = (n: ExtractedNode) => {
527
+ if (n.asset) assetCount++;
528
+ n.children?.forEach(countAssets);
529
+ };
530
+ countAssets(extracted);
531
+
532
+ console.log(`[Export] Pre-flight: ${elementCount} elements, ${assetCount} assets`);
533
+ ```
534
+
535
+ #### Clean Up References
536
+
537
+ Null out large cached data structures when they are no longer needed. Module-level caches in the main thread persist for the plugin's entire lifetime:
538
+
539
+ ```ts
540
+ // Module-level variable cache
541
+ let localVariableIds: Set<string> | null = null;
542
+
543
+ // Clear when starting a new extraction run
544
+ export function clearVariableCache(): void {
545
+ localVariableIds = null;
546
+ }
547
+ ```
548
+
549
+ ---
550
+
551
+ ### 4. Caching Strategies
552
+
553
+ Figma's async APIs have non-trivial overhead. Caching results of repeated calls dramatically improves performance, especially during tree traversal where the same information is needed for every node.
554
+
555
+ #### Variable Cache (Lazy-Load Once, Reuse)
556
+
557
+ The most impactful cache in a design-to-code plugin: loading local variables once and reusing the set for every node's variable bindings check:
558
+
559
+ ```ts
560
+ // Cache for local variable IDs — populated once per extraction run
561
+ let localVariableIds: Set<string> | null = null;
562
+
563
+ async function isLocalVariable(variableId: string): Promise<boolean> {
564
+ if (localVariableIds === null) {
565
+ // Lazy-load: first call populates the cache
566
+ try {
567
+ const localVars = await figma.variables.getLocalVariablesAsync();
568
+ localVariableIds = new Set(localVars.map(v => v.id));
569
+ } catch {
570
+ localVariableIds = new Set();
571
+ }
572
+ }
573
+ return localVariableIds.has(variableId);
574
+ }
575
+ ```
576
+
577
+ Without this cache, every fill/stroke/effect extraction that checks for variable bindings would call `getLocalVariablesAsync()` — a slow async operation. With the cache, the first node pays the cost and all subsequent nodes get an O(1) Set lookup.
578
+
579
+ #### Font Loading Cache
580
+
581
+ Font loading is required before text node modification but not before text node reading during extraction. For operations that modify text nodes (like bidirectional sync), cache already-loaded fonts:
582
+
583
+ ```ts
584
+ const loadedFonts = new Set<string>();
585
+
586
+ async function ensureFontLoaded(fontName: FontName): Promise<void> {
587
+ const key = `${fontName.family}-${fontName.style}`;
588
+ if (loadedFonts.has(key)) return;
589
+
590
+ await figma.loadFontAsync(fontName);
591
+ loadedFonts.add(key);
592
+ }
593
+
594
+ // Usage in text sync handler
595
+ if (textNode.fontName === figma.mixed) {
596
+ const len = textNode.characters.length;
597
+ for (let i = 0; i < len; i++) {
598
+ const font = textNode.getRangeFontName(i, i + 1) as FontName;
599
+ await ensureFontLoaded(font);
600
+ }
601
+ } else {
602
+ await ensureFontLoaded(textNode.fontName as FontName);
603
+ }
604
+ ```
605
+
606
+ #### Component Reference Cache
607
+
608
+ When processing instances, cache the mapping from component IDs to component names to avoid repeated `getMainComponentAsync()` calls:
609
+
610
+ ```ts
611
+ const componentNameCache = new Map<string, string>();
612
+
613
+ async function getComponentName(instance: InstanceNode): Promise<string | null> {
614
+ const mainComponent = instance.mainComponent;
615
+ if (!mainComponent) return null;
616
+
617
+ const cached = componentNameCache.get(mainComponent.id);
618
+ if (cached) return cached;
619
+
620
+ componentNameCache.set(mainComponent.id, mainComponent.name);
621
+ return mainComponent.name;
622
+ }
623
+ ```
624
+
625
+ #### Cache Invalidation
626
+
627
+ Caches must be invalidated when the underlying data changes:
628
+
629
+ | Cache | Invalidation Trigger | Strategy |
630
+ |-------|---------------------|----------|
631
+ | Variable IDs | New extraction run | Clear at extraction start |
632
+ | Loaded fonts | Never (fonts don't change) | Persist for plugin lifetime |
633
+ | Component names | Page change | Clear on `currentpagechange` |
634
+ | Extracted node data | Selection change | Clear and re-extract |
635
+ | Asset map | New extraction run | Rebuild per extraction |
636
+
637
+ ```ts
638
+ // Clear caches on page change
639
+ figma.on('currentpagechange', () => {
640
+ componentNameCache.clear();
641
+ clearVariableCache();
642
+ });
643
+ ```
644
+
645
+ #### Session vs Persistent Caching
646
+
647
+ Two storage mechanisms are available for caching:
648
+
649
+ **Session-scoped (module-level variables):**
650
+ - Lives in memory for the plugin's lifetime
651
+ - Lost when plugin is closed
652
+ - Use for: variable IDs, font caches, component names, extracted data
653
+
654
+ **Persistent (`figma.clientStorage`):**
655
+ - Survives plugin restarts
656
+ - Local to user's machine and plugin ID
657
+ - Use for: user preferences, export settings, component mapping configs
658
+
659
+ ```ts
660
+ // Persistent: User settings survive restarts
661
+ const SETTINGS_KEY = 'pluginSettings';
662
+ await figma.clientStorage.setAsync(SETTINGS_KEY, settings);
663
+ const settings = await figma.clientStorage.getAsync(SETTINGS_KEY);
664
+
665
+ // Persistent: Clear all stored data
666
+ const keys = await figma.clientStorage.keysAsync();
667
+ for (const key of keys) {
668
+ await figma.clientStorage.deleteAsync(key);
669
+ }
670
+ ```
671
+
672
+ **Node-level (`pluginData`):**
673
+ - Stored on individual Figma nodes
674
+ - Persists with the file, visible only to your plugin
675
+ - Use for: per-node metadata, export markers, last-generated state
676
+
677
+ ```ts
678
+ // Store export metadata on a node
679
+ node.setPluginData('lastExport', JSON.stringify({ timestamp: Date.now(), format: 'html' }));
680
+ const meta = JSON.parse(node.getPluginData('lastExport') || 'null');
681
+ ```
682
+
683
+ ---
684
+
685
+ ### 5. Async Patterns
686
+
687
+ Figma's Plugin API is heavily async. The most common plugin bugs come from incorrect async handling — using sync methods in dynamic-page plugins, forgetting to load fonts, or not handling timeouts.
688
+
689
+ #### getNodeByIdAsync for Dynamic-Page Plugins
690
+
691
+ All new plugins must use `documentAccess: "dynamic-page"`. This means pages load on demand and node access is async:
692
+
693
+ ```ts
694
+ // CORRECT: Async node access (required for dynamic-page)
695
+ const node = await figma.getNodeByIdAsync(nodeId);
696
+
697
+ // WRONG: Sync access (deprecated, fails with dynamic-page)
698
+ const node = figma.getNodeById(nodeId); // Don't use this
699
+ ```
700
+
701
+ Always null-check the result — nodes can be deleted, on unloaded pages, or invalid:
702
+
703
+ ```ts
704
+ const node = await figma.getNodeByIdAsync(nodeId);
705
+ if (!node) {
706
+ emitError(ERROR_CODES.INVALID_NODE, 'Node not found. It may have been deleted.');
707
+ return;
708
+ }
709
+ ```
710
+
711
+ #### loadFontAsync Before Text Modifications
712
+
713
+ Font loading is **mandatory** before modifying text node content or style properties. Reading text content (during extraction) does not require font loading:
714
+
715
+ ```ts
716
+ // Reading text — NO font loading needed
717
+ const content = textNode.characters; // Works without loading fonts
718
+ const fontSize = textNode.fontSize; // Works without loading fonts
719
+
720
+ // Modifying text — MUST load fonts first
721
+ await figma.loadFontAsync(textNode.fontName as FontName);
722
+ textNode.characters = 'New text'; // Throws if font not loaded
723
+
724
+ // Mixed fonts — must load each font range
725
+ if (textNode.fontName === figma.mixed) {
726
+ const len = textNode.characters.length;
727
+ const loaded = new Set<string>();
728
+ for (let i = 0; i < len; i++) {
729
+ const font = textNode.getRangeFontName(i, i + 1) as FontName;
730
+ const key = `${font.family}-${font.style}`;
731
+ if (!loaded.has(key)) {
732
+ loaded.add(key);
733
+ await figma.loadFontAsync(font);
734
+ }
735
+ }
736
+ }
737
+ ```
738
+
739
+ #### exportAsync for Images and SVGs
740
+
741
+ `exportAsync()` is the most time-consuming Figma API call. Use it carefully:
742
+
743
+ ```ts
744
+ // PNG/JPG export with retina scaling
745
+ const pngBytes = await node.exportAsync({
746
+ format: 'PNG',
747
+ constraint: { type: 'SCALE', value: 2 }, // 2x for retina
748
+ });
749
+
750
+ // SVG export (vector-native, no scaling needed)
751
+ const svgBytes = await node.exportAsync({ format: 'SVG' });
752
+
753
+ // Thumbnail export with fixed width (consistent quality)
754
+ const thumbBytes = await node.exportAsync({
755
+ format: 'PNG',
756
+ constraint: { type: 'WIDTH', value: 200 },
757
+ });
758
+ ```
759
+
760
+ For image fills, prefer exporting from Figma's image store directly to get original quality:
761
+
762
+ ```ts
763
+ async function exportImageByHash(imageHash: string): Promise<Uint8Array> {
764
+ const image = figma.getImageByHash(imageHash);
765
+ if (!image) throw new Error(`Image not found: ${imageHash}`);
766
+ return image.getBytesAsync();
767
+ }
768
+ ```
769
+
770
+ #### Timeout Handling for Long Operations
771
+
772
+ Wrap long-running operations with timeouts to prevent the plugin from appearing frozen:
773
+
774
+ ```ts
775
+ const EXPORT_TIMEOUT_MS = 10000;
776
+
777
+ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
778
+ return new Promise((resolve, reject) => {
779
+ const timer = setTimeout(() => {
780
+ reject(new Error(`Export timeout after ${ms}ms for: ${label}`));
781
+ }, ms);
782
+
783
+ promise
784
+ .then((result) => {
785
+ clearTimeout(timer);
786
+ resolve(result);
787
+ })
788
+ .catch((error) => {
789
+ clearTimeout(timer);
790
+ reject(error);
791
+ });
792
+ });
793
+ }
794
+
795
+ // Usage
796
+ const svgData = await withTimeout(
797
+ node.exportAsync({ format: 'SVG' }),
798
+ EXPORT_TIMEOUT_MS,
799
+ `SVG export of "${node.name}"`
800
+ );
801
+ ```
802
+
803
+ > **Note:** The main thread sandbox does not have `setTimeout`. This `withTimeout` pattern works because the timer is created inside a Promise executor, using the Promise-based timing that the sandbox does support. For codegen plugins, the 3-second hard timeout set by Figma itself is the limiting factor — see `plugin-codegen.md`.
804
+
805
+ #### Sequential vs Parallel Async
806
+
807
+ Use this decision guide for async patterns:
808
+
809
+ | Operation | Strategy | Why |
810
+ |-----------|:--------:|-----|
811
+ | Extract child node properties | Parallel (`Promise.all`) | Read-only, no contention |
812
+ | Export multiple assets | Sequential (for loop) | Each `exportAsync` is heavy, avoids overwhelming Figma |
813
+ | Load multiple fonts | Sequential (for loop) | Font loading can fail if too many concurrent |
814
+ | Process multiple frames | Sequential (for loop) | Each frame extraction is memory-intensive |
815
+ | Generate code from extracted data | Synchronous | Pure data transformation, no async needed |
816
+
817
+ ```ts
818
+ // Parallel: extracting children (read-only, safe)
819
+ const childResults = await Promise.all(
820
+ visibleChildren.map(child => extractNode(child, node, depth + 1, state))
821
+ );
822
+
823
+ // Sequential: exporting assets (heavy I/O, one at a time)
824
+ for (const assetNode of nodesWithAssets) {
825
+ const data = await exportAsset(assetNode.id, format);
826
+ assets.push({ filename, data, mimeType });
827
+ }
828
+ ```
829
+
830
+ #### Cancellation Patterns
831
+
832
+ For long-running operations, check whether the plugin is still active before continuing:
833
+
834
+ ```ts
835
+ // Check if plugin was closed during a long operation
836
+ let pluginActive = true;
837
+ figma.on('close', () => { pluginActive = false; });
838
+
839
+ async function extractLargeTree(root: SceneNode): Promise<ExtractedNode> {
840
+ for (const child of root.children) {
841
+ if (!pluginActive) {
842
+ console.log('[Extraction] Plugin closed, aborting');
843
+ throw new Error('Plugin closed during extraction');
844
+ }
845
+ await extractNode(child);
846
+ }
847
+ }
848
+ ```
849
+
850
+ ---
851
+
852
+ ### 6. Testing and Debugging
853
+
854
+ Plugin code is challenging to test because extraction modules depend on the Figma API runtime. The key strategy: isolate Figma API calls to a thin extraction layer and make everything else pure functions on plain data.
855
+
856
+ #### Console Logging Strategy
857
+
858
+ Use prefixed, structured console logging throughout the plugin for debugging:
859
+
860
+ ```ts
861
+ // Good: prefixed, contextual, with relevant data
862
+ console.log('[Extraction] 450 elements in 234ms');
863
+ console.log('[Export] Processing asset 3/12: "hero-image" as PNG');
864
+ console.log('[Settings] Saved settings to client storage');
865
+ console.warn('[Extraction] Depth limit (30) reached at "deeply-nested-frame"');
866
+ console.error('[Export] Failed to export asset "broken-image":', error);
867
+
868
+ // Bad: unprefixed, no context
869
+ console.log('done');
870
+ console.log(node.name);
871
+ console.log('error', e);
872
+ ```
873
+
874
+ Use consistent prefixes per domain:
875
+ - `[Extraction]` — Node tree traversal and property extraction
876
+ - `[Generation]` — HTML/CSS code generation
877
+ - `[Export]` — Asset export and bundle preparation
878
+ - `[Settings]` — Settings persistence
879
+ - `[TextSync]` — Bidirectional text sync
880
+ - `[LayoutSync]` — Bidirectional layout sync
881
+ - `[Cache]` — Cache operations
882
+ - `[Thumbnails]` — Thumbnail generation
883
+ - `[Error]` — Error propagation
884
+ - `[Pipeline]` — Overall pipeline timing
885
+ - `[Variants]` — Component variant resolution
886
+
887
+ #### Code Validation
888
+
889
+ Validate generated HTML and CSS before displaying to users. Broken output erodes trust:
890
+
891
+ ```ts
892
+ interface ValidationResult {
893
+ valid: boolean;
894
+ errors: ValidationError[];
895
+ sanitizedCode: string;
896
+ }
897
+
898
+ // HTML validation: tag structure, bracket matching, script removal
899
+ function validateHTML(html: string): ValidationResult {
900
+ const errors: ValidationError[] = [];
901
+ let sanitized = html;
902
+
903
+ sanitized = removeJavaScript(sanitized, errors); // Security
904
+ validateTagStructure(sanitized, errors); // Unclosed tags
905
+ validateBrackets(sanitized, errors); // Quote matching
906
+
907
+ return { valid: errors.filter(e => e.type === 'error').length === 0, errors, sanitizedCode: sanitized };
908
+ }
909
+
910
+ // CSS validation: bracket matching, property syntax, JS removal
911
+ function validateCSS(css: string): ValidationResult {
912
+ const errors: ValidationError[] = [];
913
+ let sanitized = css;
914
+
915
+ sanitized = removeJSFromCSS(sanitized, errors); // Security
916
+ validateCSSBrackets(sanitized, errors); // Brace matching
917
+ validateCSSProperties(sanitized, errors); // Semicolons, colons
918
+
919
+ return { valid: errors.filter(e => e.type === 'error').length === 0, errors, sanitizedCode: sanitized };
920
+ }
921
+ ```
922
+
923
+ Always strip JavaScript from generated output for security — generated HTML should never contain executable code:
924
+
925
+ ```ts
926
+ function removeJavaScript(html: string, errors: ValidationError[]): string {
927
+ let result = html;
928
+ // Remove <script> tags
929
+ result = result.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
930
+ // Remove inline event handlers (onclick, onerror, etc.)
931
+ result = result.replace(/\s(on\w+)=["'][^"']*["']/gi, '');
932
+ // Remove javascript: URLs
933
+ result = result.replace(/href=["']javascript:[^"']*["']/gi, 'href="#"');
934
+ return result;
935
+ }
936
+ ```
937
+
938
+ > **Cross-reference:** See `plugin-codegen.md` for code quality patterns specific to codegen plugins (HTML validation, CSS validation, formatting).
939
+
940
+ #### Mock Data for UI Development
941
+
942
+ Develop and test the plugin UI independently of the Figma runtime by creating mock data fixtures:
943
+
944
+ ```ts
945
+ // test/fixtures/mock-extracted.ts
946
+ export const mockExtractedFrame: ExtractedNode = {
947
+ id: '1:100',
948
+ name: 'Hero Section',
949
+ type: 'FRAME',
950
+ bounds: { x: 0, y: 0, width: 1440, height: 600 },
951
+ opacity: 1,
952
+ visible: true,
953
+ layout: {
954
+ mode: 'FLEX_COLUMN',
955
+ gap: 24,
956
+ padding: { top: 48, right: 64, bottom: 48, left: 64 },
957
+ primaryAlign: 'CENTER',
958
+ crossAlign: 'CENTER',
959
+ },
960
+ children: [
961
+ {
962
+ id: '1:101',
963
+ name: 'Title',
964
+ type: 'TEXT',
965
+ bounds: { x: 0, y: 0, width: 800, height: 48 },
966
+ opacity: 1,
967
+ visible: true,
968
+ text: {
969
+ content: 'Welcome to Our Platform',
970
+ font: { family: 'Inter', style: 'Bold', size: 48, weight: 700 },
971
+ },
972
+ },
973
+ // ... more mock children
974
+ ],
975
+ };
976
+ ```
977
+
978
+ Use these fixtures for:
979
+ - UI component development (render with mock data, no Figma needed)
980
+ - Unit tests for generation modules
981
+ - Visual regression testing of generated HTML output
982
+ - Developing export features offline
983
+
984
+ #### Testing Pure Generation Functions
985
+
986
+ The generation pipeline operates on `ExtractedNode` data, not Figma API objects. This makes it fully testable with standard test frameworks:
987
+
988
+ ```ts
989
+ // generation/layout.test.ts
990
+ import { describe, it, expect } from 'vitest';
991
+ import { generateLayoutStyles } from './layout';
992
+
993
+ describe('generateLayoutStyles', () => {
994
+ it('generates flex-direction: row for horizontal layout', () => {
995
+ const node = {
996
+ layout: {
997
+ mode: 'FLEX_ROW',
998
+ gap: 8,
999
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
1000
+ },
1001
+ };
1002
+ const styles = generateLayoutStyles(node);
1003
+ expect(styles.display).toBe('flex');
1004
+ expect(styles.flexDirection).toBe('row');
1005
+ expect(styles.gap).toBe('8px');
1006
+ });
1007
+
1008
+ it('generates flex-wrap for wrapping layouts', () => {
1009
+ const node = {
1010
+ layout: { mode: 'FLEX_ROW', wrap: true, gap: 16 },
1011
+ };
1012
+ const styles = generateLayoutStyles(node);
1013
+ expect(styles.flexWrap).toBe('wrap');
1014
+ });
1015
+ });
1016
+ ```
1017
+
1018
+ #### Edge Case Handling
1019
+
1020
+ Common edge cases that cause plugin crashes or incorrect output:
1021
+
1022
+ | Edge Case | Symptom | Defense |
1023
+ |-----------|---------|--------|
1024
+ | Empty selection | `figma.currentPage.selection` is `[]` | Check length before processing |
1025
+ | Text without fonts loaded | `fontName === figma.mixed` | Handle mixed fonts with range iteration |
1026
+ | Zero-size frames | `width: 0, height: 0` | Filter out or skip in generation |
1027
+ | SLICE nodes | Not renderable, no visual output | Skip during traversal (`node.type === 'SLICE'`) |
1028
+ | Invisible nodes | `visible: false` | Filter during extraction |
1029
+ | Detached instances | `instance.mainComponent` is `null` | Null-check before accessing component |
1030
+ | Deeply nested groups | 50+ levels of nesting | Depth limit (MAX_DEPTH = 30) |
1031
+ | Deleted nodes | `getNodeByIdAsync` returns `null` | Null-check after every async lookup |
1032
+ | Stale selection | Node deleted after selection event | Re-validate before processing |
1033
+ | Missing image fills | Image removed from library | Try/catch around image export |
1034
+
1035
+ ```ts
1036
+ // SLICE node handling in traversal
1037
+ if (node.type === 'SLICE') {
1038
+ return null; // Skip entirely — SLICE nodes are export regions, not visual elements
1039
+ }
1040
+
1041
+ // Zero-size frame handling
1042
+ if (node.width === 0 || node.height === 0) {
1043
+ console.warn(`[Extraction] Skipping zero-size node: "${node.name}"`);
1044
+ return null;
1045
+ }
1046
+
1047
+ // Detached instance handling
1048
+ if (node.type === 'INSTANCE') {
1049
+ const mainComponent = instance.mainComponent;
1050
+ if (!mainComponent) {
1051
+ console.warn(`[Extraction] Detached instance: "${instance.name}"`);
1052
+ // Continue extraction without component reference
1053
+ }
1054
+ }
1055
+ ```
1056
+
1057
+ #### Common Pitfalls Checklist
1058
+
1059
+ Before publishing, verify your plugin handles these scenarios:
1060
+
1061
+ 1. **Accessing node after deletion** — Always use `getNodeByIdAsync` and null-check the result
1062
+ 2. **Font not loaded before text modification** — Call `loadFontAsync` before setting `characters`
1063
+ 3. **Stale selection data** — Re-fetch selection on each operation, do not cache selection state
1064
+ 4. **Sync API in dynamic-page plugin** — Use async variants (`getNodeByIdAsync`, not `getNodeById`)
1065
+ 5. **Binary data across IPC** — Convert `Uint8Array` to base64 string before `postMessage`
1066
+ 6. **Missing `figma.mixed` handling** — Properties like `fontName`, `cornerRadius`, `fills` can be `figma.mixed`
1067
+ 7. **Module-level cache leaks** — Clear caches between extraction runs or on page change
1068
+ 8. **Unbounded `findAll()` calls** — Never call `figma.root.findAll()` on large files; scope to current page or selected subtree
1069
+ 9. **Unhandled promise rejections** — Wrap every async handler in try/catch
1070
+ 10. **Plugin close during async operation** — Check `pluginActive` flag in long operations
1071
+
1072
+ ---
1073
+
1074
+ ### 7. Plugin Distribution
1075
+
1076
+ Preparing a plugin for publishing requires attention to manifest metadata, permissions, and version management.
1077
+
1078
+ #### Manifest Metadata
1079
+
1080
+ For `@create-figma-plugin` projects, metadata is configured in the `figma-plugin` section of `package.json`. For manual projects, it goes in `manifest.json`:
1081
+
1082
+ ```json
1083
+ {
1084
+ "figma-plugin": {
1085
+ "name": "My Plugin",
1086
+ "id": "YOUR_PLUGIN_ID",
1087
+ "editorType": ["figma"],
1088
+ "main": "src/main.ts",
1089
+ "ui": "src/ui.tsx",
1090
+ "documentAccess": "dynamic-page"
1091
+ }
1092
+ }
1093
+ ```
1094
+
1095
+ Publishing checklist:
1096
+ - **Name:** Clear, descriptive, max 50 characters
1097
+ - **Description:** Explain what the plugin does in 1-2 sentences
1098
+ - **Icon:** 128x128 PNG with rounded corners (Figma enforces this)
1099
+ - **Cover image:** 1920x960 PNG showing the plugin in action
1100
+ - **Tags:** Choose relevant tags for discoverability (up to 5)
1101
+
1102
+ #### Permission Minimization
1103
+
1104
+ Request only the permissions your plugin actually needs. Figma reviews permissions during publishing:
1105
+
1106
+ | Permission | Grants | When to Use |
1107
+ |-----------|--------|-------------|
1108
+ | `currentuser` | Read current user's name and ID | Personalized settings, analytics |
1109
+ | `activeusers` | Read names/IDs of users currently viewing the file | Collaborative features |
1110
+ | `fileusers` | Read all file collaborators | Collaboration tracking |
1111
+ | `payments` | Access payment/licensing APIs | Paid plugins |
1112
+ | `teamlibrary` | Access team library styles/components | Design system tools |
1113
+
1114
+ ```json
1115
+ {
1116
+ "permissions": ["currentuser"]
1117
+ }
1118
+ ```
1119
+
1120
+ Most plugins need **zero** permissions. Only add permissions you actively use and can justify.
1121
+
1122
+ #### Network Access Justification
1123
+
1124
+ If your plugin makes network requests (through the UI iframe), declare the domains and provide reasoning:
1125
+
1126
+ ```json
1127
+ {
1128
+ "networkAccess": {
1129
+ "allowedDomains": [
1130
+ "https://fonts.googleapis.com",
1131
+ "https://fonts.gstatic.com"
1132
+ ],
1133
+ "reasoning": "Google Fonts: Used to preview generated HTML with the correct fonts."
1134
+ }
1135
+ }
1136
+ ```
1137
+
1138
+ Figma reviewers check network access claims. Use the narrowest domain scope possible:
1139
+
1140
+ ```json
1141
+ // GOOD: Specific domains
1142
+ { "allowedDomains": ["https://api.example.com"] }
1143
+
1144
+ // ACCEPTABLE: Wildcard subdomain
1145
+ { "allowedDomains": ["https://*.example.com"] }
1146
+
1147
+ // BAD: Open access (requires strong justification)
1148
+ { "allowedDomains": ["*"] }
1149
+
1150
+ // BEST: No network access
1151
+ { "allowedDomains": ["none"] }
1152
+ ```
1153
+
1154
+ #### Version Management with @create-figma-plugin
1155
+
1156
+ Version your plugin through `package.json`. The build tool includes the version in the generated manifest:
1157
+
1158
+ ```json
1159
+ {
1160
+ "version": "1.2.0",
1161
+ "figma-plugin": {
1162
+ "name": "My Plugin v1.2.0"
1163
+ }
1164
+ }
1165
+ ```
1166
+
1167
+ Use semantic versioning:
1168
+ - **Major:** Breaking changes (UI redesign, data format changes)
1169
+ - **Minor:** New features (new export format, new settings)
1170
+ - **Patch:** Bug fixes (crash fix, rendering correction)
1171
+
1172
+ Build and publish workflow:
1173
+
1174
+ ```bash
1175
+ # Development
1176
+ npm run watch # Rebuild on changes
1177
+
1178
+ # Production build
1179
+ npm run build # build-figma-plugin --typecheck --minify
1180
+
1181
+ # Testing
1182
+ npm test # vitest run
1183
+
1184
+ # Publishing
1185
+ # 1. Update version in package.json
1186
+ # 2. Build production bundle
1187
+ # 3. In Figma desktop app: Plugins > Manage plugins > Publish
1188
+ # 4. Upload build/main.js and build/ui.js
1189
+ # 5. Add description, tags, cover image
1190
+ # 6. Submit for review
1191
+ ```
1192
+
1193
+ > **Cross-reference:** See `plugin-architecture.md` for the complete `@create-figma-plugin` project setup and build configuration.
1194
+
1195
+ ---
1196
+
1197
+ ## Cross-References
1198
+
1199
+ - **`plugin-architecture.md`** — Production plugin architecture patterns: project setup with `@create-figma-plugin`, project structure, IPC event system, data flow pipeline (extraction → generation → export), UI architecture. This module builds on that foundation with quality and reliability patterns.
1200
+ - **`plugin-codegen.md`** — Codegen plugin development patterns: Dev Mode integration, generate callback, preferences system, code quality. Shares many best practices from this module (error handling, caching, code validation) applied to the codegen context.
1201
+ - **`figma-api-plugin.md`** — Plugin API reference: sandbox model, SceneNode types, manifest fields, key API methods, constraints. This module provides the production patterns for using those APIs correctly.
1202
+ - **`figma-api-devmode.md`** — Dev Mode and codegen API reference. Codegen plugins face additional constraints (3-second timeout) that amplify the need for caching and performance optimization documented here.
1203
+ - **`design-to-code-layout.md`** — Auto Layout to Flexbox mapping rules consumed by extraction and generation modules.
1204
+ - **`design-to-code-visual.md`** — Visual property extraction patterns consumed by extraction modules.
1205
+ - **`design-to-code-assets.md`** — Asset detection and export patterns. The deduplication and timeout patterns in this module apply directly to asset export.
1206
+ - **`design-tokens-variables.md`** — Figma Variables to CSS custom property mapping. The variable caching pattern in this module optimizes the variable resolution described there.