├── .babelrc ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── logo.png ├── package.json ├── src ├── _locales │ ├── en │ │ └── messages.json │ └── ru │ │ └── messages.json ├── assets │ ├── icons │ │ └── logo.svg │ └── img │ │ ├── icon-128x128.png │ │ ├── icon-16x16.png │ │ └── icon-48x48.png ├── background.js ├── components │ ├── Box │ │ ├── Box.jsx │ │ └── index.js │ └── Example │ │ ├── Example.jsx │ │ └── index.js ├── content.js ├── libs │ ├── omitProps.js │ └── polyfills.js ├── manifest.json ├── options.js ├── popup.js └── themes │ ├── config.js │ └── default.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | [ 5 | "babel-plugin-styled-components", 6 | { 7 | "displayName": false 8 | } 9 | ], 10 | "@babel/plugin-proposal-class-properties" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '19 6 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | build 5 | build.pem 6 | ext.nex 7 | ext.pem 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "bracketSpacing": true, 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxSingleQuote": false, 8 | "printWidth": 100, 9 | "arrowParens": "avoid", 10 | "jsxBracketSameLine": false 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.0.0 2 | 3 | - Switched from CSS Modules to styled-components 4 | - Removed postcss 5 | - Added version sync between package.json and manifest.json 6 | - Fixed auto reloading 7 | - Added SVGR to load *.svg as React components 8 | - Updated all dependencies 9 | - Created an example of usage (see: [Framer ToDo](https://github.com/ElForastero/framer)) 10 | - Setup `src` directory as a modules source 11 | 12 | ### 0.0.2 13 | 14 | - Added auto reloading with [webpack-extension-reloader](https://github.com/rubenspgcavalcante/webpack-extension-reloader) 15 | - Fix: content.js tries to load background.css instead of content.css 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eugene Dzhumak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

React Web Extension Boilerplate

2 |

3 | 4 | 5 | Documentation 6 | 7 | 8 | Maintenance 9 | 10 | 11 | License: MIT 12 | 13 |

