├── .env ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ └── upload.yml ├── .gitignore ├── .markdownlint.json ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── preview ├── Sep-21-2020 15-15-55.gif └── profile-chrome.png ├── src ├── _locales │ └── en │ │ └── messages.json ├── assets │ └── icons │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-24.png │ │ ├── icon-32.png │ │ ├── icon-48.png │ │ └── icon-64.png ├── background │ └── index.ts ├── baseManifest_chrome.json ├── baseManifest_edge.json ├── baseManifest_firefox.json ├── baseManifest_opera.json ├── components │ └── Button │ │ ├── button.scss │ │ └── index.tsx ├── content │ ├── index.tsx │ └── messageListener.ts ├── options │ ├── index.html │ └── options.tsx ├── popup │ ├── Popup.tsx │ ├── index.html │ └── index.tsx ├── types │ ├── message.d.ts │ ├── openControl.d.ts │ └── response.d.ts └── utils │ ├── sendMessages.ts │ └── storage.ts ├── tsconfig.json ├── webpack.config.ts ├── webpack.config.utils.ts └── yarn.lock /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/.env -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, // Allows for the parsing of modern ECMAScript features 5 | "sourceType": "module", // Allows for the use of imports 6 | "ecmaFeatures": { 7 | "jsx": true // Allows for the parsing of JSX 8 | } 9 | }, 10 | "settings": { 11 | "react": { 12 | "version": "detect" // Tells eslint-plugin-react to automatically detect the version of React to use 13 | } 14 | }, 15 | "plugins": ["@typescript-eslint", "prettier"], 16 | "rules": { 17 | "react/prop-types": [1], 18 | "prettier/prettier": [2], 19 | "@typescript-eslint/ban-ts-comment": [0], 20 | "@typescript-eslint/no-explicit-any": [1], 21 | "@typescript-eslint/no-var-requires": [0] 22 | }, 23 | "extends": ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"] 24 | } 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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: '34 5 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['typescript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v2 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/upload.yml: -------------------------------------------------------------------------------- 1 | name: Upload Built files 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | upload: 10 | name: Build and Upload files on Artifact 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18.16.0 17 | 18 | - name: Build 19 | run: | 20 | yarn install --frozen-lockfile 21 | yarn upload 22 | - name: Uploading Chrome Extension 23 | uses: actions/upload-artifact@v3 24 | with: 25 | name: chrome 26 | path: dist/chrome 27 | - name: Uploading Firefox Extension 28 | uses: actions/upload-artifact@v3 29 | with: 30 | name: firefox 31 | path: dist/firefox 32 | - name: Uploading Opera Extension 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: opera 36 | path: dist/opera 37 | - name: Uploading Edge Extension 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: edge 41 | path: dist/edge 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | dev 4 | temp 5 | .idea -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-inline-html": { 3 | "allowed_elements": ["details", "p", "div", "h1", "strong"] 4 | }, 5 | "line-length": false 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "singleQuote": true, 5 | "useTabs": false, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[markdown]": { 3 | "editor.formatOnSave": false 4 | }, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": true 7 | }, 8 | "editor.tabSize": 4, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "editor.formatOnSave": true, 11 | "editor.formatOnType": true 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 WebEXP0528 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 Extension Boilerplate 2 | 3 | Works for Chrome, Opera, Edge & Firefox. 4 | 5 | ![preview](preview/Sep-21-2020%2015-15-55.gif) 6 | 7 | ## Features 8 | 9 | >- ___Supports Manifest Version 3___ 10 | > 11 | >- ___Write in your favorite framework - React! :)___ 12 | > 13 | > Now you can create part of your extensions in React framework - as you wish ;) 14 | > 15 | >- ___Write once and deploy to Chrome, Opera, Edge & Firefox___ 16 | > 17 | > Based on `webextension-polyfill`. It also includes a tiny polyfill to bring uniformity to the APIs exposed by different browsers. 18 | > 19 | >- ___Live-reload___ 20 | > 21 | > Your changes to CSS, HTML & JS files will be relayed instantly without having 22 | > to manually reload the extension. This ends up saving a lot of time and 23 | > improving the developer experience. Based on `web-ext-reloader-mv3` 24 | > 25 | >- ___Newest js technology stack___ 26 | > 27 | > You can use `Typescript` or `Babel` 28 | > 29 | >- ___Profiling JS Packages___ 30 | > 31 | > ![Profile](preview/profile-chrome.png) 32 | > 33 | >- ___Comfortable styles import___ 34 | > 35 | > With react you can load styles directly and you can use scss for styling. 36 | > 37 | >- ___Easily configurable and extendable___ 38 | > 39 | > Project use webpack so you can easily customize your project depends on your needs. 40 | > In config.json you can define source path for each browser 41 | > (if needed - default it's the same source), destination and develop directory. 42 | > 43 | >- ___Clean code___ 44 | > 45 | > Clean code is the best way for long term support for project. Boilerplate has 46 | > fully configured eslint with airbnb style guide. 47 | > 48 | >- ___Test your components!___ 49 | > 50 | > Project use some library which support your testing process. 51 | > As test runner we use karma, as testing framework mocha. 52 | > As support to assertion we use chai. 53 | 54 | ## Run & Installation 55 | 56 | >### Run & Build 57 | > 58 | >> 1. Clone the repository `git clone https://github.com/WebExp0528/React-Extension-Boilerplate.git`. If you want to use Babel, `git clone --single-branch --branch babel https://github.com/WebExp0528/React-Extension-Boilerplate.git` 59 | >> 2. Run `npm install` or `yarn install` 60 | >> 3. Run `npm run build:{target browser}` or `yarn build:{target browser}`. EX: `yarn build:chrome` 61 | >> 62 | >> Note: You can [download](https://github.com/WebExp0528/React-Extension-Boilerplate/releases/latest) build file 63 | > 64 | >### Load the extension in Chrome & Opera 65 | > 66 | >> 1. Open Chrome/Opera browser and navigate to chrome://extensions 67 | >> 2. Select "Developer Mode" and then click "Load unpacked extension..." 68 | >> 3. From the file browser, choose to `React-Extension-Boilerplate/dev/chrome` 69 | >> or > (`React-Extension-Boilerplate/dev/opera`) 70 | > 71 | >### Load the extension in Firefox 72 | > 73 | >>1. Open Firefox browser and navigate to about:debugging 74 | >>2. Click "Load Temporary Add-on" and from the file browser, choose >>`React-Extension-Boilerplate/dev/firefox` 75 | > 76 | >### Load the extension in Edge 77 | > 78 | >>adding-and-removing-extensions> 79 | 80 | ## Developing 81 | 82 | >The following tasks can be used when you want to start developing the extension 83 | >and want to enable live reload - 84 | >`npm run dev:{target browser}` or `yarn dev:{target browser}` 85 | 86 | ## Profiling 87 | 88 | > Run `npm run profile:{target browser}` or `yarn profile:{target browser}` 89 | 90 | ## Packaging 91 | 92 | >Run `npm run build:{target browser}` or `yarn build:{target browser}` to create a zipped, 93 | production-ready extension for each browser. 94 | You can then upload that to the app store. 95 | 96 | ## Available Target Browsers 97 | 98 | > `chrome` `firefox` `opera` `edge` 99 | 100 | --- 101 | 102 | This project is licensed under the MIT license. 103 | 104 | If you have any questions or comments, please create a new issue. 105 | I'd be happy to hear your thoughts. 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-extension-boilerplate", 3 | "version": "3.0.0", 4 | "description": "Boilerplate for Chrome, FF, Opera extension", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "dev:chrome": "cross-env NODE_ENV=development TARGET=chrome webpack", 8 | "dev:firefox": "cross-env NODE_ENV=development TARGET=firefox webpack", 9 | "dev:opera": "cross-env NODE_ENV=development TARGET=opera webpack", 10 | "dev:edge": "cross-env NODE_ENV=development TARGET=edge webpack", 11 | "profile:chrome": "cross-env NODE_ENV=profile TARGET=chrome webpack", 12 | "profile:firefox": "cross-env NODE_ENV=profile TARGET=firefox webpack", 13 | "profile:opera": "cross-env NODE_ENV=profile TARGET=opera webpack", 14 | "profile:edge": "cross-env NODE_ENV=profile TARGET=edge webpack", 15 | "build:chrome": "cross-env NODE_ENV=production TARGET=chrome webpack", 16 | "build:firefox": "cross-env NODE_ENV=production TARGET=firefox webpack", 17 | "build:opera": "cross-env NODE_ENV=production TARGET=opera webpack", 18 | "build:edge": "cross-env NODE_ENV=production TARGET=edge webpack", 19 | "upload:chrome": "cross-env NODE_ENV=upload TARGET=chrome webpack", 20 | "upload:firefox": "cross-env NODE_ENV=upload TARGET=firefox webpack", 21 | "upload:opera": "cross-env NODE_ENV=upload TARGET=opera webpack", 22 | "upload:edge": "cross-env NODE_ENV=upload TARGET=edge webpack", 23 | "build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera && yarn run build:edge", 24 | "upload": "yarn run upload:chrome && yarn run upload:firefox && yarn run upload:opera && yarn run upload:edge", 25 | "lint-fix": "eslint --ext js,jsx,ts,tsx, src --fix" 26 | }, 27 | "author": "Web Dev", 28 | "dependencies": { 29 | "clean-webpack-plugin": "^4.0.0", 30 | "copy-webpack-plugin": "^11.0.0", 31 | "core-js": "^3.32.1", 32 | "cross-env": "^7.0.3", 33 | "css-loader": "^6.8.1", 34 | "dotenv": "^16.3.1", 35 | "html-webpack-plugin": "^5.5.3", 36 | "prop-types": "^15.8.1", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0", 39 | "sass": "^1.66.1", 40 | "sass-loader": "^13.3.2", 41 | "style-loader": "^3.3.3", 42 | "terser-webpack-plugin": "^5.3.9", 43 | "ts-node": "^10.9.1", 44 | "webextension-polyfill": "^0.10.0", 45 | "webpack": "^5.88.2", 46 | "webpack-bundle-analyzer": "^4.9.1", 47 | "webpack-ext-reloader-mv3": "^2.1.1", 48 | "webpack-extension-manifest-plugin": "^0.8.0", 49 | "zip-webpack-plugin": "^4.0.1" 50 | }, 51 | "devDependencies": { 52 | "@types/chrome": "^0.0.244", 53 | "@types/inboxsdk": "^2.0.11", 54 | "@types/jquery": "^3.5.18", 55 | "@types/lodash": "^4.14.197", 56 | "@types/node": "^20.5.8", 57 | "@types/react": "^18.2.21", 58 | "@types/react-dom": "^18.2.7", 59 | "@types/react-router-dom": "^5.3.3", 60 | "@types/redux": "^3.6.31", 61 | "@types/webpack-bundle-analyzer": "^4.6.0", 62 | "@types/zip-webpack-plugin": "^3.0.4", 63 | "@typescript-eslint/eslint-plugin": "^6.5.0", 64 | "@typescript-eslint/parser": "^6.5.0", 65 | "eslint": "^8.48.0", 66 | "eslint-config-airbnb": "^19.0.4", 67 | "eslint-config-prettier": "^9.0.0", 68 | "eslint-plugin-babel": "^5.3.1", 69 | "eslint-plugin-import": "^2.28.1", 70 | "eslint-plugin-jsx-a11y": "^6.7.1", 71 | "eslint-plugin-prettier": "^5.0.0", 72 | "eslint-plugin-react": "^7.33.2", 73 | "eslint-plugin-react-hooks": "^4.6.0", 74 | "eslint-webpack-plugin": "^4.0.1", 75 | "prettier": "^3.0.3", 76 | "ts-loader": "^9.4.4", 77 | "typescript": "^5.2.2", 78 | "webpack-cli": "^5.1.4" 79 | }, 80 | "license": "SEE LICENSE IN LICENSE", 81 | "repository": { 82 | "type": "git", 83 | "url": "https://github.com/WebExp0528/React-Extension-Boilerplate" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /preview/Sep-21-2020 15-15-55.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/preview/Sep-21-2020 15-15-55.gif -------------------------------------------------------------------------------- /preview/profile-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/preview/profile-chrome.png -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "React Extension Boilerplate", 4 | "description": "The name of the react extension." 5 | }, 6 | "appDescription": { 7 | "message": "Boilerplate for building cross browser extensions", 8 | "description": "The description of the extension." 9 | }, 10 | "btnTooltip": { 11 | "message": "Extension Boilerplate", 12 | "description": "Tooltip for the button." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/src/assets/icons/icon-128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/src/assets/icons/icon-16.png -------------------------------------------------------------------------------- /src/assets/icons/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/src/assets/icons/icon-24.png -------------------------------------------------------------------------------- /src/assets/icons/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/src/assets/icons/icon-32.png -------------------------------------------------------------------------------- /src/assets/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/src/assets/icons/icon-48.png -------------------------------------------------------------------------------- /src/assets/icons/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebExp0528/React-Extension-Boilerplate/cca4175d219685fa7103e2f2c234bbbd38e0cdf8/src/assets/icons/icon-64.png -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { runtime, tabs, Tabs, Runtime } from 'webextension-polyfill'; 2 | 3 | /** 4 | * Define background script functions 5 | * @type {class} 6 | */ 7 | class Background { 8 | _port: number; 9 | constructor() { 10 | this.init(); 11 | } 12 | 13 | /** 14 | * Document Ready 15 | * 16 | * @returns {void} 17 | */ 18 | init = () => { 19 | console.log('[===== Loaded Background Scripts =====]'); 20 | 21 | //When extension installed 22 | runtime.onInstalled.addListener(this.onInstalled); 23 | 24 | //Add message listener in Browser. 25 | runtime.onMessage.addListener(this.onMessage); 26 | 27 | //Add Update listener for tab 28 | tabs.onUpdated.addListener(this.onUpdatedTab); 29 | 30 | //Add New tab create listener 31 | tabs.onCreated.addListener(this.onCreatedTab); 32 | }; 33 | 34 | //TODO: Listeners 35 | 36 | /** 37 | * Extension Installed 38 | */ 39 | onInstalled = () => { 40 | console.log('[===== Installed Extension!] ====='); 41 | }; 42 | 43 | /** 44 | * Message Handler Function 45 | * 46 | * @param message 47 | * @param sender 48 | * @returns 49 | */ 50 | onMessage = async (message: EXTMessage, sender: Runtime.MessageSender) => { 51 | try { 52 | console.log('[===== Received message =====]', message, sender); 53 | switch (message.type) { 54 | } 55 | return true; // result to reply 56 | } catch (error) { 57 | console.log('[===== Error in MessageListener =====]', error); 58 | return error; 59 | } 60 | }; 61 | 62 | /** 63 | * Message from Long Live Connection 64 | * 65 | * @param msg 66 | */ 67 | onMessageFromExtension = (msg: EXTMessage) => { 68 | console.log('[===== Message from Long Live Connection =====]', msg); 69 | }; 70 | 71 | /** 72 | * 73 | * @param tab 74 | */ 75 | onCreatedTab = (tab: Tabs.Tab) => { 76 | console.log('[===== New Tab Created =====]', tab); 77 | }; 78 | 79 | /** 80 | * When changes tabs 81 | * 82 | * @param {*} tabId 83 | * @param {*} changeInfo 84 | * @param {*} tab 85 | */ 86 | onUpdatedTab = (tabId: number, changeInfo: Tabs.OnUpdatedChangeInfoType, tab: Tabs.Tab) => { 87 | console.log('[===== Tab Created =====]', tabId, changeInfo, tab); 88 | }; 89 | 90 | /** 91 | * Get url from tabId 92 | * 93 | */ 94 | getURLFromTab = async (tabId: number) => { 95 | try { 96 | const tab = await tabs.get(tabId); 97 | return tab.url || ''; 98 | } catch (error) { 99 | console.log(`[===== Could not get Tab Info$(tabId) in getURLFromTab =====]`, error); 100 | throw ''; 101 | } 102 | }; 103 | 104 | /** 105 | * Open new tab by url 106 | * 107 | */ 108 | openNewTab = async (url: string) => { 109 | try { 110 | const tab = await tabs.create({ url }); 111 | return tab; 112 | } catch (error) { 113 | console.log(`[===== Error in openNewTab =====]`, error); 114 | return null; 115 | } 116 | }; 117 | 118 | /** 119 | * Close specific tab 120 | * 121 | * @param {number} tab 122 | */ 123 | closeTab = async (tab: Tabs.Tab) => { 124 | try { 125 | await tabs.remove(tab.id ?? 0); 126 | } catch (error) { 127 | console.log(`[===== Error in closeTab =====]`, error); 128 | } 129 | }; 130 | 131 | /** 132 | * send message 133 | */ 134 | sendMessage = async (tab: Tabs.Tab, msg: EXTMessage) => { 135 | try { 136 | const res = await tabs.sendMessage(tab.id ?? 0, msg); 137 | return res; 138 | } catch (error) { 139 | console.log(`[===== Error in sendMessage =====]`, error); 140 | return null; 141 | } 142 | }; 143 | } 144 | 145 | export const background = new Background(); 146 | -------------------------------------------------------------------------------- /src/baseManifest_chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "author": "WebDEV", 4 | "version": "3.0", 5 | "manifest_version": 3, 6 | "description": "__MSG_appDescription__", 7 | "icons": { 8 | "16": "assets/icons/icon-16.png", 9 | "24": "assets/icons/icon-24.png", 10 | "64": "assets/icons/icon-64.png", 11 | "128": "assets/icons/icon-128.png" 12 | }, 13 | "default_locale": "en", 14 | "content_scripts": [ 15 | { 16 | "matches": ["http://*/*", "https://*/*"], 17 | "js": ["content/content.js"] 18 | } 19 | ], 20 | "background": { 21 | "service_worker": "background/background.js" 22 | }, 23 | "permissions": ["tabs", "storage"], 24 | "host_permissions": ["http://*/*", "https://*/*"], 25 | "options_ui": { 26 | "page": "options/index.html" 27 | }, 28 | "action": { 29 | "default_icon": { 30 | "16": "assets/icons/icon-16.png", 31 | "48": "assets/icons/icon-48.png" 32 | }, 33 | "default_title": "Extension Boilerplate", 34 | "default_popup": "popup/index.html" 35 | }, 36 | "web_accessible_resources": [ 37 | { 38 | "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"], 39 | "matches": [""] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/baseManifest_edge.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "author": "WebDEV", 4 | "version": "1.0", 5 | "manifest_version": 3, 6 | "description": "__MSG_appDescription__", 7 | "icons": { 8 | "16": "assets/icons/icon-16.png", 9 | "24": "assets/icons/icon-24.png", 10 | "64": "assets/icons/icon-64.png", 11 | "128": "assets/icons/icon-128.png" 12 | }, 13 | "default_locale": "en", 14 | "content_scripts": [ 15 | { 16 | "matches": ["http://*/*", "https://*/*"], 17 | "js": ["content/content.js"] 18 | } 19 | ], 20 | "background": { 21 | "service_worker": "background/background.js" 22 | }, 23 | "host_permissions": ["http://*/*", "https://*/*"], 24 | "web_accessible_resources": [ 25 | { 26 | "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"], 27 | "matches": [""] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/baseManifest_firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "author": "WebDEV", 4 | "version": "3.0", 5 | "manifest_version": 3, 6 | "description": "__MSG_appDescription__", 7 | "icons": { 8 | "16": "assets/icons/icon-16.png", 9 | "24": "assets/icons/icon-24.png", 10 | "64": "assets/icons/icon-64.png", 11 | "128": "assets/icons/icon-128.png" 12 | }, 13 | "default_locale": "en", 14 | "content_scripts": [ 15 | { 16 | "matches": ["http://*/*", "https://*/*"], 17 | "js": ["content/content.js"] 18 | } 19 | ], 20 | "background": { 21 | "scripts": ["background/background.js"], 22 | "type": "module" 23 | }, 24 | "permissions": ["tabs", "storage"], 25 | "host_permissions": ["http://*/*", "https://*/*"], 26 | "options_ui": { 27 | "page": "options/index.html", 28 | "browser_style": false 29 | }, 30 | "action": { 31 | "default_icon": { 32 | "16": "assets/icons/icon-16.png", 33 | "48": "assets/icons/icon-48.png" 34 | }, 35 | "default_title": "Extension Boilerplate", 36 | "default_popup": "popup/index.html", 37 | "browser_style": false 38 | }, 39 | "web_accessible_resources": [ 40 | { 41 | "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"], 42 | "matches": [""] 43 | } 44 | ], 45 | "browser_specific_settings": { 46 | "gecko": { 47 | "id": "react-extension-boilerplate@test.com" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/baseManifest_opera.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "author": "WebDEV", 4 | "version": "1.0", 5 | "manifest_version": 3, 6 | "description": "__MSG_appDescription__", 7 | "icons": { 8 | "16": "assets/icons/icon-16.png", 9 | "24": "assets/icons/icon-24.png", 10 | "64": "assets/icons/icon-64.png", 11 | "128": "assets/icons/icon-128.png" 12 | }, 13 | "default_locale": "en", 14 | "content_scripts": [ 15 | { 16 | "matches": ["http://*/*", "https://*/*"], 17 | "js": ["content/content.js"] 18 | } 19 | ], 20 | "background": { 21 | "service_worker": "background/background.js" 22 | }, 23 | "host_permissions": ["http://*/*", "https://*/*"], 24 | "web_accessible_resources": [ 25 | { 26 | "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"], 27 | "matches": [""] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Button/button.scss: -------------------------------------------------------------------------------- 1 | .icbs-button { 2 | width: 100px; 3 | padding: 5px 10px; 4 | margin: 10px; 5 | text-transform: capitalize; 6 | background-color: #eeeeee; 7 | border-radius: 5px; 8 | font-size: 20px; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './button.scss'; 3 | 4 | export type ButtonProps = { 5 | action: (e: any) => void; 6 | label: string; 7 | }; 8 | 9 | export const Button = (props: ButtonProps) => ( 10 | 13 | ); 14 | 15 | export default Button; 16 | -------------------------------------------------------------------------------- /src/content/index.tsx: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | import { runtime } from 'webextension-polyfill'; 6 | import MessageListener from './messageListener'; 7 | 8 | // import "./content.css"; 9 | 10 | runtime.onMessage.addListener(MessageListener); 11 | 12 | class Main extends React.Component { 13 | render() { 14 | return ( 15 |
16 |

