Skip to content

Commit 03d2e4d

Browse files
committed
chore: initial commit
0 parents  commit 03d2e4d

23 files changed

+3461
-0
lines changed

.eslintignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/*
2+
*.js
3+
4+
# temp
5+
code.ts

.eslintrc.js

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const prettierConfig = require('./.prettierrc');
2+
3+
module.exports = {
4+
extends: [
5+
'plugin:react/recommended',
6+
'airbnb',
7+
'airbnb-typescript',
8+
'airbnb/hooks',
9+
'plugin:@typescript-eslint/recommended',
10+
'plugin:@typescript-eslint/recommended-requiring-type-checking',
11+
'plugin:prettier/recommended',
12+
],
13+
parserOptions: {
14+
project: './tsconfig.json',
15+
sourceType: 'module',
16+
ecmaVersion: 2018,
17+
ecmaFeatures: {
18+
jsx: true,
19+
},
20+
},
21+
settings: {
22+
'import/parsers': {
23+
'@typescript-eslint/parser': ['.ts', '.tsx'],
24+
},
25+
'import/resolver': {
26+
typescript: {},
27+
},
28+
react: {
29+
version: 'detect',
30+
},
31+
},
32+
plugins: [
33+
'@typescript-eslint/eslint-plugin',
34+
'react',
35+
'react-hooks',
36+
'import',
37+
'prettier'
38+
],
39+
rules: {
40+
'prettier/prettier': ['error', prettierConfig],
41+
'max-len': [
42+
'error',
43+
{
44+
code: 120,
45+
ignoreComments: true,
46+
ignoreUrls: true,
47+
ignoreTemplateLiterals: true,
48+
ignoreRegExpLiterals: true,
49+
}
50+
],
51+
'object-curly-newline': 'off',
52+
'class-methods-use-this': 'off',
53+
'import/prefer-default-export': 'off',
54+
'import/first': 'error',
55+
'import/newline-after-import': 'error',
56+
'import/no-duplicates': 'error',
57+
'import/order': [
58+
'error',
59+
{
60+
groups: [
61+
'external',
62+
'sibling',
63+
'parent',
64+
'internal',
65+
'builtin',
66+
'object',
67+
'type',
68+
'index',
69+
],
70+
'newlines-between': 'always',
71+
},
72+
],
73+
'@typescript-eslint/explicit-member-accessibility': 'off',
74+
'@typescript-eslint/explicit-function-return-type': 'off',
75+
'@typescript-eslint/explicit-module-boundary-types': 'off',
76+
'@typescript-eslint/no-explicit-any': 'off',
77+
'@typescript-eslint/ban-types': 'off',
78+
'@typescript-eslint/no-var-requires': 'off',
79+
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
80+
'@typescript-eslint/no-floating-promises': 'off',
81+
'linebreak-style': 'off',
82+
'no-underscore-dangle': ['error', { 'allow': ['_id', '__ENV'] }],
83+
'arrow-parens': ['error', 'always'],
84+
'no-void': ['error', { allowAsStatement: true }],
85+
'@typescript-eslint/no-use-before-define': 'off',
86+
'import/no-extraneous-dependencies': 'off',
87+
'react/jsx-props-no-spreading': 'off',
88+
'react/react-in-jsx-scope': 'off',
89+
'react/prop-types': 'off',
90+
'react/require-default-props': 'off',
91+
'react/function-component-definition': [
92+
'error',
93+
{
94+
namedComponents: 'arrow-function',
95+
unnamedComponents: 'arrow-function',
96+
},
97+
],
98+
'react/jsx-first-prop-new-line': ['warn', 'multiline'],
99+
'react/jsx-closing-bracket-location': [
100+
'warn',
101+
'tag-aligned',
102+
],
103+
'react/jsx-max-props-per-line': [
104+
'warn',
105+
{ maximum: 1, when: 'multiline' },
106+
],
107+
'react/jsx-indent-props': ['warn', 2],
108+
'no-param-reassign': ['error', { 'props': true, 'ignorePropertyModificationsForRegex': ['^figma'] }],
109+
'react/jsx-no-useless-fragment': 'off',
110+
'@typescript-eslint/unbound-method': 'off',
111+
},
112+
};

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Node
2+
*.log
3+
*.log.*
4+
node_modules
5+
6+
out/
7+
dist/
8+
code.js
9+
.DS_Store

.prettierrc.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
printWidth: 120,
3+
tabWidth: 2,
4+
singleQuote: true,
5+
trailingComma: 'all',
6+
semi: true,
7+
bracketSpacing: true,
8+
arrowParens: 'always',
9+
endOfLine: 'auto',
10+
singleAttributePerLine: true,
11+
};

README.md

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# `figma-plugin-react-template`
2+
3+
> A template for building Figma plugins with React and Typescript
4+
5+
## Getting started
6+
7+
1. Create a new repository from this template
8+
2. Clone the repository
9+
3. Install dependencies with `yarn`
10+
4. Change the `name` and `id` in `manifest.json`
11+
5. Start to building plugin with `yarn watch`
12+
6. Open Figma and create new file
13+
7. Click `Plugins > Development > Import from manifest...`
14+
8. Select your `manifest.json` file
15+
16+
Before using this template, you should read the [Figma Plugin API documentation](https://www.figma.com/plugin-docs/intro/).
17+
18+
## About this template
19+
20+
### Supported Targets
21+
22+
This template supports both of `Figma Design` and `Figjam`.
23+
24+
You can change the target by editing `manifest.json`
25+
26+
### `vite`
27+
28+
This template uses [vite](https://vitejs.dev/) as a bundler.
29+
30+
### `src/core`
31+
32+
This directory contains the core logic of the plugin.
33+
34+
it has dependency injection with [tsyringe](https://github.com/microsoft/tsyringe).
35+
36+
If you want to add a new command, you should add a new file in `src/core/commands` directory like below.
37+
38+
```typescript
39+
import { injectable } from 'tsyringe';
40+
41+
import { Command } from './Command';
42+
43+
import { SupportedEnvironments } from '@core/types/Environments';
44+
import { type CommandPayload } from '@core/types/FigmaUIMessage';
45+
46+
export interface DrawRectanglesPayload extends CommandPayload {
47+
count: number;
48+
}
49+
50+
@injectable()
51+
export class FigmaDrawRectangles implements Command<DrawRectanglesPayload> {
52+
type = 'DrawRectangles';
53+
54+
supportedEnvironments = [SupportedEnvironments.FIGMA];
55+
56+
execute(figma: PluginAPI, payload: DrawRectanglesPayload): void {
57+
const nodes: SceneNode[] = Array(payload.count)
58+
.fill(0)
59+
.map((_, i) => {
60+
const rect = figma.createRectangle();
61+
rect.x = i * 150;
62+
rect.fills = [{ type: 'SOLID', color: { r: 1, g: 0.5, b: 0 } }];
63+
return rect;
64+
});
65+
66+
nodes.forEach((node) => {
67+
figma.currentPage.appendChild(node);
68+
});
69+
70+
figma.currentPage.selection = nodes;
71+
figma.viewport.scrollAndZoomIntoView(nodes);
72+
figma.closePlugin();
73+
}
74+
75+
validatePayload(payload: DrawRectanglesPayload): boolean {
76+
return typeof payload.count === 'number';
77+
}
78+
}
79+
```
80+
81+
and register your command to `CommandsModule` in `src/core/commands/index.ts`.
82+
83+
```typescript
84+
import { registry } from 'tsyringe';
85+
86+
import { FigjamDrawRectangles } from './FigjamDrawRectangles';
87+
import { FigmaDrawRectangles } from './FigmaDrawRectangles';
88+
89+
@registry([
90+
{ token: CommandsModule.token, useToken: FigmaDrawRectangles },
91+
{ token: CommandsModule.token, useToken: FigjamDrawRectangles },
92+
])
93+
export abstract class CommandsModule {
94+
static readonly token = Symbol('Commands');
95+
}
96+
```
97+
98+
Done!
99+
Then, you can use your command in `src/ui/commands/index.ts`.
100+
101+
```typescript
102+
...
103+
window.parent.postMessage({ pluginMessage: { type: 'DrawRectangles', payload: { count } } }, '*');
104+
```
105+
106+
#### To-do
107+
108+
- [ ] Make `window.parent.postMessage` type-safe
109+
110+
### `src/ui`
111+
112+
This directory contains the UI of the plugin.
113+
114+
It uses [react](https://reactjs.org/). with Typescript.
115+
116+
you can build your custom UI in `src/ui/components` directory.
117+
118+
Check `src/ui/components/App.tsx` for example.
119+
120+
## Build
121+
122+
```bash
123+
yarn build
124+
```

index.html

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<div id="ui" />
2+
<script type="module" src="./src/ui/index.tsx"></script>

manifest.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "Figma Plugin React Template",
3+
"id": "0000000000000000000",
4+
"api": "1.0.0",
5+
"main": "dist/core.js",
6+
"ui": "dist/index.html",
7+
"capabilities": [],
8+
"enableProposedApi": false,
9+
"editorType": [
10+
"figma",
11+
"figjam"
12+
]
13+
}

package.json

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "figma-plugin-react-template",
3+
"version": "1.0.0",
4+
"description": "Template for Figma plugin with React and Typescript",
5+
"main": "dist/index.js",
6+
"scripts": {
7+
"build:ui": "cross-env TARGET=ui NODE_ENV=\"production\" vite build",
8+
"watch:ui": "cross-env TARGET=ui NODE_ENV=\"development\" vite build --watch",
9+
"build:core": "cross-env TARGET=core NODE_ENV=\"production\" vite build",
10+
"watch:core": "cross-env TARGET=core NODE_ENV=\"development\" vite build --watch",
11+
"build": "concurrently \"yarn:build:*\"",
12+
"watch": "concurrently \"yarn:watch:*\"",
13+
"lint": "eslint --ext .ts,.tsx .",
14+
"lint:fix": "yarn lint --fix"
15+
},
16+
"author": "beomjungil <[email protected]>",
17+
"license": "",
18+
"devDependencies": {
19+
"@figma/plugin-typings": "*",
20+
"@rollup/plugin-typescript": "^11.0.0",
21+
"@types/node": "^18.14.1",
22+
"@types/react": "^18.0.28",
23+
"@types/react-dom": "^18.0.11",
24+
"@typescript-eslint/eslint-plugin": "^5.53.0",
25+
"@typescript-eslint/parser": "^5.53.0",
26+
"@vitejs/plugin-react": "^3.1.0",
27+
"concurrently": "^7.6.0",
28+
"cross-env": "^7.0.3",
29+
"eslint": "^8.34.0",
30+
"eslint-config-airbnb": "^19.0.4",
31+
"eslint-config-airbnb-typescript": "^17.0.0",
32+
"eslint-config-prettier": "^8.6.0",
33+
"eslint-import-resolver-typescript": "^3.5.3",
34+
"eslint-plugin-import": "^2.27.5",
35+
"eslint-plugin-jsx-a11y": "^6.7.1",
36+
"eslint-plugin-prettier": "^4.2.1",
37+
"eslint-plugin-react": "^7.32.2",
38+
"eslint-plugin-react-hooks": "^4.6.0",
39+
"prettier": "^2.8.4",
40+
"typescript": "*",
41+
"vite": "^4.1.4",
42+
"vite-plugin-singlefile": "^0.13.3"
43+
},
44+
"dependencies": {
45+
"height-app-api": "^1.0.5",
46+
"react": "^18.2.0",
47+
"react-dom": "^18.2.0",
48+
"reflect-metadata": "^0.1.13",
49+
"tsyringe": "^4.7.0"
50+
}
51+
}

src/core/base/Plugin.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { injectAll, singleton } from 'tsyringe';
2+
3+
import { SupportedEnvironments } from '@core/types/Environments';
4+
import { CommandsModule } from '@core/commands';
5+
import { Command } from '@core/commands/Command';
6+
import { FigmaUIMessage } from '@core/types/FigmaUIMessage';
7+
8+
@singleton()
9+
export class FigmaPlugin {
10+
private figma: PluginAPI = figma;
11+
12+
private commands: Command[];
13+
14+
constructor(@injectAll(CommandsModule.token) commands: Command[]) {
15+
this.commands = commands;
16+
}
17+
18+
start() {
19+
this.figma.showUI(__html__);
20+
this.figma.ui.onmessage = (message) => {
21+
this.handleMessage(message as unknown as FigmaUIMessage);
22+
};
23+
}
24+
25+
private handleMessage(message: FigmaUIMessage) {
26+
const command = this.commands.find(({ type, supportedEnvironments }) => {
27+
return message.type === type && supportedEnvironments.includes(figma.editorType as SupportedEnvironments);
28+
});
29+
30+
if (!command) {
31+
this.figma.notify(`Can't execute ${message.type}: command not found`, { error: true });
32+
return;
33+
}
34+
35+
if (command.validatePayload && !command.validatePayload(message.payload)) {
36+
this.figma.notify(`Can't execute ${command.type}: invalid payload`, {
37+
error: true,
38+
button: {
39+
text: 'Submit a bug report',
40+
action: () => {
41+
const url = 'https://example.com';
42+
const openLinkUIString = `<script>window.open('${url}','_blank');</script>`;
43+
44+
figma.showUI(openLinkUIString, { visible: false });
45+
setTimeout(figma.closePlugin, 1000);
46+
},
47+
},
48+
});
49+
return;
50+
}
51+
52+
command.execute(figma, message.payload);
53+
}
54+
}

src/core/bootstrap.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { container } from 'tsyringe';
2+
3+
import { FigmaPlugin } from './base/Plugin';
4+
5+
const plugin = container.resolve(FigmaPlugin);
6+
7+
plugin.start();

0 commit comments

Comments
 (0)