├── custom_typings ├── dotenv-webpack │ └── index.d.ts ├── html-webpack-plugin │ └── index.d.ts ├── webpack-bundle-analyzer │ └── index.d.ts ├── html-webpack-inline-source-plugin │ └── index.d.ts └── tsconfig-paths-webpack-plugin │ └── index.d.ts ├── src ├── ui │ ├── app │ │ ├── bg.png │ │ ├── bg@x2.png │ │ ├── app.css.d.ts │ │ ├── demo.worker.ts │ │ ├── app.css │ │ └── app.tsx │ ├── index.html │ └── index.tsx ├── shared │ ├── debug.ts │ ├── utils.ts │ └── types.ts └── main │ └── index.ts ├── .prettierrc ├── manifest.json ├── tslint.json ├── .gitignore ├── .vscode └── settings.json ├── tsconfig.json ├── README.md ├── LICENSE ├── package.json ├── webpack.config.ts └── logos.svg /custom_typings/dotenv-webpack/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'dotenv-webpack' 2 | -------------------------------------------------------------------------------- /custom_typings/html-webpack-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'html-webpack-plugin' 2 | -------------------------------------------------------------------------------- /custom_typings/webpack-bundle-analyzer/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'webpack-bundle-analyzer' 2 | -------------------------------------------------------------------------------- /src/ui/app/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okotoki/figma-plugin-starter/HEAD/src/ui/app/bg.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /src/ui/app/bg@x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okotoki/figma-plugin-starter/HEAD/src/ui/app/bg@x2.png -------------------------------------------------------------------------------- /custom_typings/html-webpack-inline-source-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'html-webpack-inline-source-plugin' 2 | -------------------------------------------------------------------------------- /src/shared/debug.ts: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug' 2 | 3 | if (process.env.DEBUG) { 4 | debug.enable('*') 5 | } 6 | -------------------------------------------------------------------------------- /custom_typings/tsconfig-paths-webpack-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'tsconfig-paths-webpack-plugin' { 2 | const e: any 3 | export = e 4 | } 5 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Figma Plugin Starter", 3 | "id": "777232730725597330", 4 | "api": "1.0.0", 5 | "main": "dist/main.js", 6 | "ui": "dist/ui.html", 7 | "enableProposedApi": true 8 | } 9 | -------------------------------------------------------------------------------- /src/ui/app/app.css.d.ts: -------------------------------------------------------------------------------- 1 | declare const styles: { 2 | readonly "container": string; 3 | readonly "header": string; 4 | readonly "items": string; 5 | readonly "logo": string; 6 | }; 7 | export = styles; 8 | 9 | -------------------------------------------------------------------------------- /src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * /shared – contains shared code for both iframe (/ui) and main thread sandbox (/main) environments 3 | */ 4 | 5 | export function area(a: number, b: number) { 6 | return a * b 7 | } 8 | -------------------------------------------------------------------------------- /src/ui/index.html: -------------------------------------------------------------------------------- 1 | 12 |
13 | -------------------------------------------------------------------------------- /src/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import 'shared/debug' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | 6 | import { App } from './app/app' 7 | 8 | ReactDOM.render(, document.getElementById('app')) 9 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-config-prettier", 5 | "tslint-plugin-prettier" 6 | ], 7 | "rules": { 8 | "object-literal-sort-keys": false, 9 | "array-type": false, 10 | "no-shadowed-variable": false, 11 | "no-console": false, 12 | "interface-name": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ui/app/demo.worker.ts: -------------------------------------------------------------------------------- 1 | import 'shared/debug' 2 | 3 | import * as debug from 'debug' 4 | 5 | const log = debug('[Worker]') 6 | 7 | const ctx: Worker = self as any 8 | 9 | ctx.addEventListener('message', event => { 10 | log('received message', event.data) 11 | 12 | ctx.postMessage('hey from worker. Message received!') 13 | }) 14 | 15 | export default (null as any) as typeof Worker 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Coverage directory used by tools like istanbul 9 | coverage 10 | 11 | # nyc test coverage 12 | .nyc_output 13 | 14 | # Dependency directories 15 | node_modules/ 16 | 17 | # Optional npm cache directory 18 | .npm 19 | 20 | # Yarn Integrity file 21 | .yarn-integrity 22 | 23 | # dotenv environment variables file 24 | .env 25 | 26 | # Dist 27 | dist 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.detectIndentation": false, 4 | "files.insertFinalNewline": true, 5 | "files.trimTrailingWhitespace": true, 6 | "[markdown]": { 7 | "files.trimTrailingWhitespace": false 8 | }, 9 | "editor.insertSpaces": true, 10 | "[typescript]": { 11 | "editor.formatOnSave": true 12 | }, 13 | "[typescriptreact]": { 14 | "editor.formatOnSave": true 15 | }, 16 | "typescript.tsdk": "node_modules/typescript/lib", 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/types.ts: -------------------------------------------------------------------------------- 1 | export interface Layers { 2 | name: string 3 | id: string 4 | } 5 | 6 | /** 7 | * Messages to be sent from Iframe side, listeners on Main Thread side. 8 | */ 9 | export interface IframeToMain { 10 | unsubscribeFromIframeMessages(): void 11 | subscribeToIframeMessages(): void 12 | heyFromIframe(msg: string): void 13 | } 14 | 15 | /** 16 | * Messages to be sent from Main Thread side, listeners on Iframe side. 17 | */ 18 | export interface MainToIframe { 19 | selectionChanged(els: Layers[]): void 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "declarationDir": "dist", 5 | "target": "es6", 6 | "lib": ["es2016", "dom"], 7 | "jsx": "react", 8 | "strict": true, 9 | "declaration": true, 10 | "experimentalDecorators": true, 11 | "allowSyntheticDefaultImports": true, 12 | "typeRoots": ["node_modules/@types", "./custom_typings"], 13 | "moduleResolution": "node", 14 | "skipLibCheck": true, 15 | "baseUrl": "./src" 16 | }, 17 | "exclude": ["dist", "node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { createMainThreadMessenger } from 'figma-messenger' 2 | import { IframeToMain, MainToIframe } from 'shared/types' 3 | 4 | // This shows the HTML page in "ui.html". 5 | figma.showUI(__html__, { 6 | width: 400, 7 | height: 300 8 | }) 9 | 10 | const messenger = createMainThreadMessenger() 11 | 12 | const sendSelection = (selection: readonly SceneNode[]) => { 13 | const sel = selection.map(({ name, id }) => ({ 14 | name, 15 | id 16 | })) 17 | 18 | // Send current selection to Iframe. 19 | messenger.send('selectionChanged', sel) 20 | } 21 | 22 | sendSelection(figma.currentPage.selection) 23 | 24 | figma.on('selectionchange', () => sendSelection(figma.currentPage.selection)) 25 | -------------------------------------------------------------------------------- /src/ui/app/app.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | padding: 5px 20px; 5 | font-family: Helvetica Neue, Helvetica, sans-serif; 6 | font-size: 13px; 7 | line-height: 1.5; 8 | } 9 | 10 | .header { 11 | text-align: center; 12 | } 13 | 14 | .logo { 15 | display: inline-block; 16 | width: 64px; 17 | height: 64px; 18 | border-radius: 32px; 19 | background-image: url('./bg.png'); 20 | } 21 | 22 | @media 23 | (-webkit-min-device-pixel-ratio: 2), 24 | (min-resolution: 192dpi) { 25 | .logo { 26 | background-image: url('./bg@x2.png'); 27 | background-size: 64px 64px; 28 | } 29 | } 30 | 31 | .items { 32 | color: #3a3a3a 33 | } 34 | 35 | .items div { 36 | padding: 4px 0; 37 | } 38 | 39 | .items b{ 40 | float: right; 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Yet another Figma plugin starter 3 | 4 | ## Basics 5 | **Typescript** – the best JavaScript to date. 6 | **React** – renders stuff, so you don't have to. 7 | **Webpack** – bundles it all together. 8 | 9 | ## More goodies 10 | **workers** – heavy computation? Try using Webworkers! Check [demo.worker.ts](src/ui/app/demo.worker.ts) and [app.tsx](src/ui/app/app.tsx). Workers MUST be inlined due to Figma plugins system design, so be size-aware. 11 | **[debug](https://www.npmjs.com/package/debug)** – smarter `console.log`. Can be switched on/off via `process.env.DEBUG`. Default: `on` in dev mode, `off` on prod. 12 | **[figma-messenger](https://github.com/okotoki/figma-messenger)** – helper utility for type-safe communication between iframe and main thread. 13 | 14 | ## Usage 15 | Clone, copy/paste and run `yarn dev` or `yarn build` for production build. 16 | 17 | ## License 18 | BSD-3 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Eugene C 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-plugin-starter", 3 | "version": "0.0.1", 4 | "description": "Yet another Figma plugin starter", 5 | "repository": "https://github.com/okotoki/figma-plugin-starter", 6 | "author": "Opudalo", 7 | "license": "BSD-3", 8 | "private": true, 9 | "scripts": { 10 | "dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --watch", 11 | "build": "NODE_ENV=production tsc && webpack --mode=production" 12 | }, 13 | "dependencies": { 14 | "@types/debug": "^4.1.5", 15 | "@types/figma": "^1.0.3", 16 | "@types/react": "^16.9.36", 17 | "@types/react-dom": "^16.9.8", 18 | "debug": "^4.1.1", 19 | "figma-messenger": "^1.0.5", 20 | "react": "^16.13.1", 21 | "react-dom": "^16.13.1" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^14.0.13", 25 | "@types/webpack": "^4.41.17", 26 | "cross-env": "^7.0.2", 27 | "css-loader": "^3.5.3", 28 | "dotenv-webpack": "^1.8.0", 29 | "html-webpack-inline-source-plugin": "^0.0.10", 30 | "html-webpack-plugin": "^3.2.0", 31 | "prettier": "^2.0.5", 32 | "raw-loader": "^4.0.1", 33 | "style-loader": "^1.2.1", 34 | "ts-loader": "^7.0.5", 35 | "ts-node": "^8.10.2", 36 | "tsconfig-paths-webpack-plugin": "^3.2.0", 37 | "tslint": "^6.1.2", 38 | "tslint-config-prettier": "^1.18.0", 39 | "tslint-plugin-prettier": "^2.3.0", 40 | "typed-css-modules-webpack-plugin": "^0.1.3", 41 | "typescript": "^3.9.5", 42 | "url-loader": "^4.1.0", 43 | "webpack": "^4.43.0", 44 | "webpack-bundle-analyzer": "^3.8.0", 45 | "webpack-cli": "^3.3.11", 46 | "worker-loader": "^2.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/app/app.tsx: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug' 2 | import { createIframeMessenger } from 'figma-messenger' 3 | import * as React from 'react' 4 | import { IframeToMain, Layers, MainToIframe } from 'shared/types' 5 | 6 | import * as styles from './app.css' 7 | import DemoWorker from './demo.worker' 8 | 9 | const log = debug('[App]') 10 | 11 | const demoWorker = new DemoWorker(undefined as any) 12 | 13 | demoWorker.postMessage('hey worker!') 14 | demoWorker.onmessage = data => { 15 | log('message from worker received', data) 16 | } 17 | 18 | const messenger = createIframeMessenger() 19 | 20 | export const App = () => { 21 | const [layers, setLayers] = React.useState([]) 22 | 23 | React.useEffect(() => { 24 | // Listen for SelectionChanged message 25 | messenger.on('selectionChanged', els => { 26 | log('Message received from Main Thread.', els) 27 | if (!!els) { 28 | setLayers(els) 29 | } 30 | }) 31 | 32 | // unsubscribing all handlers. 33 | return () => messenger.off('selectionChanged') 34 | }, []) 35 | 36 | return ( 37 |
38 |
39 | 40 |
41 | {!!layers.length ? ( 42 |
43 |
44 | Layer Name 45 | Id 46 |
47 | {layers.map((x, i) => ( 48 |
49 | {x.name} 50 | {x.id} 51 |
52 | ))} 53 |
54 | ) : ( 55 |

Select one or multiple elements

56 | )} 57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as Dotenv from 'dotenv-webpack' 2 | import * as HtmlWebpackInlineSourcePlugin from 'html-webpack-inline-source-plugin' 3 | import * as HtmlWebpackPlugin from 'html-webpack-plugin' 4 | import * as path from 'path' 5 | import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' 6 | import { TypedCssModulesPlugin } from 'typed-css-modules-webpack-plugin' 7 | import * as webpack from 'webpack' 8 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' 9 | 10 | const createConfig = ( 11 | _: any, 12 | argv: webpack.Configuration 13 | ): webpack.Configuration => { 14 | const isProd = argv.mode === 'production' 15 | return { 16 | mode: argv.mode, 17 | devtool: false, 18 | stats: 'minimal', 19 | entry: { 20 | ui: './src/ui/index.tsx', // The entry point for your UI code 21 | main: './src/main/index.ts' 22 | }, 23 | output: { 24 | path: path.resolve(__dirname, 'dist'), 25 | filename: '[name].js' 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.worker\.ts$/, 31 | loader: [ 32 | { 33 | loader: 'worker-loader', 34 | options: { inline: true } 35 | } 36 | ] 37 | }, 38 | { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }, 39 | { 40 | test: /\.css$/, 41 | loader: [ 42 | { 43 | loader: 'style-loader' 44 | }, 45 | { 46 | loader: 'css-loader', 47 | options: { 48 | modules: true 49 | } 50 | } 51 | ] 52 | }, 53 | { 54 | test: /\.(png|jpg|gif|webp|svg)$/, 55 | loader: [{ loader: 'url-loader' }] 56 | } 57 | ] 58 | }, 59 | resolve: { 60 | extensions: ['.tsx', '.ts', '.jsx', '.js', '.css'], 61 | plugins: [new TsconfigPathsPlugin()] 62 | }, 63 | plugins: [ 64 | isProd 65 | ? new BundleAnalyzerPlugin() 66 | : () => { 67 | /* */ 68 | }, 69 | new webpack.EnvironmentPlugin({ 70 | DEBUG: !isProd 71 | }), 72 | new Dotenv(), 73 | new TypedCssModulesPlugin({ 74 | globPattern: 'src/**/*.css' 75 | }), 76 | new HtmlWebpackPlugin({ 77 | template: './src/ui/index.html', 78 | filename: 'ui.html', 79 | inlineSource: '.(js)$', 80 | chunks: ['ui'] 81 | }), 82 | new HtmlWebpackInlineSourcePlugin() 83 | ] 84 | } 85 | } 86 | 87 | export default createConfig 88 | -------------------------------------------------------------------------------- /logos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------