Hello world - My first Extension

17 |
18 | ); 19 | } 20 | } 21 | 22 | const app = document.createElement('div'); 23 | app.id = 'my-extension-root'; 24 | document.body.appendChild(app); 25 | const root = ReactDOM.createRoot(app); 26 | root.render(
); 27 | -------------------------------------------------------------------------------- /src/content/messageListener.ts: -------------------------------------------------------------------------------- 1 | import { Runtime } from 'webextension-polyfill'; 2 | 3 | export const onRequest = async ( 4 | msg: EXTMessage, 5 | sender: Runtime.SendMessageOptionsType 6 | ): Promise => { 7 | console.log('~~~~~~~', msg); 8 | try { 9 | switch (msg.type) { 10 | case 'CHANGE_COLOR': { 11 | document.body.style.background = msg?.data?.color; 12 | break; 13 | } 14 | default: 15 | return { type: 'SUCCESS' }; 16 | } 17 | } catch (error) { 18 | throw error; 19 | } 20 | }; 21 | 22 | export default onRequest; 23 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Options & Settings 9 | 10 | 11 | 12 |
13 |
14 |
15 |

Extension Boilerplate

16 |

17 | A foundation for creating cross-browser extensions 18 |

19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 |

29 | © Extension Boilerplate 30 |

