├── .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 | --------------------------------------------------------------------------------