├── 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 | #
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 |
27 |
--------------------------------------------------------------------------------