├── .eslintrc.json ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── README.md ├── manifest.json ├── package.json ├── src ├── code.ts ├── figma │ ├── index.ts │ ├── page.ts │ └── storage.ts ├── pages │ ├── about.tsx │ ├── home.tsx │ └── index.tsx ├── types.ts ├── ui.html ├── ui.tsx └── utils │ └── event.ts ├── tsconfig.json ├── typings.d.ts └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true 5 | }, 6 | "plugins": ["@typescript-eslint", "react"], 7 | "settings": { 8 | "react": { 9 | "version": "detect" 10 | } 11 | }, 12 | "extends": [ 13 | "standard", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:react/recommended", 16 | "plugin:react-hooks/recommended", 17 | "prettier" 18 | ], 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "ecmaFeatures": { 22 | "jsx": true 23 | }, 24 | "ecmaVersion": 11, 25 | "sourceType": "module" 26 | }, 27 | "rules": { 28 | "comma-dangle": ["error", "always-multiline"], 29 | "no-async-promise-executor": ["off"], 30 | "no-use-before-define": ["off"], 31 | 32 | "react/display-name": ["off"], 33 | "react/prop-types": ["off"], 34 | "react/react-in-jsx-scope": "error", 35 | 36 | "@typescript-eslint/no-explicit-any": ["off"], 37 | "@typescript-eslint/no-non-null-assertion": ["off"], 38 | "@typescript-eslint/explicit-module-boundary-types": ["off"], 39 | "@typescript-eslint/ban-ts-comment": ["off"], 40 | "@typescript-eslint/no-use-before-define": ["error"] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | .eslintcache 4 | package-lock.json 5 | yarn.lock 6 | *.zip -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix="~" 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/* -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "htmlWhitespaceSensitivity": "ignore", 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma Plugin Sample React Hot Reload 2 | 3 | 支持 React(部分)热更新的 Figma 插件 demo 4 | 5 | ## 功能介绍 6 | 7 | 这个 Demo 用于开发 Figma 插件,支持了如下的功能: 8 | 9 | - [x] React 10 | - [x] Webpack 11 | - [x] WebpackDevServer 12 | - [x] React Hot Reload 13 | - [x] React Router 14 | - [x] React Hooks 15 | - [x] 便捷的消息通信机制 16 | 17 | 更多关于此仓库的介绍,请移步到我的博客文章 [Figma 插件开发浅浅谈](https://banyudu.com/posts/figma-plugin-development-intro.f7472f?v=OKGW6q)。 18 | 19 | ## 使用说明 20 | 21 | ### 安装依赖 22 | 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | ### 启动调试 28 | 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | ### 功能开发 34 | 35 | #### 目录约定 36 | 37 | 功能开发主要需要修改两处位置。 38 | 39 | - src/figma: 存储 Figma 沙箱中运行的代码,不可以使用 window / fetch 等各种 api,但是可以访问 figma 文档结构。 40 | - src/pages: React 开发的页面,和 create-react-app 创建的项目差别不大,可以正常使用 react-router (只能是 memory router) 和 antd 组件库等常用库。 41 | 42 | #### 内部通信 43 | 44 | 当 pages 和 figma 中需要通信时,需要用到 postMessage 在两个窗口间通信,比较繁琐。 45 | 46 | 我提取了一个帮助工具 `emitToFigma`,可以用于在 pages 中发消息给 figma,并获得其响应。 47 | 48 | 使用方法: 49 | 50 | 首先,在 src/figma 中定义 actions,参见 `src/figma/index.ts` 51 | 52 | ```typescript 53 | // 如果添加了新的文件,需要在上面导入,并在actions中加上相应的变量名 54 | const actions: Record> = { 55 | storage, 56 | page, 57 | } 58 | ``` 59 | 60 | 然后在相应的文件,如 `src/figma/storage` 中定义方法,如 `get` 61 | 62 | 之后在 `src/pages` 中的任何位置,可以通过 `await emitToFigma('storage.get', params)` 的方式调用此方法并获取响应值。 63 | 64 | ### 打包发布 65 | 66 | 执行命令 67 | 68 | ```bash 69 | npm run pack 70 | ``` 71 | 72 | 它会在 `dist` 目录中生成一个可供生产环境使用的 `ui.html`、`ui.js`、`code.js` 文件。 73 | 74 | 然后将 `manifest.json` 等必要的文件与 `dist` 目录一起打包成 `figma-plugin.zip` 文件。 75 | 76 | 之后可以内部分享 `figma-plugin.zip` 文件给其他人(无需发布到市场,但需要用开发者模式导入),或者直接发布到 Figma 插件市场。 77 | 78 | ## 避坑指南 79 | 80 | Figma 插件,受限于其运行时环境,和普通的前端开发略有不同。 81 | 82 | 主要的区别是有些 API 或者库不再可用,列举如下: 83 | 84 | ### 沙箱环境中的代码可以访问 Figma,不能访问 window 等全局变量和 api 85 | 86 | `src/figma` 中的代码,运行在沙箱环境中,基本上只能用 Figma 的 api,无法使用大部分的 window / fetch 等 api. 87 | 88 | ### Iframe 中的代码,可以访问 window / fetch 等常见的前端全局变量和 API,但略有不同 89 | 90 | `src/pages` 中的代码,运行在 iframe 之中,但是它的 src 并不是一个远程地址(也没有 localhost 地址可用),而是一段 dataurl,形如 `data:text/html;base64,` 这样的地址。 91 | 92 | 这就导致它不能正常使用 `BrowserRouter` 中的前进后退等能力,无法使用 `` 标签进行页面导航,而只能使用 `MemoryRouter` 和 `history.push` 这样的内存路由进行页面导航。 93 | 94 | 同时也会导致它不能正常使用 `localStorage` 等存储类的 api,而必须借助 `src/figma` 中的 `figma.clientStorage.getAsync` 和 `figma.clientStorage.setAsync` 等方法进行数据持久化存储。 95 | 96 | 更多原理介绍,请移步到我的博客文章 [Figma 插件开发浅浅谈](https://banyudu.com/posts/figma-plugin-development-intro.f7472f?v=OKGW6q)。 97 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React-Hot-Reload", 3 | "id": "1029157888727225342", 4 | "api": "1.0.0", 5 | "editorType": ["figma"], 6 | "main": "dist/code.js", 7 | "ui": "dist/ui.html" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-plugin-sample-react-hot-reload", 3 | "version": "1.0.0", 4 | "description": "Your Figma plugin", 5 | "main": "code.js", 6 | "scripts": { 7 | "clean": "rimraf dist", 8 | "dev:plugin": "cross-env NODE_ENV=development webpack --mode=development --watch", 9 | "dev:ui": "cross-env NODE_ENV=development webpack serve --mode=development", 10 | "dev": "run-p dev:plugin dev:ui", 11 | "dev:mock": "cross-env MOCK=true npm run dev", 12 | "build": "npm run clean && cross-env NODE_ENV=production webpack --mode=production", 13 | "analyze": "cross-env NODE_ENV=production ANALYZE=true webpack --mode=production", 14 | "pretty": "prettier --write './**/*.(js|ts|jsx|tsx|md|html)'", 15 | "pack": "rimraf figma-plugin.zip && npm run build && cd dist && bestzip ../figma-plugin.zip *", 16 | "prepare": "husky install" 17 | }, 18 | "author": "", 19 | "license": "", 20 | "devDependencies": { 21 | "@babel/plugin-transform-runtime": "~7.15.0", 22 | "@figma/plugin-typings": "^1.28.0", 23 | "@types/lodash": "~4.14.172", 24 | "@types/react": "~17.0.15", 25 | "@types/react-dom": "~17.0.9", 26 | "@types/uuid": "~8.3.1", 27 | "@typescript-eslint/eslint-plugin": "~4.29.0", 28 | "@typescript-eslint/parser": "~4.29.0", 29 | "babel-loader": "~8.2.2", 30 | "babel-plugin-import": "~1.13.3", 31 | "babel-plugin-named-asset-import": "~0.3.7", 32 | "babel-preset-react-app": "~10.0.0", 33 | "bestzip": "~2.2.0", 34 | "cross-env": "~7.0.3", 35 | "css-loader": "^6.2.0", 36 | "eslint": "~7.32.0", 37 | "eslint-config-prettier": "~8.3.0", 38 | "eslint-config-standard": "~16.0.2", 39 | "eslint-plugin-import": "~2.23.4", 40 | "eslint-plugin-node": "~11.1.0", 41 | "eslint-plugin-promise": "~5.1.0", 42 | "eslint-plugin-react": "~7.24.0", 43 | "eslint-plugin-react-hooks": "~4.2.0", 44 | "eslint-webpack-plugin": "~3.0.1", 45 | "html-webpack-plugin": "^5.3.2", 46 | "husky": "~7.0.2", 47 | "less": "^4.1.1", 48 | "less-loader": "^10.0.1", 49 | "lint-staged": "~11.1.2", 50 | "npm-run-all": "~4.1.5", 51 | "postcss": "~8.3.6", 52 | "postcss-flexbugs-fixes": "^5.0.2", 53 | "postcss-loader": "^6.1.1", 54 | "postcss-preset-env": "~7.6.0", 55 | "prettier": "^2.3.2", 56 | "react-dev-utils": "~11.0.4", 57 | "style-loader": "^3.2.1", 58 | "typescript": "^4.3.5", 59 | "url-loader": "^4.1.1", 60 | "webpack": "~5.48.0", 61 | "webpack-bundle-analyzer": "~4.4.2", 62 | "webpack-cli": "^4.7.2", 63 | "webpack-dev-server": "~4.1.0" 64 | }, 65 | "dependencies": { 66 | "@hot-loader/react-dom": "~17.0.1", 67 | "antd": "~4.20.5", 68 | "axios": "~0.21.4", 69 | "eventemitter3": "~4.0.7", 70 | "react": "^17.0.2", 71 | "react-dom": "^17.0.2", 72 | "react-hot-loader": "~4.13.0", 73 | "react-router-dom": "~5.3.0", 74 | "uuid": "~8.3.2" 75 | }, 76 | "lint-staged": { 77 | "*.{js,jsx,ts,tsx}": "eslint --cache --fix", 78 | "*.{js,jsx,ts,tsx,css,less,md}": "prettier --write" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/code.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugin 初始化 3 | */ 4 | 5 | import './figma/index' 6 | figma.showUI(__html__, { 7 | title: 'Figma Plugin Sample', 8 | width: 400, 9 | height: 300, 10 | }) 11 | -------------------------------------------------------------------------------- /src/figma/index.ts: -------------------------------------------------------------------------------- 1 | import * as storage from './storage' 2 | import * as page from './page' 3 | import { FigmaMessageHandler, FigmaMessageParams } from 'types' 4 | 5 | // 如果添加了新的文件,需要在上面导入,并在actions中加上相应的变量名 6 | const actions: Record> = { 7 | storage, 8 | page, 9 | } 10 | 11 | figma.ui.onmessage = async ({ id, type, payload = {} }: FigmaMessageParams) => { 12 | // type 是 x.y 形式的,x 是引入的文件,如 storage,而 y 是其内的方法,如 get 等。示例: storage.get 13 | if (type && typeof type !== 'string') { 14 | console.error('不合法的figma message type!') 15 | return 16 | } 17 | const [fileName, funcName] = type?.split('.') 18 | const handler: FigmaMessageHandler = actions[fileName]?.[funcName] 19 | if (!handler) { 20 | console.warn(`找不到${type}对应的handler`) 21 | return 22 | } 23 | // 执行相应的handler,并获取其返回值 24 | let result 25 | let error 26 | try { 27 | result = await handler(payload) 28 | } catch (err) { 29 | error = err 30 | } 31 | if (id) { 32 | // 如果有id,需要再用 postMessage 的方式将消息体传回 ui。ui窗口中会根据 id 值找到相应的message,并执行其callback 33 | figma.ui.postMessage({ 34 | type: 'callback', 35 | payload: { id, result, error }, 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/figma/page.ts: -------------------------------------------------------------------------------- 1 | interface ExportOptions { 2 | id: string 3 | settings?: ExportSettings 4 | } 5 | 6 | export const exportNode = async ({ 7 | id, 8 | settings, 9 | }: ExportOptions): Promise => { 10 | const node = figma.getNodeById(id) as FrameNode 11 | return node.exportAsync(settings) 12 | } 13 | -------------------------------------------------------------------------------- /src/figma/storage.ts: -------------------------------------------------------------------------------- 1 | export const get = async (key: string) => { 2 | const data = figma.clientStorage.getAsync(key) 3 | return data 4 | } 5 | 6 | export const set = async (params: { name: string; data: any }) => { 7 | await figma.clientStorage.setAsync(params.name, params.data) 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | import { Button } from 'antd' 4 | 5 | const About = () => { 6 | const history = useHistory() 7 | 8 | const handleClick = () => { 9 | history.push('/') 10 | } 11 | 12 | return ( 13 |
14 |

关于

15 | 本仓库是一个支持 React 的 Figma 插件开发 Demo 16 |
17 |
18 | 19 |
20 | ) 21 | } 22 | 23 | export default About 24 | -------------------------------------------------------------------------------- /src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | import { Button, message } from 'antd' 4 | 5 | const Home = () => { 6 | const history = useHistory() 7 | 8 | const handleClick = () => { 9 | history.push('/about') 10 | } 11 | 12 | const showMessage = () => { 13 | message.success('Antd 的组件可以正常使用') 14 | } 15 | 16 | return ( 17 |
18 |

Hello Figma Plugin

19 | 20 |
21 |
22 | 23 |
24 |
25 | 编辑 src/pages/home.tsx 查看热更新效果。 26 |
27 | ) 28 | } 29 | 30 | export default Home 31 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { hot } from 'react-hot-loader/root' 2 | import React from 'react' 3 | import { MemoryRouter as Router, Switch, Route } from 'react-router-dom' 4 | import Home from './home' 5 | import About from './about' 6 | 7 | export default hot(function App() { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ) 22 | }) 23 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // figma 中的 message handler,用来处理 ui 中发来的消息 2 | export type FigmaMessageHandler = ( 3 | payload?: any, 4 | data?: any, 5 | ) => any | Promise 6 | export interface FigmaMessageParams { 7 | id?: string // 唯一ID,用于实现 callback 机制 8 | type: string // 消息type 9 | payload?: any // 数据体 10 | } 11 | 12 | export type FigmaMessageCallback = (payload: any) => any | Promise 13 | export interface CallbackPayload { 14 | id: string 15 | result: any 16 | error: any 17 | } 18 | -------------------------------------------------------------------------------- /src/ui.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/ui.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * UI 初始化 3 | */ 4 | 5 | import React from 'react' 6 | import { render } from 'react-dom' 7 | import App from 'pages' 8 | import EventEmitter from './utils/event' 9 | 10 | // 渲染 11 | render(, document.getElementById('my-plugin')) 12 | 13 | // 通过 event emitter 处理消息,需要访问来自figma的消息时,通过如下方式 14 | // import EventEmitter from 'utils/event' 15 | // event.on('xxx', (payload) => {}) 16 | 17 | onmessage = (evt) => { 18 | const pluginMessage = evt.data.pluginMessage ?? {} 19 | const { type, payload } = pluginMessage 20 | if (type) { 21 | // 上传 22 | EventEmitter.emit(type, payload) 23 | } 24 | } 25 | 26 | // @ts-ignore 27 | if (module.hot) { 28 | // @ts-ignore 29 | module.hot.accept('./ui.tsx', () => { 30 | render(, document.getElementById('my-plugin')) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/event.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3' 2 | import { v4 as uuid } from 'uuid' 3 | import type { CallbackPayload, FigmaMessageCallback } from 'types' 4 | 5 | const ee = new EventEmitter() 6 | 7 | // 处理回调,下游与下面的 emitToFigma 中的 emit.once 对应,上游对应于 src/figma/index.ts 中的 figma.ui.postMessage 8 | ee.on('callback', ({ id, result, error }: CallbackPayload) => { 9 | ee.emit(`callback:${id}`, { result, error }) 10 | }) 11 | 12 | export default ee 13 | 14 | /** 15 | * 向figma发送消息,获取执行结果并执行回调,或返回 promise 16 | * @example emitToFigma('storage.get', 'token', (token) => { console.log(token )}) 17 | * @example const token = await emitToFigma('storage.get', 'token') 18 | * @param string type: 要发给 figma 的消息类型 19 | * @param any payload: 要发给 figma 的消息体 20 | * @param callback 要执行的回调函数,也可以不用回调,选择使用 promise 形式。 21 | * @returns 22 | */ 23 | export const emitToFigma = async ( 24 | type: string, 25 | payload?: any, 26 | callback?: FigmaMessageCallback, 27 | ): Promise => { 28 | // 生成一个 id 用来匹配回调 29 | const id = uuid() 30 | return new Promise((resolve, reject) => { 31 | ee.once(`callback:${id}`, ({ result, error }) => { 32 | if (callback) { 33 | callback(result) // 有 callback 就调用callback 34 | } 35 | // 同时结束promise 36 | if (error) { 37 | reject(error) 38 | } else { 39 | resolve(result) 40 | } 41 | }) 42 | 43 | // 发送消息 44 | parent.postMessage({ pluginMessage: { id, type, payload } }, '*') 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "baseUrl": "./src", 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "removeComments": true, 14 | "noImplicitAny": false, 15 | "moduleResolution": "node", 16 | "typeRoots": ["./node_modules/@types", "./node_modules/@figma"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | declare module '*.jpg' 3 | declare module '*.jpeg' 4 | declare module '*.svg' 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin') 4 | const path = require('path') 5 | const webpack = require('webpack') 6 | const EslintPlugin = require('eslint-webpack-plugin') 7 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') 8 | 9 | const PORT = Number(process.env.PORT) || 8000 10 | 11 | process.env.NODE_ENV = process.env.NODE_ENV.replace(/^\s*|\s*$/g, '') 12 | 13 | const postCssConfig = { 14 | loader: require.resolve('postcss-loader'), 15 | options: { 16 | postcssOptions: { 17 | ident: 'postcss', 18 | plugins: [ 19 | require('postcss-flexbugs-fixes'), 20 | require('postcss-preset-env')({ 21 | autoprefixer: { 22 | flexbox: 'no-2009', 23 | }, 24 | stage: 3, 25 | }), 26 | ], 27 | }, 28 | }, 29 | } 30 | 31 | module.exports = (env, argv) => { 32 | const isProduction = argv.mode === 'production' 33 | return { 34 | mode: isProduction ? 'production' : 'development', 35 | devtool: isProduction ? false : 'inline-source-map', 36 | entry: { 37 | ui: './src/ui.tsx', 38 | code: './src/code.ts', 39 | }, 40 | 41 | module: { 42 | strictExportPresence: true, 43 | rules: [ 44 | { 45 | oneOf: [ 46 | { 47 | test: /\.(js|mjs|jsx|ts|tsx)$/, 48 | include: path.resolve('./src'), 49 | loader: require.resolve('babel-loader'), 50 | options: { 51 | customize: require.resolve( 52 | 'babel-preset-react-app/webpack-overrides', 53 | ), 54 | presets: [[require.resolve('babel-preset-react-app')]], 55 | plugins: [ 56 | [ 57 | require.resolve('babel-plugin-import'), 58 | { 59 | libraryName: 'antd', 60 | style: 'css', 61 | }, 62 | ], 63 | [ 64 | require.resolve('babel-plugin-named-asset-import'), 65 | { 66 | loaderMap: { 67 | svg: { 68 | ReactComponent: 69 | '@svgr/webpack?-svgo,+titleProp,+ref![path]', 70 | }, 71 | }, 72 | }, 73 | ], 74 | [ 75 | '@babel/plugin-transform-runtime', 76 | { 77 | regenerator: true, 78 | }, 79 | ], 80 | 'react-hot-loader/babel', 81 | ], 82 | babelrc: false, 83 | configFile: false, 84 | cacheDirectory: true, 85 | cacheCompression: false, 86 | compact: isProduction, 87 | }, 88 | }, 89 | { 90 | test: /\.css$/, 91 | use: [ 92 | require.resolve('style-loader'), 93 | { 94 | loader: require.resolve('css-loader'), 95 | options: { 96 | importLoaders: 1, 97 | }, 98 | }, 99 | { 100 | ...postCssConfig, 101 | }, 102 | ], 103 | }, 104 | { 105 | test: /\.less$/, 106 | use: [ 107 | require.resolve('style-loader'), 108 | { 109 | loader: require.resolve('css-loader'), 110 | options: { 111 | importLoaders: 2, 112 | }, 113 | }, 114 | { 115 | ...postCssConfig, 116 | }, 117 | { 118 | loader: require.resolve('less-loader'), 119 | options: { 120 | lessOptions: { 121 | javascriptEnabled: true, 122 | }, 123 | }, 124 | }, 125 | ], 126 | }, 127 | { 128 | test: /\.(png|jpg|gif|webp|svg)$/, 129 | loader: 'url-loader', 130 | }, 131 | ], 132 | }, 133 | ], 134 | }, 135 | 136 | resolve: { 137 | extensions: ['.tsx', '.ts', '.jsx', '.js'], 138 | alias: { 139 | 'react-dom': '@hot-loader/react-dom', 140 | }, 141 | modules: [path.resolve(__dirname, 'src'), 'node_modules'], 142 | }, 143 | output: { 144 | filename: '[name].js', 145 | path: path.resolve(__dirname, 'dist'), 146 | publicPath: `http://localhost:${PORT}/`, 147 | }, 148 | 149 | plugins: [ 150 | new EslintPlugin({ 151 | extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'], 152 | eslintPath: require.resolve('eslint'), 153 | cache: true, 154 | }), 155 | new HtmlWebpackPlugin({ 156 | template: './src/ui.html', 157 | filename: 'ui.html', 158 | chunks: ['ui'], 159 | inject: 'body', 160 | }), 161 | isProduction && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/ui/]), 162 | new webpack.EnvironmentPlugin({ 163 | REACT_FIGMA_EXPERIMENTAL: 'REACT_FIGMA_EXPERIMENTAL', 164 | MOCK: 'MOCK', 165 | BASE_URL: 'BASE_URL', 166 | }), 167 | process.env.ANALYZE && new BundleAnalyzerPlugin(), 168 | ].filter(Boolean), 169 | 170 | devServer: isProduction 171 | ? undefined 172 | : { 173 | port: PORT, 174 | host: '0.0.0.0', 175 | allowedHosts: 'all', 176 | hot: true, 177 | headers: { 178 | 'Access-Control-Allow-Origin': '*', 179 | 'Access-Control-Allow-Methods': 180 | 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 181 | }, 182 | client: { 183 | webSocketURL: `ws://127.0.0.1:${PORT}/ws`, 184 | }, 185 | }, 186 | } 187 | } 188 | --------------------------------------------------------------------------------