14 | 15 | > This project aims to provide a simple boilerplate for writing browser extensions for the most popular browsers, such as Chrome, Firefox, Opera, new Edge and other Chromium-based browsers. 16 | 17 | ![React Web Extension Boilerplate](logo.png) 18 | 19 | ## 🎉 Features 20 | 21 | - **Preact X or React** (you can switch them easily in webpack config) 22 | - **Shadow DOM** for injected content 23 | - **Styled-Components** 24 | - **Auto reloading** (there's no need to manually reload extension) 25 | - Ready-to-go setup with **internationalization** 26 | 27 | ## 👨‍💻 Examples 28 | 29 | You can look at: 30 | 31 | - [Framer](https://github.com/ElForastero/framer): a todo list extension as an example of using this boilerplate. 32 | - [Good Block](https://github.com/LucasAndrad/block-sites-react-extension): an extension to block websites, also using this boilerplate. 33 | - [ScreenplaySubs](https://github.com/SMASH-CUT/extension): an extension to watch movies in Netflix with screenplays, in sync. Also using this boilerplate. 34 | 35 | There are no straight restrictions on how to use it, or any limitations on tools and technologies. Think of it as a regular react application with some special properties. 36 | 37 | ## 📝 Description 38 | 39 | It's built with `preact` and `preact-compat` which allows you to switch between `react` and `preact`. 40 | 41 | Content and styles which are injected directly to the page, are isolated inside Shadow DOM. 42 | 43 | ## 🏁 Install 44 | 45 | ```sh 46 | git clone git@github.com:ElForastero/react-browser-extension-boilerplate.git 47 | ``` 48 | 49 | ## 🚀 Usage 50 | 51 | ```sh 52 | yarn watch 53 | ``` 54 | 55 | Runs webpack in watch mode. Automatically reloads the page after changes in files. Thanks to [webpack-extension-reloader](https://github.com/rubenspgcavalcante/webpack-extension-reloader). 56 | 57 | ```sh 58 | yarn build 59 | ``` 60 | 61 | Builds the extension in production mode. This version can be shipped to the store. 62 | 63 | ## How to increment version 64 | 65 | Use [npm version](https://docs.npmjs.com/cli/version) cli command to bump a version of your package.json. The version of manifest will stay in sync with version specified in package.json. 66 | 67 | For example: 68 | 69 | ```sh 70 | npm version patch 71 | ``` 72 | 73 | This will increase your patch package.json version. During the next build output manifest file will have the same version. 74 | 75 | ## ⚠️ Content Security Policy (CSP) 76 | "unsafe-eval" in directive "script-src" and "connect-src" are needed for auto reloading, and should be removed from production manifest.json. 77 | 78 | ## 💻 Useful links 79 | 80 | - [Manifest File Format](https://developer.chrome.com/apps/manifest) 81 | - [Content Security Policy (CSP)](https://developer.chrome.com/extensions/contentSecurityPolicy) 82 | - [Chrome i18n](https://developer.chrome.com/extensions/i18n) 83 | - [Porting a Google Chrome extension to Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Porting_a_Google_Chrome_extension) 84 | - [Firefox add-ons examples](https://github.com/mdn/webextensions-examples) 85 | - [exthouse - tool for performance testing](https://github.com/treosh/exthouse) 86 | - [webext-redux - a set o utilities to use redux in web extensions](https://github.com/tshaddix/webext-redux) 87 | - [webpack-manifest-version-sync-plugin](https://github.com/ElForastero/webpack-manifest-version-sync-plugin) 88 | 89 | ## 🤝 Show your support 90 | 91 | Give a ⭐️ if this project helped you! 92 | 93 | ## 📝 License 94 | 95 | Copyright © 2019 [Eugene Dzhumak](https://github.com/ElForastero).
96 | This project is [MIT](https://github.com/ElForastero/react-browser-extension-boilerplate/blob/master/LICENSE) licensed. 97 | 98 | *** 99 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ 100 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElForastero/react-browser-extension-boilerplate/93d32e288e93776b0e9d320f158440a442228724/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.1", 3 | "name": "react-browser-extension-boilerplate", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/ElForastero/react-browser-extension-boilerplate" 7 | }, 8 | "scripts": { 9 | "build": "cross-env NODE_ENV=production yarn webpack -c webpack.config.js --mode production", 10 | "watch": "cross-env NODE_ENV=development yarn webpack -c webpack.config.js --mode development -w" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.6.4", 14 | "@babel/plugin-proposal-class-properties": "^7.5.5", 15 | "@babel/preset-env": "^7.6.3", 16 | "@babel/preset-react": "^7.6.3", 17 | "@svgr/webpack": "^4.3.3", 18 | "babel-loader": "^8.0.6", 19 | "babel-plugin-styled-components": "^1.10.6", 20 | "clean-webpack-plugin": "^3.0.0", 21 | "copy-webpack-plugin": "^5.0.4", 22 | "cross-env": "^6.0.3", 23 | "html-webpack-plugin": "^4.0.0-beta.8", 24 | "prettier": "^1.18.2", 25 | "raw-loader": "^3.1.0", 26 | "webpack": "^4.41.2", 27 | "webpack-cli": "^3.3.9", 28 | "webpack-extension-reloader": "^1.1.1", 29 | "webpack-manifest-version-sync-plugin": "^0.0.3" 30 | }, 31 | "dependencies": { 32 | "@styled-system/prop-types": "^5.1.2", 33 | "classnames": "^2.2.6", 34 | "core-js": "^3.3.3", 35 | "lodash.omit": "^4.5.0", 36 | "preact": "^10.0.1", 37 | "preact-shadow-root": "^1.0.2", 38 | "prop-types": "^15.7.2", 39 | "react-switch": "^5.0.1", 40 | "regenerator-runtime": "^0.13.3", 41 | "styled-components": "^4.4.0", 42 | "styled-system": "^5.1.2", 43 | "use-click-outside": "^1.1.0", 44 | "webextension-polyfill": "^0.5.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "greeting": { 3 | "message": "Hello, World!" 4 | }, 5 | "appName": { 6 | "message": "React Web Extension Boilerplate", 7 | "description": "The title of the application, displayed in the web store." 8 | }, 9 | "appShortName": { 10 | "message": "RWEB", 11 | "description": "The short_name (maximum of 12 characters recommended) is a short version of the extension's name. " 12 | }, 13 | "appDesc": { 14 | "message": "This app does something awesome.", 15 | "description": "The description of the application, displayed in the web store." 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "greeting": { 3 | "message": "Привет, Мир!" 4 | }, 5 | "appName": { 6 | "message": "React Web Extension Boilerplate", 7 | "description": "Название расширения для магазина расширений." 8 | }, 9 | "appShortName": { 10 | "message": "RWEB", 11 | "description": "Короткая (рекомендовано максимум 12 символов) версия названия расширения." 12 | }, 13 | "appDesc": { 14 | "message": "Это расширение делает что-то крутое.", 15 | "description": "Описание расширения, отображаемое в магазине расширений." 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElForastero/react-browser-extension-boilerplate/93d32e288e93776b0e9d320f158440a442228724/src/assets/img/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/img/icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElForastero/react-browser-extension-boilerplate/93d32e288e93776b0e9d320f158440a442228724/src/assets/img/icon-16x16.png -------------------------------------------------------------------------------- /src/assets/img/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElForastero/react-browser-extension-boilerplate/93d32e288e93776b0e9d320f158440a442228724/src/assets/img/icon-48x48.png -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElForastero/react-browser-extension-boilerplate/93d32e288e93776b0e9d320f158440a442228724/src/background.js -------------------------------------------------------------------------------- /src/components/Box/Box.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { space, layout, flexbox, position } from 'styled-system'; 4 | import propTypes from '@styled-system/prop-types'; 5 | import omitProps from 'libs/omitProps'; 6 | 7 | const propsToOmit = [ 8 | ...Object.keys(propTypes.space), 9 | ...Object.keys(propTypes.layout), 10 | ...Object.keys(propTypes.flexbox), 11 | ...Object.keys(propTypes.position), 12 | ]; 13 | 14 | const Box = styled(omitProps('div', propsToOmit))` 15 | box-sizing: border-box; 16 | ${layout} 17 | ${space} 18 | ${flexbox} 19 | ${position} 20 | `; 21 | 22 | export default Box; 23 | -------------------------------------------------------------------------------- /src/components/Box/index.js: -------------------------------------------------------------------------------- 1 | import Box from './Box'; 2 | 3 | export default Box; -------------------------------------------------------------------------------- /src/components/Example/Example.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Box from 'components/Box'; 4 | import Logo from 'assets/icons/logo.svg'; 5 | 6 | const Example = styled.div` 7 | color: ${props => props.theme.palette.primary}; 8 | font-size: 20px; 9 | text-align: center; 10 | `; 11 | 12 | export default () => { 13 | const greeting = chrome.i18n.getMessage('greeting'); 14 | 15 | return ( 16 | 17 | 18 | 19 | {greeting} 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Example/index.js: -------------------------------------------------------------------------------- 1 | import Example from './Example'; 2 | 3 | export default Example; 4 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | import 'libs/polyfills'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { ThemeProvider, StyleSheetManager } from 'styled-components'; 5 | import Box from 'components/Box'; 6 | import Example from 'components/Example'; 7 | import defaultTheme from 'themes/default'; 8 | 9 | const root = document.createElement('div'); 10 | const shadow = root.attachShadow({ mode: 'open' }); 11 | 12 | const styleContainer = document.createElement('div'); 13 | const appContainer = document.createElement('div'); 14 | 15 | shadow.appendChild(styleContainer); 16 | shadow.appendChild(appContainer); 17 | 18 | document.body.appendChild(root); 19 | 20 | const App = () => { 21 | return ( 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | ReactDOM.render(, appContainer); 37 | -------------------------------------------------------------------------------- /src/libs/omitProps.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import omit from 'lodash.omit'; 4 | 5 | /** 6 | * @see https://github.com/styled-system/styled-system/issues/593#issuecomment-512350138 7 | */ 8 | export default function omitProps(Component, propsToOmit) { 9 | function WithoutOmittedProps({ children, ...rest }) { 10 | return {children}; 11 | } 12 | 13 | WithoutOmittedProps.propTypes = { 14 | children: PropTypes.node, 15 | }; 16 | 17 | WithoutOmittedProps.displayName = `WithoutOmittedProps(${Component.displayName || 18 | Component.name})`; 19 | 20 | return WithoutOmittedProps; 21 | } 22 | -------------------------------------------------------------------------------- /src/libs/polyfills.js: -------------------------------------------------------------------------------- 1 | import 'core-js/stable'; 2 | import 'regenerator-runtime/runtime'; 3 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "version": "1.0.0", 4 | "name": "__MSG_appName__", 5 | "short_name": "__MSG_appShortName__", 6 | "description": "__MSG_appDesc__", 7 | "default_locale": "en", 8 | "options_ui": { 9 | "page": "options.html" 10 | }, 11 | "background": { 12 | "scripts": ["background.js"] 13 | }, 14 | "browser_action": { 15 | "default_popup": "popup.html" 16 | }, 17 | "content_scripts": [ 18 | { 19 | "all_frames": false, 20 | "matches": ["http://*/*", "https://*/*"], 21 | "js": ["content.js"] 22 | } 23 | ], 24 | "icons": { 25 | "16": "assets/img/icon-16x16.png", 26 | "48": "assets/img/icon-48x48.png", 27 | "128": "assets/img/icon-128x128.png" 28 | }, 29 | "permissions": ["storage"], 30 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'; connect-src ws://localhost:*/", 31 | "web_accessible_resources": ["assets/img/*"] 32 | } 33 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { ThemeProvider } from 'styled-components'; 4 | import Example from 'components/Example'; 5 | import defaultTheme from 'themes/default'; 6 | 7 | const OptionsPage = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | const root = document.createElement('div'); 16 | document.body.appendChild(root); 17 | 18 | ReactDOM.render(, root); 19 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | import 'libs/polyfills'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import Box from 'components/Box'; 6 | import Example from 'components/Example'; 7 | import defaultTheme from 'themes/default'; 8 | 9 | const Popup = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | const root = document.createElement('div'); 20 | document.body.appendChild(root); 21 | 22 | ReactDOM.render(, root); 23 | -------------------------------------------------------------------------------- /src/themes/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | space: [0, 4, 8, 16, 32, 64, 128, 256, 512], 3 | sizes: [0, 4, 8, 16, 32, 64, 128, 256, 512], 4 | }; 5 | -------------------------------------------------------------------------------- /src/themes/default.js: -------------------------------------------------------------------------------- 1 | import config from './config'; 2 | 3 | export default { 4 | ...config, 5 | palette: { 6 | primary: '#488eff', 7 | secondary: '#FFFFFF', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HTMLPlugin = require('html-webpack-plugin'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const path = require('path'); 4 | const ExtensionReloader = require('webpack-extension-reloader'); 5 | const ManifestVersionSyncPlugin = require('webpack-manifest-version-sync-plugin'); 6 | 7 | module.exports = { 8 | entry: { 9 | options: './src/options.js', 10 | popup: './src/popup.js', 11 | content: './src/content.js', 12 | background: './src/background.js', 13 | }, 14 | output: { 15 | filename: '[name].js', 16 | path: path.resolve(__dirname, 'build'), 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx', '.css'], 20 | modules: [path.resolve(__dirname, 'src'), 'node_modules'], 21 | alias: { 22 | react: 'preact/compat', 23 | 'react-dom': 'preact/compat', 24 | }, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.jsx?$/, 30 | exclude: /node_modules/, 31 | use: [ 32 | { 33 | loader: 'babel-loader', 34 | }, 35 | ], 36 | }, 37 | { 38 | test: /\.svg$/, 39 | use: ['@svgr/webpack'], 40 | }, 41 | ], 42 | }, 43 | plugins: [ 44 | new HTMLPlugin({ 45 | chunks: ['options'], 46 | filename: 'options.html', 47 | title: 'Options page title', 48 | }), 49 | new HTMLPlugin({ 50 | chunks: ['popup'], 51 | filename: 'popup.html', 52 | }), 53 | new CopyPlugin([ 54 | { from: './src/_locales/', to: './_locales' }, 55 | { from: './src/assets', to: './assets' }, 56 | { from: './src/manifest.json', to: './manifest.json' }, 57 | ]), 58 | new ExtensionReloader({ 59 | manifest: path.resolve(__dirname, './src/manifest.json'), 60 | }), 61 | new ManifestVersionSyncPlugin(), 62 | ], 63 | optimization: { 64 | minimize: true, 65 | }, 66 | mode: 'production', 67 | stats: 'minimal', 68 | }; 69 | --------------------------------------------------------------------------------