31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /src/options/options.tsx: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | 6 | const Index = () =>
Hello React!
; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('display-container')!); 9 | root.render(); 10 | -------------------------------------------------------------------------------- /src/popup/Popup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from 'components/Button'; 3 | import { sendMessageToActiveTab } from 'utils/sendMessages'; 4 | 5 | function setGreen() { 6 | sendMessageToActiveTab({ type: 'CHANGE_COLOR', data: { color: 'green' } }); 7 | } 8 | 9 | function setRed() { 10 | sendMessageToActiveTab({ type: 'CHANGE_COLOR', data: { color: 'red' } }); 11 | } 12 | 13 | export const Popup = () => ( 14 |
15 |
18 | ); 19 | 20 | export default Popup; 21 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import Popup from './Popup'; 4 | 5 | const Index = () => ; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('display-container')!); 8 | root.render(); 9 | -------------------------------------------------------------------------------- /src/types/message.d.ts: -------------------------------------------------------------------------------- 1 | declare type EXTMessageType = 'CHANGE_COLOR'; 2 | 3 | declare type EXTMessage = { 4 | type: EXTMessageType; 5 | data?: T; 6 | }; 7 | -------------------------------------------------------------------------------- /src/types/openControl.d.ts: -------------------------------------------------------------------------------- 1 | declare type OpenControls = { 2 | isVisible: boolean; 3 | handleClose: () => void; 4 | handleOpen: () => void; 5 | handleToggle: () => void; 6 | isOpen: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/response.d.ts: -------------------------------------------------------------------------------- 1 | declare type EXTResponseType = 'SUCCESS' | 'FAILED' | 'PENDING' | 'UNAUTHORIZED' | 'AUTHENTICATED'; 2 | 3 | declare type EXTResponse = { 4 | type: EXTResponseType; 5 | data?: T; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/sendMessages.ts: -------------------------------------------------------------------------------- 1 | import { runtime, tabs, Runtime, Tabs } from 'webextension-polyfill'; 2 | 3 | /** 4 | * Send Message to Background Script 5 | * 6 | * @param msg 7 | * @returns 8 | */ 9 | export const sendMessage = (msg: EXTMessage, options?: Runtime.SendMessageOptionsType): Promise => { 10 | return runtime.sendMessage(msg, options); 11 | }; 12 | 13 | /** 14 | * Send Message to Content Script 15 | */ 16 | export const sendMessageToTab = ( 17 | tab: Tabs.Tab, 18 | msg: EXTMessage, 19 | options?: Tabs.SendMessageOptionsType 20 | ): Promise => { 21 | return tabs.sendMessage(tab.id as number, msg, options); 22 | }; 23 | 24 | /** 25 | * Send Message to Content Script 26 | */ 27 | export const sendMessageToActiveTab = async ( 28 | msg: EXTMessage, 29 | options?: Tabs.SendMessageOptionsType 30 | ): Promise => { 31 | let activeTab: Tabs.Tab; 32 | try { 33 | const activeTabs = await tabs.query({ active: true, currentWindow: true }); 34 | activeTab = activeTabs[0]; 35 | } catch (error) { 36 | console.log('[===== Error in sendMessageToActiveTab =====]', error); 37 | throw `Error in sendMessageToActiveTab`; 38 | } 39 | return sendMessageToTab(activeTab, msg, options); 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import { storage } from 'webextension-polyfill'; 2 | 3 | export default storage.sync ? storage.sync : storage.local; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "strictNullChecks": true, 5 | "noImplicitThis": true, 6 | "baseUrl": "./src", 7 | "esModuleInterop": true, 8 | "module": "commonjs", 9 | "target": "es5", 10 | "allowJs": true, 11 | "jsx": "react", 12 | "sourceMap": true, 13 | "moduleResolution": "node", 14 | "allowSyntheticDefaultImports": true, 15 | "allowUmdGlobalAccess": true, 16 | "resolveJsonModule": true, 17 | "paths": { 18 | "utils/*": ["./utils/*"], 19 | "popup/*": ["./popup/*"], 20 | "background/*": ["./background/*"], 21 | "options/*": ["./options/*"], 22 | "content/*": ["./content/*"], 23 | "assets/*": ["./assets/*"], 24 | "components/*": ["./components/*"], 25 | "types/*": ["./types/*"], 26 | "@redux/*": ["./@redux/*"], 27 | "@hooks/*": ["./hooks/*"] 28 | }, 29 | "typeRoots": ["node_modules/@types", "./src/types"] 30 | }, 31 | "exclude": ["dist", "dev", "temp"] 32 | } 33 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import TerserPlugin from 'terser-webpack-plugin'; 2 | 3 | import { 4 | getHTMLPlugins, 5 | getOutput, 6 | getCopyPlugins, 7 | getZipPlugins, 8 | getEntry, 9 | getResolves, 10 | getDefinePlugins, 11 | getCleanWebpackPlugins, 12 | config, 13 | getExtensionManifestPlugins, 14 | getEslintPlugins, 15 | getProgressPlugins, 16 | getExtensionReloaderPlugins, 17 | Directories, 18 | getAnalyzerPlugins, 19 | } from './webpack.config.utils'; 20 | 21 | let generalConfig: any = { 22 | mode: config.NODE_ENV === 'production' || config.NODE_ENV === 'upload' ? 'production' : 'development', 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(js|jsx|ts|tsx)$/, 27 | use: [ 28 | { 29 | loader: 'ts-loader', 30 | // options: { 31 | // transpileOnly: true, 32 | // }, 33 | }, 34 | ], 35 | exclude: /node_modules/, 36 | }, 37 | { 38 | test: /\.scss$/, 39 | use: [ 40 | { 41 | loader: 'style-loader', 42 | }, 43 | { 44 | loader: 'css-loader', 45 | }, 46 | { 47 | loader: 'sass-loader', 48 | }, 49 | ], 50 | }, 51 | ], 52 | }, 53 | resolve: getResolves(), 54 | entry: getEntry(Directories.SRC_DIR), 55 | output: getOutput(config.TARGET, config.OUTPUT_DIR), 56 | }; 57 | 58 | let plugins: any[] = [ 59 | ...getCleanWebpackPlugins(`${config.OUTPUT_DIR}/${config.TARGET}`, `${Directories.DIST_DIR}/${config.TARGET}`), 60 | ...getProgressPlugins(), 61 | ...getEslintPlugins(), 62 | ...getDefinePlugins(), 63 | ...getExtensionManifestPlugins(), 64 | ...getHTMLPlugins(config.TARGET, config.OUTPUT_DIR, Directories.SRC_DIR), 65 | ...getCopyPlugins(config.TARGET, config.OUTPUT_DIR, Directories.SRC_DIR), 66 | ]; 67 | 68 | if (config.NODE_ENV === 'development') { 69 | generalConfig = { 70 | ...generalConfig, 71 | devtool: 'source-map', 72 | stats: { 73 | all: false, 74 | builtAt: true, 75 | errors: true, 76 | hash: true, 77 | }, 78 | watch: true, 79 | watchOptions: { 80 | aggregateTimeout: 200, 81 | poll: 1000, 82 | }, 83 | }; 84 | 85 | plugins = [...plugins, ...getExtensionReloaderPlugins()]; 86 | } 87 | 88 | if (config.NODE_ENV === 'profile') { 89 | generalConfig = { 90 | ...generalConfig, 91 | devtool: 'source-map', 92 | stats: { 93 | all: false, 94 | builtAt: true, 95 | errors: true, 96 | hash: true, 97 | }, 98 | }; 99 | 100 | plugins = [...plugins, ...getAnalyzerPlugins()]; 101 | } 102 | 103 | if (config.NODE_ENV === 'upload') { 104 | generalConfig = { 105 | ...generalConfig, 106 | optimization: { 107 | minimize: true, 108 | minimizer: [ 109 | new TerserPlugin({ 110 | parallel: true, 111 | terserOptions: { 112 | format: { 113 | comments: false, 114 | }, 115 | }, 116 | extractComments: false, 117 | }), 118 | ], 119 | }, 120 | }; 121 | 122 | plugins = [...plugins]; 123 | } 124 | 125 | if (config.NODE_ENV === 'production') { 126 | generalConfig = { 127 | ...generalConfig, 128 | optimization: { 129 | minimize: true, 130 | minimizer: [ 131 | new TerserPlugin({ 132 | parallel: true, 133 | terserOptions: { 134 | format: { 135 | comments: false, 136 | }, 137 | }, 138 | extractComments: false, 139 | }), 140 | ], 141 | }, 142 | }; 143 | 144 | plugins = [...plugins, ...getZipPlugins(config.TARGET, Directories.DIST_DIR)]; 145 | } 146 | 147 | export default [ 148 | { 149 | ...generalConfig, 150 | plugins, 151 | }, 152 | ]; 153 | -------------------------------------------------------------------------------- /webpack.config.utils.ts: -------------------------------------------------------------------------------- 1 | import { ProgressPlugin, DefinePlugin } from 'webpack'; 2 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 3 | import CopyWebpackPlugin from 'copy-webpack-plugin'; 4 | import ZipPlugin from 'zip-webpack-plugin'; 5 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 6 | 7 | import path from 'path'; 8 | import { CleanWebpackPlugin } from 'clean-webpack-plugin'; 9 | import ESLintPlugin from 'eslint-webpack-plugin'; 10 | import WebpackExtensionManifestPlugin from 'webpack-extension-manifest-plugin'; 11 | 12 | const ExtReloader = require('webpack-ext-reloader-mv3'); 13 | 14 | const baseManifestChrome = require('./src/baseManifest_chrome.json'); 15 | const baseManifestFirefox = require('./src/baseManifest_firefox.json'); 16 | const baseManifestOpera = require('./src/baseManifest_opera.json'); 17 | const baseManifestEdge = require('./src/baseManifest_edge.json'); 18 | 19 | const baseManifest = { 20 | chrome: baseManifestChrome, 21 | firefox: baseManifestFirefox, 22 | opera: baseManifestOpera, 23 | edge: baseManifestEdge, 24 | }; 25 | 26 | const dotenv = require('dotenv').config({ path: __dirname + '/.env' }); 27 | 28 | interface EnvironmentConfig { 29 | NODE_ENV: string; 30 | OUTPUT_DIR: string; 31 | TARGET: string; 32 | } 33 | 34 | export const Directories = { 35 | DEV_DIR: 'dev', 36 | DIST_DIR: 'dist', 37 | TEMP_DIR: 'temp', 38 | SRC_DIR: 'src', 39 | }; 40 | 41 | /** 42 | * Environment Config 43 | * 44 | */ 45 | const EnvConfig: EnvironmentConfig = { 46 | OUTPUT_DIR: 47 | process.env.NODE_ENV === 'production' 48 | ? Directories.TEMP_DIR 49 | : process.env.NODE_ENV === 'upload' 50 | ? Directories.DIST_DIR 51 | : Directories.DEV_DIR, 52 | ...(process.env.NODE_ENV ? { NODE_ENV: process.env.NODE_ENV } : { NODE_ENV: 'development' }), 53 | ...(process.env.TARGET ? { TARGET: process.env.TARGET } : { TARGET: 'chrome' }), 54 | }; 55 | 56 | /** 57 | * Get HTML Plugins 58 | * 59 | * @param browserDir 60 | * @param outputDir 61 | * @param sourceDir 62 | * @returns 63 | */ 64 | export const getHTMLPlugins = ( 65 | browserDir: string, 66 | outputDir = Directories.DEV_DIR, 67 | sourceDir = Directories.SRC_DIR, 68 | ) => [ 69 | new HtmlWebpackPlugin({ 70 | title: 'Popup', 71 | filename: path.resolve(__dirname, `${outputDir}/${browserDir}/popup/index.html`), 72 | template: path.resolve(__dirname, `${sourceDir}/popup/index.html`), 73 | chunks: ['popup'], 74 | }), 75 | new HtmlWebpackPlugin({ 76 | title: 'Options', 77 | filename: path.resolve(__dirname, `${outputDir}/${browserDir}/options/index.html`), 78 | template: path.resolve(__dirname, `${sourceDir}/options/index.html`), 79 | chunks: ['options'], 80 | }), 81 | ]; 82 | 83 | /** 84 | * Get DefinePlugins 85 | * 86 | * @param config 87 | * @returns 88 | */ 89 | export const getDefinePlugins = (config = {}) => [ 90 | new DefinePlugin({ 91 | 'process.env': JSON.stringify({ ...config, ...(dotenv.parsed ?? {}) }), 92 | }), 93 | ]; 94 | 95 | /** 96 | * Get Output Configurations 97 | * 98 | * @param browserDir 99 | * @param outputDir 100 | * @returns 101 | */ 102 | export const getOutput = (browserDir: string, outputDir = Directories.DEV_DIR) => { 103 | return { 104 | path: path.resolve(process.cwd(), `${outputDir}/${browserDir}`), 105 | filename: '[name]/[name].js', 106 | }; 107 | }; 108 | 109 | /** 110 | * Get Entry Points 111 | * 112 | * @param sourceDir 113 | * @returns 114 | */ 115 | export const getEntry = (sourceDir = Directories.SRC_DIR) => { 116 | return { 117 | popup: [path.resolve(__dirname, `${sourceDir}/popup/index.tsx`)], 118 | options: [path.resolve(__dirname, `${sourceDir}/options/options.tsx`)], 119 | content: [path.resolve(__dirname, `${sourceDir}/content/index.tsx`)], 120 | background: [path.resolve(__dirname, `${sourceDir}/background/index.ts`)], 121 | }; 122 | }; 123 | 124 | /** 125 | * Get CopyPlugins 126 | * 127 | * @param browserDir 128 | * @param outputDir 129 | * @param sourceDir 130 | * @returns 131 | */ 132 | export const getCopyPlugins = ( 133 | browserDir: string, 134 | outputDir = Directories.DEV_DIR, 135 | sourceDir = Directories.SRC_DIR, 136 | ) => { 137 | return [ 138 | new CopyWebpackPlugin({ 139 | patterns: [ 140 | { 141 | from: path.resolve(__dirname, `${sourceDir}/assets`), 142 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/assets`), 143 | }, 144 | { 145 | from: path.resolve(__dirname, `${sourceDir}/_locales`), 146 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/_locales`), 147 | }, 148 | ], 149 | }), 150 | ]; 151 | }; 152 | 153 | /** 154 | * Get ZipPlugins 155 | * 156 | * @param browserDir 157 | * @param outputDir 158 | * @returns 159 | */ 160 | export const getZipPlugins = (browserDir: string, outputDir = Directories.DIST_DIR) => { 161 | return [ 162 | new ZipPlugin({ 163 | path: path.resolve(process.cwd(), `${outputDir}/${browserDir}`), 164 | filename: browserDir, 165 | extension: 'zip', 166 | fileOptions: { 167 | mtime: new Date(), 168 | mode: 0o100664, 169 | compress: true, 170 | forceZip64Format: false, 171 | }, 172 | zipOptions: { 173 | forceZip64Format: false, 174 | }, 175 | }), 176 | ]; 177 | }; 178 | 179 | /** 180 | * Get Analyzer Plugins 181 | * 182 | * @returns 183 | */ 184 | export const getAnalyzerPlugins = () => { 185 | return [ 186 | new BundleAnalyzerPlugin({ 187 | analyzerMode: 'server', 188 | }), 189 | ]; 190 | }; 191 | 192 | /** 193 | * Get CleanWebpackPlugins 194 | * 195 | * @param dirs 196 | * @returns 197 | */ 198 | export const getCleanWebpackPlugins = (...dirs: string[]) => { 199 | return [ 200 | new CleanWebpackPlugin({ 201 | cleanOnceBeforeBuildPatterns: [...dirs?.map((dir) => path.join(process.cwd(), `${dir}`) ?? [])], 202 | cleanStaleWebpackAssets: false, 203 | verbose: true, 204 | }), 205 | ]; 206 | }; 207 | 208 | /** 209 | * Get Resolves 210 | * 211 | * @returns 212 | */ 213 | export const getResolves = () => { 214 | return { 215 | alias: { 216 | utils: path.resolve(__dirname, './src/utils/'), 217 | popup: path.resolve(__dirname, './src/popup/'), 218 | background: path.resolve(__dirname, './src/background/'), 219 | options: path.resolve(__dirname, './src/options/'), 220 | content: path.resolve(__dirname, './src/content/'), 221 | assets: path.resolve(__dirname, './src/assets/'), 222 | components: path.resolve(__dirname, './src/components/'), 223 | types: path.resolve(__dirname, './src/types/'), 224 | hooks: path.resolve(__dirname, './src/hooks/'), 225 | '@redux': path.resolve(__dirname, './src/@redux/'), 226 | }, 227 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 228 | }; 229 | }; 230 | 231 | /** 232 | * Get Extension Manifest Plugins 233 | * 234 | * @returns 235 | */ 236 | export const getExtensionManifestPlugins = () => { 237 | return [ 238 | new WebpackExtensionManifestPlugin({ 239 | config: { base: (baseManifest as any)[EnvConfig.TARGET] }, 240 | }), 241 | ]; 242 | }; 243 | 244 | export const eslintOptions = { 245 | fix: true, 246 | }; 247 | 248 | /** 249 | * Get Eslint Plugins 250 | * 251 | * @returns 252 | */ 253 | export const getEslintPlugins = (options = eslintOptions) => { 254 | return [new ESLintPlugin(options)]; 255 | }; 256 | 257 | /** 258 | * Get Progress Plugins 259 | * 260 | * @returns 261 | */ 262 | export const getProgressPlugins = () => { 263 | return [new ProgressPlugin()]; 264 | }; 265 | 266 | /** 267 | * Environment Configuration Variables 268 | * 269 | */ 270 | export const config = EnvConfig; 271 | 272 | /** 273 | * Get Extension Reloader Plugin 274 | * 275 | * @returns 276 | */ 277 | export const getExtensionReloaderPlugins = () => { 278 | return [ 279 | new ExtReloader({ 280 | port: 9090, 281 | reloadPage: true, 282 | entries: { 283 | contentScript: ['content'], 284 | background: 'background', 285 | extensionPage: ['popup', 'options'], 286 | }, 287 | }), 288 | ]; 289 | }; 290 | --------------------------------------------------------------------------------