├── .gitignore
├── .nowignore
├── README.md
├── editor.html
├── figma.d.ts
├── index.js
├── manifest.json
├── now.json
├── package.json
├── src
├── code.tsx
├── components
│ └── editor-layout
│ │ └── EditorLayout.tsx
├── editor.html
├── editor.tsx
├── serialize.ts
├── transformers
│ └── snapshotToReactFigmaAst.ts
├── ui.html
└── ui.tsx
├── tsconfig.json
├── webpack.config.editor.js
├── webpack.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 | /build
4 | .idea
5 | /public
6 |
--------------------------------------------------------------------------------
/.nowignore:
--------------------------------------------------------------------------------
1 | .cache
2 | node_modules
3 | dist
4 | .idea
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [WIP] Figma Code Generator Plugin
2 |
3 | ### Pipeline
4 |
5 | * Serialize Figma node tree to plain JS-object snapshot
6 | * Transform plain JS-object snapshot to AST (via [@babel/types](https://babeljs.io/docs/en/babel-types))
7 | * Transform AST to code (via [@babel/generator](https://babeljs.io/docs/en/babel-generator))
8 | * Format code (via [Prettier](https://prettier.io/))
9 | * Show code with syntax highlighting (via [Monaco Editor](https://microsoft.github.io/monaco-editor/))
10 |
11 | ### Possible variations
12 |
13 | * `react-figma` code
14 | - Primitives based (View, Stylesheet, etc.)
15 | - More native (Rectangle, etc.)
16 | * `react-native` code
17 | * React web code
18 | * Figma API code
19 |
--------------------------------------------------------------------------------
/editor.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
--------------------------------------------------------------------------------
/figma.d.ts:
--------------------------------------------------------------------------------
1 | // Figma Plugin API version 1, update 14
2 |
3 | declare global {
4 | // Global variable with Figma's plugin API.
5 | const figma: PluginAPI
6 | const __html__: string
7 |
8 | interface PluginAPI {
9 | readonly apiVersion: "1.0.0"
10 | readonly command: string
11 | readonly viewport: ViewportAPI
12 | closePlugin(message?: string): void
13 |
14 | notify(message: string, options?: NotificationOptions): NotificationHandler
15 |
16 | showUI(html: string, options?: ShowUIOptions): void
17 | readonly ui: UIAPI
18 |
19 | readonly clientStorage: ClientStorageAPI
20 |
21 | getNodeById(id: string): BaseNode | null
22 | getStyleById(id: string): BaseStyle | null
23 |
24 | readonly root: DocumentNode
25 | currentPage: PageNode
26 |
27 | on(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void
28 | once(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void
29 | off(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void
30 |
31 | readonly mixed: unique symbol
32 |
33 | createRectangle(): RectangleNode
34 | createLine(): LineNode
35 | createEllipse(): EllipseNode
36 | createPolygon(): PolygonNode
37 | createStar(): StarNode
38 | createVector(): VectorNode
39 | createText(): TextNode
40 | createFrame(): FrameNode
41 | createComponent(): ComponentNode
42 | createPage(): PageNode
43 | createSlice(): SliceNode
44 | /**
45 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead.
46 | */
47 | createBooleanOperation(): BooleanOperationNode
48 |
49 | createPaintStyle(): PaintStyle
50 | createTextStyle(): TextStyle
51 | createEffectStyle(): EffectStyle
52 | createGridStyle(): GridStyle
53 |
54 | // The styles are returned in the same order as displayed in the UI. Only
55 | // local styles are returned. Never styles from team library.
56 | getLocalPaintStyles(): PaintStyle[]
57 | getLocalTextStyles(): TextStyle[]
58 | getLocalEffectStyles(): EffectStyle[]
59 | getLocalGridStyles(): GridStyle[]
60 |
61 | importComponentByKeyAsync(key: string): Promise
62 | importStyleByKeyAsync(key: string): Promise
63 |
64 | listAvailableFontsAsync(): Promise
65 | loadFontAsync(fontName: FontName): Promise
66 | readonly hasMissingFont: boolean
67 |
68 | createNodeFromSvg(svg: string): FrameNode
69 |
70 | createImage(data: Uint8Array): Image
71 | getImageByHash(hash: string): Image
72 |
73 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): GroupNode
74 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode
75 |
76 | union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode
77 | subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode
78 | intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode
79 | exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode
80 | }
81 |
82 | interface ClientStorageAPI {
83 | getAsync(key: string): Promise
84 | setAsync(key: string, value: any): Promise
85 | }
86 |
87 | interface NotificationOptions {
88 | timeout?: number
89 | }
90 |
91 | interface NotificationHandler {
92 | cancel: () => void
93 | }
94 |
95 | interface ShowUIOptions {
96 | visible?: boolean
97 | width?: number
98 | height?: number
99 | }
100 |
101 | interface UIPostMessageOptions {
102 | origin?: string
103 | }
104 |
105 | interface OnMessageProperties {
106 | origin: string
107 | }
108 |
109 | type MessageEventHandler = (pluginMessage: any, props: OnMessageProperties) => void
110 |
111 | interface UIAPI {
112 | show(): void
113 | hide(): void
114 | resize(width: number, height: number): void
115 | close(): void
116 |
117 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void
118 | onmessage: MessageEventHandler | undefined
119 | on(type: "message", callback: MessageEventHandler): void
120 | once(type: "message", callback: MessageEventHandler): void
121 | off(type: "message", callback: MessageEventHandler): void
122 | }
123 |
124 | interface ViewportAPI {
125 | center: Vector
126 | zoom: number
127 | scrollAndZoomIntoView(nodes: ReadonlyArray): void
128 | readonly bounds: Rect
129 | }
130 |
131 | ////////////////////////////////////////////////////////////////////////////////
132 | // Datatypes
133 |
134 | type Transform = [
135 | [number, number, number],
136 | [number, number, number]
137 | ]
138 |
139 | interface Vector {
140 | readonly x: number
141 | readonly y: number
142 | }
143 |
144 | interface Rect {
145 | readonly x: number
146 | readonly y: number
147 | readonly width: number
148 | readonly height: number
149 | }
150 |
151 | interface RGB {
152 | readonly r: number
153 | readonly g: number
154 | readonly b: number
155 | }
156 |
157 | interface RGBA {
158 | readonly r: number
159 | readonly g: number
160 | readonly b: number
161 | readonly a: number
162 | }
163 |
164 | interface FontName {
165 | readonly family: string
166 | readonly style: string
167 | }
168 |
169 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE"
170 |
171 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH"
172 |
173 | interface ArcData {
174 | readonly startingAngle: number
175 | readonly endingAngle: number
176 | readonly innerRadius: number
177 | }
178 |
179 | interface ShadowEffect {
180 | readonly type: "DROP_SHADOW" | "INNER_SHADOW"
181 | readonly color: RGBA
182 | readonly offset: Vector
183 | readonly radius: number
184 | readonly visible: boolean
185 | readonly blendMode: BlendMode
186 | }
187 |
188 | interface BlurEffect {
189 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR"
190 | readonly radius: number
191 | readonly visible: boolean
192 | }
193 |
194 | type Effect = ShadowEffect | BlurEffect
195 |
196 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE"
197 |
198 | interface Constraints {
199 | readonly horizontal: ConstraintType
200 | readonly vertical: ConstraintType
201 | }
202 |
203 | interface ColorStop {
204 | readonly position: number
205 | readonly color: RGBA
206 | }
207 |
208 | interface ImageFilters {
209 | readonly exposure?: number
210 | readonly contrast?: number
211 | readonly saturation?: number
212 | readonly temperature?: number
213 | readonly tint?: number
214 | readonly highlights?: number
215 | readonly shadows?: number
216 | }
217 |
218 | interface SolidPaint {
219 | readonly type: "SOLID"
220 | readonly color: RGB
221 |
222 | readonly visible?: boolean
223 | readonly opacity?: number
224 | readonly blendMode?: BlendMode
225 | }
226 |
227 | interface GradientPaint {
228 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND"
229 | readonly gradientTransform: Transform
230 | readonly gradientStops: ReadonlyArray
231 |
232 | readonly visible?: boolean
233 | readonly opacity?: number
234 | readonly blendMode?: BlendMode
235 | }
236 |
237 | interface ImagePaint {
238 | readonly type: "IMAGE"
239 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE"
240 | readonly imageHash: string | null
241 | readonly imageTransform?: Transform // setting for "CROP"
242 | readonly scalingFactor?: number // setting for "TILE"
243 | readonly filters?: ImageFilters
244 |
245 | readonly visible?: boolean
246 | readonly opacity?: number
247 | readonly blendMode?: BlendMode
248 | }
249 |
250 | type Paint = SolidPaint | GradientPaint | ImagePaint
251 |
252 | interface Guide {
253 | readonly axis: "X" | "Y"
254 | readonly offset: number
255 | }
256 |
257 | interface RowsColsLayoutGrid {
258 | readonly pattern: "ROWS" | "COLUMNS"
259 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER"
260 | readonly gutterSize: number
261 |
262 | readonly count: number // Infinity when "Auto" is set in the UI
263 | readonly sectionSize?: number // Not set for alignment: "STRETCH"
264 | readonly offset?: number // Not set for alignment: "CENTER"
265 |
266 | readonly visible?: boolean
267 | readonly color?: RGBA
268 | }
269 |
270 | interface GridLayoutGrid {
271 | readonly pattern: "GRID"
272 | readonly sectionSize: number
273 |
274 | readonly visible?: boolean
275 | readonly color?: RGBA
276 | }
277 |
278 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid
279 |
280 | interface ExportSettingsConstraints {
281 | readonly type: "SCALE" | "WIDTH" | "HEIGHT"
282 | readonly value: number
283 | }
284 |
285 | interface ExportSettingsImage {
286 | readonly format: "JPG" | "PNG"
287 | readonly contentsOnly?: boolean // defaults to true
288 | readonly suffix?: string
289 | readonly constraint?: ExportSettingsConstraints
290 | }
291 |
292 | interface ExportSettingsSVG {
293 | readonly format: "SVG"
294 | readonly contentsOnly?: boolean // defaults to true
295 | readonly suffix?: string
296 | readonly svgOutlineText?: boolean // defaults to true
297 | readonly svgIdAttribute?: boolean // defaults to false
298 | readonly svgSimplifyStroke?: boolean // defaults to true
299 | }
300 |
301 | interface ExportSettingsPDF {
302 | readonly format: "PDF"
303 | readonly contentsOnly?: boolean // defaults to true
304 | readonly suffix?: string
305 | }
306 |
307 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF
308 |
309 | type WindingRule = "NONZERO" | "EVENODD"
310 |
311 | interface VectorVertex {
312 | readonly x: number
313 | readonly y: number
314 | readonly strokeCap?: StrokeCap
315 | readonly strokeJoin?: StrokeJoin
316 | readonly cornerRadius?: number
317 | readonly handleMirroring?: HandleMirroring
318 | }
319 |
320 | interface VectorSegment {
321 | readonly start: number
322 | readonly end: number
323 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 }
324 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 }
325 | }
326 |
327 | interface VectorRegion {
328 | readonly windingRule: WindingRule
329 | readonly loops: ReadonlyArray>
330 | }
331 |
332 | interface VectorNetwork {
333 | readonly vertices: ReadonlyArray
334 | readonly segments: ReadonlyArray
335 | readonly regions?: ReadonlyArray // Defaults to []
336 | }
337 |
338 | interface VectorPath {
339 | readonly windingRule: WindingRule | "NONE"
340 | readonly data: string
341 | }
342 |
343 | type VectorPaths = ReadonlyArray
344 |
345 | interface LetterSpacing {
346 | readonly value: number
347 | readonly unit: "PIXELS" | "PERCENT"
348 | }
349 |
350 | type LineHeight = {
351 | readonly value: number
352 | readonly unit: "PIXELS" | "PERCENT"
353 | } | {
354 | readonly unit: "AUTO"
355 | }
356 |
357 | type BlendMode =
358 | "PASS_THROUGH" |
359 | "NORMAL" |
360 | "DARKEN" |
361 | "MULTIPLY" |
362 | "LINEAR_BURN" |
363 | "COLOR_BURN" |
364 | "LIGHTEN" |
365 | "SCREEN" |
366 | "LINEAR_DODGE" |
367 | "COLOR_DODGE" |
368 | "OVERLAY" |
369 | "SOFT_LIGHT" |
370 | "HARD_LIGHT" |
371 | "DIFFERENCE" |
372 | "EXCLUSION" |
373 | "HUE" |
374 | "SATURATION" |
375 | "COLOR" |
376 | "LUMINOSITY"
377 |
378 | interface Font {
379 | fontName: FontName
380 | }
381 |
382 | type Reaction = { action: Action, trigger: Trigger }
383 |
384 | type Action =
385 | { readonly type: "BACK" | "CLOSE" } |
386 | { readonly type: "URL", url: string } |
387 | { readonly type: "NODE"
388 | readonly destinationId: string | null
389 | readonly navigation: Navigation
390 | readonly transition: Transition | null
391 | readonly preserveScrollPosition: boolean
392 |
393 | // Only present if navigation == "OVERLAY" and the destination uses
394 | // overlay position type "RELATIVE"
395 | readonly overlayRelativePosition?: Vector
396 | }
397 |
398 | interface SimpleTransition {
399 | readonly type: "DISSOLVE" | "SMART_ANIMATE"
400 | readonly easing: Easing
401 | readonly duration: number
402 | }
403 |
404 | interface DirectionalTransition {
405 | readonly type: "MOVE_IN" | "MOVE_OUT" | "PUSH" | "SLIDE_IN" | "SLIDE_OUT"
406 | readonly direction: "LEFT" | "RIGHT" | "TOP" | "BOTTOM"
407 | readonly matchLayers: boolean
408 |
409 | readonly easing: Easing
410 | readonly duration: number
411 | }
412 |
413 | type Transition = SimpleTransition | DirectionalTransition
414 |
415 | type Trigger =
416 | { readonly type: "ON_CLICK" | "ON_HOVER" | "ON_PRESS" | "ON_DRAG" } |
417 | { readonly type: "AFTER_TIMEOUT", readonly timeout: number } |
418 | { readonly type: "MOUSE_ENTER" | "MOUSE_LEAVE" | "MOUSE_UP" | "MOUSE_DOWN"
419 | readonly delay: number
420 | }
421 |
422 | type Navigation = "NAVIGATE" | "SWAP" | "OVERLAY"
423 |
424 | interface Easing {
425 | readonly type: "EASE_IN" | "EASE_OUT" | "EASE_IN_AND_OUT" | "LINEAR"
426 | }
427 |
428 | type OverflowDirection = "NONE" | "HORIZONTAL" | "VERTICAL" | "BOTH"
429 |
430 | type OverlayPositionType = "CENTER" | "TOP_LEFT" | "TOP_CENTER" | "TOP_RIGHT" | "BOTTOM_LEFT" | "BOTTOM_CENTER" | "BOTTOM_RIGHT" | "MANUAL"
431 |
432 | type OverlayBackground =
433 | { readonly type: "NONE" } |
434 | { readonly type: "SOLID_COLOR", readonly color: RGBA }
435 |
436 | type OverlayBackgroundInteraction = "NONE" | "CLOSE_ON_CLICK_OUTSIDE"
437 |
438 | ////////////////////////////////////////////////////////////////////////////////
439 | // Mixins
440 |
441 | interface BaseNodeMixin {
442 | readonly id: string
443 | readonly parent: (BaseNode & ChildrenMixin) | null
444 | name: string // Note: setting this also sets `autoRename` to false on TextNodes
445 | readonly removed: boolean
446 | toString(): string
447 | remove(): void
448 |
449 | getPluginData(key: string): string
450 | setPluginData(key: string, value: string): void
451 |
452 | // Namespace is a string that must be at least 3 alphanumeric characters, and should
453 | // be a name related to your plugin. Other plugins will be able to read this data.
454 | getSharedPluginData(namespace: string, key: string): string
455 | setSharedPluginData(namespace: string, key: string, value: string): void
456 | setRelaunchData(data: { [command: string]: /* description */ string }): void
457 | }
458 |
459 | interface SceneNodeMixin {
460 | visible: boolean
461 | locked: boolean
462 | }
463 |
464 | interface ChildrenMixin {
465 | readonly children: ReadonlyArray
466 |
467 | appendChild(child: SceneNode): void
468 | insertChild(index: number, child: SceneNode): void
469 |
470 | findChildren(callback?: (node: SceneNode) => boolean): SceneNode[]
471 | findChild(callback: (node: SceneNode) => boolean): SceneNode | null
472 |
473 | /**
474 | * If you only need to search immediate children, it is much faster
475 | * to call node.children.filter(callback) or node.findChildren(callback)
476 | */
477 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[]
478 |
479 | /**
480 | * If you only need to search immediate children, it is much faster
481 | * to call node.children.find(callback) or node.findChild(callback)
482 | */
483 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null
484 | }
485 |
486 | interface ConstraintMixin {
487 | constraints: Constraints
488 | }
489 |
490 | interface LayoutMixin {
491 | readonly absoluteTransform: Transform
492 | relativeTransform: Transform
493 | x: number
494 | y: number
495 | rotation: number // In degrees
496 |
497 | readonly width: number
498 | readonly height: number
499 | constrainProportions: boolean
500 |
501 | layoutAlign: "MIN" | "CENTER" | "MAX" | "STRETCH" // applicable only inside auto-layout frames
502 |
503 | resize(width: number, height: number): void
504 | resizeWithoutConstraints(width: number, height: number): void
505 | }
506 |
507 | interface BlendMixin {
508 | opacity: number
509 | blendMode: BlendMode
510 | isMask: boolean
511 | effects: ReadonlyArray
512 | effectStyleId: string
513 | }
514 |
515 | interface ContainerMixin {
516 | expanded: boolean
517 | backgrounds: ReadonlyArray // DEPRECATED: use 'fills' instead
518 | backgroundStyleId: string // DEPRECATED: use 'fillStyleId' instead
519 | }
520 |
521 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL"
522 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND"
523 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH"
524 |
525 | interface GeometryMixin {
526 | fills: ReadonlyArray | PluginAPI['mixed']
527 | strokes: ReadonlyArray
528 | strokeWeight: number
529 | strokeMiterLimit: number
530 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE"
531 | strokeCap: StrokeCap | PluginAPI['mixed']
532 | strokeJoin: StrokeJoin | PluginAPI['mixed']
533 | dashPattern: ReadonlyArray
534 | fillStyleId: string | PluginAPI['mixed']
535 | strokeStyleId: string
536 | outlineStroke(): VectorNode | null
537 | }
538 |
539 | interface CornerMixin {
540 | cornerRadius: number | PluginAPI['mixed']
541 | cornerSmoothing: number
542 | }
543 |
544 | interface RectangleCornerMixin {
545 | topLeftRadius: number
546 | topRightRadius: number
547 | bottomLeftRadius: number
548 | bottomRightRadius: number
549 | }
550 |
551 | interface ExportMixin {
552 | exportSettings: ReadonlyArray
553 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format
554 | }
555 |
556 | interface ReactionMixin {
557 | readonly reactions: ReadonlyArray
558 | }
559 |
560 | interface DefaultShapeMixin extends
561 | BaseNodeMixin, SceneNodeMixin, ReactionMixin,
562 | BlendMixin, GeometryMixin, LayoutMixin,
563 | ExportMixin {
564 | }
565 |
566 | interface DefaultFrameMixin extends
567 | BaseNodeMixin, SceneNodeMixin, ReactionMixin,
568 | ChildrenMixin, ContainerMixin,
569 | GeometryMixin, CornerMixin, RectangleCornerMixin,
570 | BlendMixin, ConstraintMixin, LayoutMixin,
571 | ExportMixin {
572 |
573 | layoutMode: "NONE" | "HORIZONTAL" | "VERTICAL"
574 | counterAxisSizingMode: "FIXED" | "AUTO" // applicable only if layoutMode != "NONE"
575 | horizontalPadding: number // applicable only if layoutMode != "NONE"
576 | verticalPadding: number // applicable only if layoutMode != "NONE"
577 | itemSpacing: number // applicable only if layoutMode != "NONE"
578 |
579 | layoutGrids: ReadonlyArray
580 | gridStyleId: string
581 | clipsContent: boolean
582 | guides: ReadonlyArray
583 |
584 | overflowDirection: OverflowDirection
585 | numberOfFixedChildren: number
586 |
587 | readonly overlayPositionType: OverlayPositionType
588 | readonly overlayBackground: OverlayBackground
589 | readonly overlayBackgroundInteraction: OverlayBackgroundInteraction
590 | }
591 |
592 | ////////////////////////////////////////////////////////////////////////////////
593 | // Nodes
594 |
595 | interface DocumentNode extends BaseNodeMixin {
596 | readonly type: "DOCUMENT"
597 |
598 | readonly children: ReadonlyArray
599 |
600 | appendChild(child: PageNode): void
601 | insertChild(index: number, child: PageNode): void
602 | findChildren(callback?: (node: PageNode) => boolean): Array
603 | findChild(callback: (node: PageNode) => boolean): PageNode | null
604 |
605 | /**
606 | * If you only need to search immediate children, it is much faster
607 | * to call node.children.filter(callback) or node.findChildren(callback)
608 | */
609 | findAll(callback?: (node: PageNode | SceneNode) => boolean): Array
610 |
611 | /**
612 | * If you only need to search immediate children, it is much faster
613 | * to call node.children.find(callback) or node.findChild(callback)
614 | */
615 | findOne(callback: (node: PageNode | SceneNode) => boolean): PageNode | SceneNode | null
616 | }
617 |
618 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin {
619 |
620 | readonly type: "PAGE"
621 | clone(): PageNode
622 |
623 | guides: ReadonlyArray
624 | selection: ReadonlyArray
625 | selectedTextRange: { node: TextNode, start: number, end: number } | null
626 |
627 | backgrounds: ReadonlyArray
628 |
629 | readonly prototypeStartNode: FrameNode | GroupNode | ComponentNode | InstanceNode | null
630 | }
631 |
632 | interface FrameNode extends DefaultFrameMixin {
633 | readonly type: "FRAME"
634 | clone(): FrameNode
635 | }
636 |
637 | interface GroupNode extends
638 | BaseNodeMixin, SceneNodeMixin, ReactionMixin,
639 | ChildrenMixin, ContainerMixin, BlendMixin,
640 | LayoutMixin, ExportMixin {
641 |
642 | readonly type: "GROUP"
643 | clone(): GroupNode
644 | }
645 |
646 | interface SliceNode extends
647 | BaseNodeMixin, SceneNodeMixin, LayoutMixin,
648 | ExportMixin {
649 |
650 | readonly type: "SLICE"
651 | clone(): SliceNode
652 | }
653 |
654 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin, RectangleCornerMixin {
655 | readonly type: "RECTANGLE"
656 | clone(): RectangleNode
657 | }
658 |
659 | interface LineNode extends DefaultShapeMixin, ConstraintMixin {
660 | readonly type: "LINE"
661 | clone(): LineNode
662 | }
663 |
664 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin {
665 | readonly type: "ELLIPSE"
666 | clone(): EllipseNode
667 | arcData: ArcData
668 | }
669 |
670 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin {
671 | readonly type: "POLYGON"
672 | clone(): PolygonNode
673 | pointCount: number
674 | }
675 |
676 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin {
677 | readonly type: "STAR"
678 | clone(): StarNode
679 | pointCount: number
680 | innerRadius: number
681 | }
682 |
683 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin {
684 | readonly type: "VECTOR"
685 | clone(): VectorNode
686 | vectorNetwork: VectorNetwork
687 | vectorPaths: VectorPaths
688 | handleMirroring: HandleMirroring | PluginAPI['mixed']
689 | }
690 |
691 | interface TextNode extends DefaultShapeMixin, ConstraintMixin {
692 | readonly type: "TEXT"
693 | clone(): TextNode
694 | readonly hasMissingFont: boolean
695 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED"
696 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM"
697 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT"
698 | paragraphIndent: number
699 | paragraphSpacing: number
700 | autoRename: boolean
701 |
702 | textStyleId: string | PluginAPI['mixed']
703 | fontSize: number | PluginAPI['mixed']
704 | fontName: FontName | PluginAPI['mixed']
705 | textCase: TextCase | PluginAPI['mixed']
706 | textDecoration: TextDecoration | PluginAPI['mixed']
707 | letterSpacing: LetterSpacing | PluginAPI['mixed']
708 | lineHeight: LineHeight | PluginAPI['mixed']
709 |
710 | characters: string
711 | insertCharacters(start: number, characters: string, useStyle?: "BEFORE" | "AFTER"): void
712 | deleteCharacters(start: number, end: number): void
713 |
714 | getRangeFontSize(start: number, end: number): number | PluginAPI['mixed']
715 | setRangeFontSize(start: number, end: number, value: number): void
716 | getRangeFontName(start: number, end: number): FontName | PluginAPI['mixed']
717 | setRangeFontName(start: number, end: number, value: FontName): void
718 | getRangeTextCase(start: number, end: number): TextCase | PluginAPI['mixed']
719 | setRangeTextCase(start: number, end: number, value: TextCase): void
720 | getRangeTextDecoration(start: number, end: number): TextDecoration | PluginAPI['mixed']
721 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void
722 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | PluginAPI['mixed']
723 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void
724 | getRangeLineHeight(start: number, end: number): LineHeight | PluginAPI['mixed']
725 | setRangeLineHeight(start: number, end: number, value: LineHeight): void
726 | getRangeFills(start: number, end: number): Paint[] | PluginAPI['mixed']
727 | setRangeFills(start: number, end: number, value: Paint[]): void
728 | getRangeTextStyleId(start: number, end: number): string | PluginAPI['mixed']
729 | setRangeTextStyleId(start: number, end: number, value: string): void
730 | getRangeFillStyleId(start: number, end: number): string | PluginAPI['mixed']
731 | setRangeFillStyleId(start: number, end: number, value: string): void
732 | }
733 |
734 | interface ComponentNode extends DefaultFrameMixin {
735 | readonly type: "COMPONENT"
736 | clone(): ComponentNode
737 |
738 | createInstance(): InstanceNode
739 | description: string
740 | readonly remote: boolean
741 | readonly key: string // The key to use with "importComponentByKeyAsync"
742 | }
743 |
744 | interface InstanceNode extends DefaultFrameMixin {
745 | readonly type: "INSTANCE"
746 | clone(): InstanceNode
747 | masterComponent: ComponentNode
748 | scaleFactor: number
749 | }
750 |
751 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin {
752 | readonly type: "BOOLEAN_OPERATION"
753 | clone(): BooleanOperationNode
754 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE"
755 |
756 | expanded: boolean
757 | }
758 |
759 | type BaseNode =
760 | DocumentNode |
761 | PageNode |
762 | SceneNode
763 |
764 | type SceneNode =
765 | SliceNode |
766 | FrameNode |
767 | GroupNode |
768 | ComponentNode |
769 | InstanceNode |
770 | BooleanOperationNode |
771 | VectorNode |
772 | StarNode |
773 | LineNode |
774 | EllipseNode |
775 | PolygonNode |
776 | RectangleNode |
777 | TextNode
778 |
779 | type NodeType =
780 | "DOCUMENT" |
781 | "PAGE" |
782 | "SLICE" |
783 | "FRAME" |
784 | "GROUP" |
785 | "COMPONENT" |
786 | "INSTANCE" |
787 | "BOOLEAN_OPERATION" |
788 | "VECTOR" |
789 | "STAR" |
790 | "LINE" |
791 | "ELLIPSE" |
792 | "POLYGON" |
793 | "RECTANGLE" |
794 | "TEXT"
795 |
796 | ////////////////////////////////////////////////////////////////////////////////
797 | // Styles
798 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID"
799 |
800 | interface BaseStyle {
801 | readonly id: string
802 | readonly type: StyleType
803 | name: string
804 | description: string
805 | remote: boolean
806 | readonly key: string // The key to use with "importStyleByKeyAsync"
807 | remove(): void
808 | }
809 |
810 | interface PaintStyle extends BaseStyle {
811 | type: "PAINT"
812 | paints: ReadonlyArray
813 | }
814 |
815 | interface TextStyle extends BaseStyle {
816 | type: "TEXT"
817 | fontSize: number
818 | textDecoration: TextDecoration
819 | fontName: FontName
820 | letterSpacing: LetterSpacing
821 | lineHeight: LineHeight
822 | paragraphIndent: number
823 | paragraphSpacing: number
824 | textCase: TextCase
825 | }
826 |
827 | interface EffectStyle extends BaseStyle {
828 | type: "EFFECT"
829 | effects: ReadonlyArray
830 | }
831 |
832 | interface GridStyle extends BaseStyle {
833 | type: "GRID"
834 | layoutGrids: ReadonlyArray
835 | }
836 |
837 | ////////////////////////////////////////////////////////////////////////////////
838 | // Other
839 |
840 | interface Image {
841 | readonly hash: string
842 | getBytesAsync(): Promise
843 | }
844 | } // declare global
845 |
846 | export {}
847 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const generate = require("@babel/generator");
2 | const t = require("@babel/types");
3 |
4 | const ast = t.program([
5 | t.variableDeclaration("let", [t.variableDeclarator(t.identifier("a"))])
6 | ]);
7 |
8 | const output = generate.default(ast);
9 | console.log(output.code);
10 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Babel",
3 | "id": "745995778738837862",
4 | "api": "1.0.0",
5 | "main": "dist/code.js",
6 | "ui": "dist/ui.html"
7 | }
8 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "figma-code-gen",
4 | "env": {
5 | "NODE_ENV": "production"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "figma-code-gen",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "author": "Ilya Lesik ",
6 | "license": "MIT",
7 | "scripts": {
8 | "webpack:watch": "webpack --watch",
9 | "run:localy": "node index.js",
10 | "build": "webpack --config webpack.config.editor.js"
11 | },
12 | "husky": {
13 | "hooks": {
14 | "pre-commit": "pretty-quick --staged"
15 | }
16 | },
17 | "dependencies": {
18 | "@babel/core": "^7.8.7",
19 | "@babel/generator": "^7.8.7",
20 | "@babel/parser": "^7.8.7",
21 | "@babel/types": "^7.8.7",
22 | "figma-ui-components": "^0.0.8",
23 | "monaco-editor": "^0.20.0",
24 | "react": "^16.13.0",
25 | "react-dom": "^16.13.0",
26 | "react-monaco-editor": "^0.34.0",
27 | "styled-components": "^5.0.1"
28 | },
29 | "devDependencies": {
30 | "css-loader": "^3.4.2",
31 | "html-webpack-inline-source-plugin": "^0.0.10",
32 | "html-webpack-plugin": "^3.2.0",
33 | "husky": "^4.2.3",
34 | "monaco-editor-webpack-plugin": "^1.9.0",
35 | "prettier": "1.19.1",
36 | "pretty-quick": "^2.0.1",
37 | "react-figma-webpack-config": "^0.0.4",
38 | "style-loader": "^1.1.3",
39 | "ts-loader": "^6.2.1",
40 | "typescript": "^3.8.3",
41 | "url-loader": "^3.0.0",
42 | "webpack": "^4.42.0",
43 | "webpack-cli": "^3.3.11"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/code.tsx:
--------------------------------------------------------------------------------
1 | import { serialize } from "./serialize";
2 |
3 | figma.showUI(__html__, {
4 | height: 360,
5 | width: 600
6 | });
7 |
8 | figma.on("selectionchange", () => {
9 | figma.ui.postMessage(serialize(figma.currentPage.selection[0]));
10 | });
11 |
--------------------------------------------------------------------------------
/src/components/editor-layout/EditorLayout.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styled from "styled-components";
3 |
4 | const Container = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | height: 100%;
8 | `;
9 |
10 | const EditorContainer = styled.div`
11 | width: 100%;
12 | flex: 1;
13 | `;
14 |
15 | export const EditorLayout = props => {
16 | const { children, panel } = props;
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/editor.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
--------------------------------------------------------------------------------
/src/editor.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import MonacoEditor from "react-monaco-editor";
4 | import { EditorLayout } from "./components/editor-layout/EditorLayout";
5 | import { TypographyStyles, BaseStyles } from "figma-ui-components";
6 |
7 | const App = () => {
8 | const [code, setCode] = React.useState("");
9 | const monacoRef = React.useRef();
10 | const options = {
11 | selectOnLineNumbers: true
12 | };
13 |
14 | React.useEffect(() => {
15 | const setCode = evt => {
16 | console.log("evt", evt);
17 | if (!evt.data) {
18 | return;
19 | }
20 | setCode(evt.data);
21 |
22 | if (!monacoRef.current) {
23 | return;
24 | }
25 |
26 | // @ts-ignore
27 | const model = monacoRef.current.editor.getModel();
28 | model.setValue(evt.data);
29 | };
30 | window.addEventListener("message", setCode, false);
31 | return () => window.removeEventListener("message", setCode, false);
32 | }, []);
33 |
34 | return (
35 | <>
36 |
37 |
38 |
39 | setCode(newValue)}
48 | editorDidMount={editor => {
49 | editor.focus();
50 | }}
51 | />
52 |
53 | >
54 | );
55 | };
56 |
57 | ReactDOM.render(, document.getElementById("root"));
58 |
--------------------------------------------------------------------------------
/src/serialize.ts:
--------------------------------------------------------------------------------
1 | const availableProps = ["width", "height", "type"];
2 |
3 | export const serialize = node => {
4 | if (!node) {
5 | return;
6 | }
7 | const result = {} as any;
8 | availableProps.map(prop => {
9 | result[prop] = node[prop];
10 | });
11 |
12 | if (node.children) {
13 | result.children = node.children.map(serialize);
14 | }
15 |
16 | return result;
17 | };
18 |
--------------------------------------------------------------------------------
/src/transformers/snapshotToReactFigmaAst.ts:
--------------------------------------------------------------------------------
1 | import * as t from "@babel/types";
2 |
3 | export const snapshotToReactFigmaAst = snapshot => {
4 | const context = {
5 | reactFigmaImportSpecifiers: []
6 | };
7 | const ComponentNode = t.exportNamedDeclaration(
8 | t.variableDeclaration("const", [
9 | t.variableDeclarator(
10 | t.identifier("Component"),
11 | t.arrowFunctionExpression(
12 | [],
13 | t.blockStatement([
14 | t.returnStatement(convertToJsxElement(snapshot, context))
15 | ])
16 | )
17 | )
18 | ])
19 | );
20 |
21 | const reactImportDeclaration = t.importDeclaration(
22 | [t.importNamespaceSpecifier(t.identifier("React"))],
23 | t.stringLiteral("react")
24 | );
25 |
26 | const reactFigmaImportDeclaration =
27 | context.reactFigmaImportSpecifiers.length > 0 &&
28 | t.importDeclaration(
29 | context.reactFigmaImportSpecifiers.map(specifierName => {
30 | return t.importSpecifier(
31 | t.identifier(specifierName),
32 | t.identifier(specifierName)
33 | );
34 | }),
35 | t.stringLiteral("react-figma")
36 | );
37 |
38 | return t.program([
39 | reactImportDeclaration,
40 | ...(reactFigmaImportDeclaration ? [reactFigmaImportDeclaration] : []),
41 | ComponentNode
42 | ]);
43 | };
44 |
45 | const ElementToComponent = {
46 | COMPONENT: "Component",
47 | PAGE: "Page",
48 | FRAME: "View",
49 | GROUP: "View",
50 | SLICE: "Slice",
51 | RECTANGLE: "View",
52 | LINE: "Line",
53 | ELLIPSE: "Ellipse",
54 | POLYGON: "Polygon",
55 | STAR: "Star",
56 | VECTOR: "Vector",
57 | TEXT: "Text",
58 | INSTANCE: "Instance",
59 | BOOLEAN_OPERATION: "BooleanOperation"
60 | };
61 |
62 | const convertToAttributes = snapshot => {
63 | return Object.keys(snapshot)
64 | .filter(key => key !== "type" && key !== "children")
65 | .map(key =>
66 | t.jsxAttribute(
67 | t.jsxIdentifier(key),
68 | typeof snapshot[key] === "number" &&
69 | t.jsxExpressionContainer(t.numericLiteral(snapshot[key]))
70 | )
71 | );
72 | };
73 |
74 | const convertToJsxElement = (snapshot, context) => {
75 | const typeToElementName = type => {
76 | const result = ElementToComponent[type] || "View";
77 | if (context.reactFigmaImportSpecifiers.indexOf(result) < 0) {
78 | context.reactFigmaImportSpecifiers.push(result);
79 | }
80 | return result;
81 | };
82 | if (!snapshot.children || snapshot.children.length === 0) {
83 | return t.jsxElement(
84 | t.jsxOpeningElement(
85 | t.jsxIdentifier(typeToElementName(snapshot.type)),
86 | convertToAttributes(snapshot),
87 | true
88 | ),
89 | null,
90 | [],
91 | null
92 | );
93 | } else {
94 | return t.jsxElement(
95 | t.jsxOpeningElement(
96 | t.jsxIdentifier(typeToElementName(snapshot.type)),
97 | convertToAttributes(snapshot),
98 | false
99 | ),
100 | t.jsxClosingElement(t.jsxIdentifier(typeToElementName(snapshot.type))),
101 | snapshot.children.map(item => convertToJsxElement(item, context)),
102 | null
103 | );
104 | }
105 | };
106 |
--------------------------------------------------------------------------------
/src/ui.html:
--------------------------------------------------------------------------------
1 |
2 |
11 |
17 |
--------------------------------------------------------------------------------
/src/ui.tsx:
--------------------------------------------------------------------------------
1 | import generate from "@babel/generator";
2 | import { snapshotToReactFigmaAst } from "./transformers/snapshotToReactFigmaAst";
3 | import prettier from "prettier/standalone";
4 | import parserTypescript from "prettier/parser-typescript";
5 |
6 | const sendToIFrame = code => {
7 | const iframeEl = document.getElementById("editor-iframe");
8 | if (!iframeEl) {
9 | return;
10 | }
11 | const iframeWin = (iframeEl as HTMLIFrameElement).contentWindow;
12 | iframeWin.postMessage(code, "https://figma-code-gen.now.sh/");
13 | };
14 |
15 | onmessage = event => {
16 | console.log(event);
17 |
18 | const root = event.data && event.data.pluginMessage;
19 | const output = generate(snapshotToReactFigmaAst(root));
20 | console.log(output.code);
21 |
22 | const formattedCode = prettier.format(output.code, {
23 | parser: "typescript",
24 | plugins: [parserTypescript]
25 | });
26 |
27 | console.log(formattedCode);
28 |
29 | sendToIFrame(formattedCode);
30 | };
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "jsx": "react"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/webpack.config.editor.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 | const path = require("path");
4 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
5 | const APP_DIR = path.resolve(__dirname, "./src");
6 | const MONACO_DIR = path.resolve(__dirname, "./node_modules/monaco-editor");
7 |
8 | module.exports = (env, argv) => ({
9 | mode: argv.mode === "production" ? "production" : "development",
10 |
11 | // This is necessary because Figma's 'eval' works differently than normal eval
12 | devtool: argv.mode === "production" ? false : "inline-source-map",
13 |
14 | entry: {
15 | editor: "./src/editor.tsx" // The entry point for your UI code
16 | },
17 |
18 | module: {
19 | rules: [
20 | // Converts TypeScript code to JavaScript
21 | { test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/ },
22 |
23 | // Enables including CSS by doing "import './file.css'" in your TypeScript code
24 | {
25 | test: /\.css$/,
26 | include: APP_DIR,
27 | use: [
28 | {
29 | loader: "style-loader"
30 | },
31 | {
32 | loader: "css-loader",
33 | options: {
34 | modules: true,
35 | namedExport: true
36 | }
37 | }
38 | ]
39 | },
40 | {
41 | test: /\.css$/,
42 | include: MONACO_DIR,
43 | use: ["style-loader", "css-loader"]
44 | },
45 |
46 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI
47 | {
48 | test: /\.(png|jpg|gif|webp|svg|zip|ttf)$/,
49 | loader: [{ loader: "url-loader" }]
50 | }
51 | ]
52 | },
53 |
54 | // Webpack tries these extensions for you if you omit the extension like "import './file'"
55 | resolve: { extensions: [".tsx", ".ts", ".jsx", ".js"] },
56 |
57 | output: {
58 | filename: "[name].js",
59 | path: path.resolve(__dirname, "public") // Compile into a folder called "dist"
60 | },
61 |
62 | // Tells Webpack to generate "ui.html" and to inline "ui.ts" into it
63 | plugins: [
64 | new MonacoWebpackPlugin({
65 | // available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options
66 | languages: ["typescript", "javascript"]
67 | }),
68 | new HtmlWebpackPlugin({
69 | template: "./src/editor.html",
70 | filename: "editor.html",
71 | inlineSource: ".(js)$",
72 | chunks: ["editor"]
73 | }),
74 | new HtmlWebpackInlineSourcePlugin()
75 | ]
76 | });
77 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var configure = require('react-figma-webpack-config');
2 |
3 | module.exports = configure();
4 |
--------------------------------------------------------------------------------