├── .eslintrc ├── .gitignore ├── LICENSE.txt ├── README.md ├── config.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── _locales │ └── en │ │ └── messages.json ├── assets │ └── icons │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-19.png │ │ ├── icon-38.png │ │ └── icon-64.png ├── background │ └── background.js ├── content │ └── content.js ├── manifest-ff.json ├── manifest.json ├── options │ ├── index.html │ └── options.jsx ├── popup │ ├── components │ │ └── button │ │ │ ├── button.jsx │ │ │ ├── button.scss │ │ │ └── button.spec.js │ ├── containers │ │ └── popup │ │ │ ├── popup.jsx │ │ │ └── popup.spec.js │ ├── index.html │ ├── popup.jsx │ └── services │ │ └── comunicationManager.js └── utils │ ├── ext.js │ ├── hot-reload.js │ └── storage.js ├── webpack.config.dev.js ├── webpack.config.dist.js ├── webpack.tests.js └── webpack.utils.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | dev 4 | temp 5 | .idea 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kamil Mikosz 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 |
2 |

3 | Webpack React Extension Boilerplate 4 |

5 | 6 |

7 | Works for Chrome, Opera, Edge & Firefox. 8 |

9 |

10 | This plugin is higly inspired by extension-boilerplate (https://github.com/EmailThis/extension-boilerplate) 11 |

12 |
13 | 14 | ## Features 15 | 16 |
17 |
Write in your favourite framework - React! :)
18 |
19 | Now you can create part of your exstensions in React framework - as you wish ;) 20 |
21 |
22 | 23 |
24 |
Write once and deploy to Chrome, Opera, Edge & Firefox
25 |
26 | Based on WebExtensions. It also includes a tiny polyfill to bring uniformity to the APIs exposed by different browsers. 27 |
28 |
29 | 30 |
31 |
Live-reload
32 |
33 | Your changes to CSS, HTML & JS files will be relayed instantly without having to manually reload the extension. This ends up saving a lot of time and improving the developer experience. Based on https://github.com/xpl/crx-hotreload 34 |
35 |
36 | 37 |
38 |
Newest js technology stack
39 |
40 | You can use ES6, ES5. 41 |
42 |
43 | 44 |
45 |
Comfortable styles import
46 |
47 | With react you can loas styles directy and you can use scss for styling. 48 |
49 |
50 | 51 |
52 |
Easily configurable and extendable
53 |
54 | Project use webpack so you can easly customize your project depends on your needs. In config.json you can define source path for each browser (if needed - default it's the same source), destintantion and develop directory. 55 |
56 |
57 | 58 |
59 |
Clean code
60 |
61 | Clean code is the best way for long term support for project. Boilerplate has fully configured eslint with airbnb styleguide. 62 |
63 |
64 | 65 |
66 |
Test your components!
67 |
68 | Project use some library which support your testing proces. As test runner we use karma, as testing framework mocha. As support to assertion we use chai. 69 |
70 |
71 | 72 | 73 | ## Installation 74 | 1. Clone the repository `git clone https://github.com/kamilogerto2/webpack-react-extension-boilerplate.git` 75 | 2. Run `npm install` 76 | 3. Run `npm run build` 77 | 78 | ##### Load the extension in Chrome & Opera 79 | 1. Open Chrome/Opera browser and navigate to chrome://extensions 80 | 2. Select "Developer Mode" and then click "Load unpacked extension..." 81 | 3. From the file browser, choose to `webpack-react-extension-boilerplate/dev/chrome` or (`wwebpack-react-extension-boilerplate/dev/opera`) 82 | 83 | 84 | ##### Load the extension in Firefox 85 | 1. Open Firefox browser and navigate to about:debugging 86 | 2. Click "Load Temporary Add-on" and from the file browser, choose `webpack-react-extension-boilerplate/dev/firefox` 87 | 88 | ##### Load the extension in Edge 89 | https://docs.microsoft.com/en-us/microsoft-edge/extensions/guides/adding-and-removing-extensions 90 | 91 | ## Developing 92 | The following tasks can be used when you want to start developing the extension and want to enable live reload - 93 | 94 | - `npm run watch-dev` 95 | 96 | 97 | ## Packaging 98 | Run `npm run build` to create a zipped, production-ready extension for each browser. You can then upload that to the appstore. 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. I'd be happy to hear your thoughts. 105 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "chromePath": "src", 3 | "operaPath": "src", 4 | "firefoxPath": "src", 5 | "devDirectory": "dev", 6 | "distDirectory": "dist", 7 | "tempDirectory": "temp" 8 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | browsers: ['Chrome'], // run in Chrome 4 | singleRun: true, // just run once by default 5 | frameworks: ['mocha'], // use the mocha test framework 6 | files: [ 7 | { pattern: 'webpack.tests.js', watched: false }, 8 | ], 9 | preprocessors: { 10 | 'webpack.tests.js': ['webpack', 'sourcemap'], 11 | }, 12 | reporters: ['dots'], // report results in this format 13 | webpack: { // kind of a copy of your webpack config 14 | devtool: 'inline-source-map', // just do inline source maps instead of the default 15 | mode: 'development', 16 | module: { 17 | rules: [ 18 | { 19 | loader: 'babel-loader', 20 | exclude: /node_modules/, 21 | test: /\.(js|jsx)$/, 22 | query: { 23 | presets: ['@babel/preset-env', '@babel/preset-react'], 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.jsx'], 27 | }, 28 | }, 29 | { 30 | test: /\.scss$/, 31 | use: [ 32 | { 33 | loader: 'style-loader', 34 | }, 35 | { 36 | loader: 'css-loader', 37 | }, 38 | { 39 | loader: 'sass-loader', 40 | }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | }, 46 | plugins: [ 47 | require("karma-mocha"), 48 | require("karma-webpack"), 49 | require("karma-sourcemap-loader"), 50 | require("karma-chrome-launcher"), 51 | ] 52 | }); 53 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension-webpack-boilerplate", 3 | "version": "0.0.2", 4 | "description": "Boilerplate for Chrome, FF, Safari exstension", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "test": "karma start", 8 | "start": "webpack --config webpack.config.dev.js", 9 | "watch-dev": "webpack --config webpack.config.dev.js --watch", 10 | "build": "webpack --config webpack.config.dist.js" 11 | }, 12 | "author": "Kamil Mikosz", 13 | "dependencies": { 14 | "@babel/core": "^7.5.5", 15 | "@babel/preset-env": "^7.5.5", 16 | "@babel/preset-react": "^7.0.0", 17 | "babel-loader": "^8.0.6", 18 | "chai": "^4.2.0", 19 | "clean-webpack-plugin": "^0.1.19", 20 | "copy-webpack-plugin": "^4.6.0", 21 | "css-loader": "^0.28.11", 22 | "enzyme": "^3.10.0", 23 | "enzyme-adapter-react-16": "^1.14.0", 24 | "eslint": "^4.19.1", 25 | "eslint-config-airbnb": "^16.1.0", 26 | "eslint-loader": "^2.2.1", 27 | "eslint-plugin-import": "^2.18.2", 28 | "eslint-plugin-jsx-a11y": "^6.2.3", 29 | "eslint-plugin-mocha": "^5.3.0", 30 | "eslint-plugin-react": "^7.14.3", 31 | "html-webpack-plugin": "^3.2.0", 32 | "jest-enzyme": "^6.1.2", 33 | "karma": "^2.0.5", 34 | "karma-chrome-launcher": "^2.2.0", 35 | "karma-cli": "^1.0.1", 36 | "karma-mocha": "^1.3.0", 37 | "karma-sourcemap-loader": "^0.3.7", 38 | "karma-webpack": "^3.0.5", 39 | "mocha": "^5.2.0", 40 | "node-sass": "^4.12.0", 41 | "prop-types": "^15.7.2", 42 | "react": "^16.8.6", 43 | "react-dom": "^16.8.6", 44 | "sass-loader": "^7.1.0", 45 | "style-loader": "^0.21.0", 46 | "uglifyjs-webpack-plugin": "^1.3.0", 47 | "webpack": "^4.37.0", 48 | "webpack-cli": "^3.3.6", 49 | "zip-webpack-plugin": "^3.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Ext React Starter", 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": "Ext Starter", 12 | "description": "Tooltip for the button." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilogerto2/webpack-react-extension-boilerplate/72a75bd34568c1ec65878b324cc0a378a2b2f517/src/assets/icons/icon-128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilogerto2/webpack-react-extension-boilerplate/72a75bd34568c1ec65878b324cc0a378a2b2f517/src/assets/icons/icon-16.png -------------------------------------------------------------------------------- /src/assets/icons/icon-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilogerto2/webpack-react-extension-boilerplate/72a75bd34568c1ec65878b324cc0a378a2b2f517/src/assets/icons/icon-19.png -------------------------------------------------------------------------------- /src/assets/icons/icon-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilogerto2/webpack-react-extension-boilerplate/72a75bd34568c1ec65878b324cc0a378a2b2f517/src/assets/icons/icon-38.png -------------------------------------------------------------------------------- /src/assets/icons/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilogerto2/webpack-react-extension-boilerplate/72a75bd34568c1ec65878b324cc0a378a2b2f517/src/assets/icons/icon-64.png -------------------------------------------------------------------------------- /src/background/background.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilogerto2/webpack-react-extension-boilerplate/72a75bd34568c1ec65878b324cc0a378a2b2f517/src/background/background.js -------------------------------------------------------------------------------- /src/content/content.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 3 | import ext from '../utils/ext'; 4 | 5 | function onRequest(request) { 6 | if (request.action === 'change-color') { 7 | document.body.style.background = request.data.color; 8 | } 9 | } 10 | 11 | ext.runtime.onMessage.addListener(onRequest); 12 | -------------------------------------------------------------------------------- /src/manifest-ff.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "version": "0.0.1", 4 | "manifest_version": 2, 5 | "description": "__MSG_appDescription__", 6 | "icons": { 7 | "16": "assets/icons/icon-16.png", 8 | "128": "assets/icons/icon-128.png" 9 | }, 10 | "default_locale": "en", 11 | "content_scripts": [ 12 | { 13 | "matches": [ 14 | "http://*/*", 15 | "https://*/*" 16 | ], 17 | "js": [ 18 | "content/content.js" 19 | ] 20 | } 21 | ], 22 | "background": { 23 | "scripts": [ 24 | "background/background.js" 25 | ] 26 | }, 27 | "permissions": [ 28 | "tabs", 29 | "storage", 30 | "http://*/*", 31 | "https://*/*" 32 | ], 33 | "options_ui": { 34 | "page": "options/index.html" 35 | }, 36 | "browser_action": { 37 | "default_icon": { 38 | "19": "assets/icons/icon-19.png", 39 | "38": "assets/icons/icon-38.png" 40 | }, 41 | "default_title": "Extension Boilerplate", 42 | "default_popup": "popup/index.html" 43 | }, 44 | "applications": { 45 | "gecko": { 46 | "id": "borderify@example.com" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "author": "Kamil Mikosz", 4 | "version": "0.0.1", 5 | "manifest_version": 2, 6 | "description": "__MSG_appDescription__", 7 | "icons": { 8 | "16": "assets/icons/icon-16.png", 9 | "128": "assets/icons/icon-128.png" 10 | }, 11 | "default_locale": "en", 12 | "content_scripts": [ 13 | { 14 | "matches": [ 15 | "http://*/*", 16 | "https://*/*" 17 | ], 18 | "js": [ 19 | "content/content.js" 20 | ] 21 | } 22 | ], 23 | "background": { 24 | "scripts": [ 25 | "background/background.js", 26 | "hotreload/hotreload.js" 27 | ], 28 | "persistent": true 29 | }, 30 | "permissions": [ 31 | "tabs", 32 | "storage", 33 | "http://*/*", 34 | "https://*/*" 35 | ], 36 | "options_ui": { 37 | "page": "options/index.html" 38 | }, 39 | "browser_action": { 40 | "default_icon": { 41 | "19": "assets/icons/icon-19.png", 42 | "38": "assets/icons/icon-38.png" 43 | }, 44 | "default_title": "Extension Boilerplate", 45 | "default_popup": "popup/index.html" 46 | } 47 | } -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Options & Settings 8 | 9 | 10 |
11 |
12 |
13 |

Extension Boilerplate

14 |

A foundation for creating cross-browser extensions

15 |
16 |
17 |
18 | 19 |
20 |
21 | 22 | 23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/options/options.jsx: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | const Index = () =>
Hello React!
; 7 | 8 | ReactDOM.render(, document.getElementById('display-container')); 9 | -------------------------------------------------------------------------------- /src/popup/components/button/button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './button.scss'; 4 | 5 | const Button = props => ; 6 | 7 | Button.propTypes = { 8 | action: PropTypes.func.isRequired, 9 | label: PropTypes.string.isRequired, 10 | }; 11 | 12 | export default Button; 13 | -------------------------------------------------------------------------------- /src/popup/components/button/button.scss: -------------------------------------------------------------------------------- 1 | .icbs-button { 2 | width: 100px; 3 | background-color: #eeeeee; 4 | } -------------------------------------------------------------------------------- /src/popup/components/button/button.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-dom/test-utils'; 3 | import { expect } from 'chai'; 4 | import Button from './button'; 5 | 6 | function sampleAction() { 7 | console.log('sampleAction'); 8 | } 9 | 10 | describe('button', function () { 11 | it('renders without problems', () => { 12 | const button = TestUtils.renderIntoDocument(
); 13 | expect(button).to.not.be.null; 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/popup/containers/popup/popup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '../../components/button/button'; 3 | import sendMessage from '../../services/comunicationManager'; 4 | 5 | function setGreen() { 6 | sendMessage('change-color', { color: 'green' }); 7 | } 8 | 9 | function setRed() { 10 | sendMessage('change-color', { color: 'red' }); 11 | } 12 | 13 | export default () => ( 14 |
15 |
18 | ); 19 | -------------------------------------------------------------------------------- /src/popup/containers/popup/popup.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import Popup from './popup'; 4 | import { mount } from 'enzyme'; 5 | 6 | describe('button', function () { 7 | it('renders without problems', () => { 8 | const popup = mount(); 9 | expect(popup).to.not.be.null; 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.title %> 5 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/popup/popup.jsx: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Popup from './containers/popup/popup'; 6 | 7 | const Index = () => ; 8 | 9 | ReactDOM.render(, document.getElementById('display-container')); 10 | -------------------------------------------------------------------------------- /src/popup/services/comunicationManager.js: -------------------------------------------------------------------------------- 1 | import ext from '../../utils/ext'; 2 | 3 | export default function sendMessage(message, data) { 4 | ext.tabs.query({ active: true, currentWindow: true }, (tabs) => { 5 | const activeTab = tabs[0]; 6 | ext.tabs.sendMessage(activeTab.id, { action: message, data }); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/ext.js: -------------------------------------------------------------------------------- 1 | /* global browser, window, chrome */ 2 | 3 | const apis = [ 4 | 'alarms', 5 | 'bookmarks', 6 | 'browserAction', 7 | 'commands', 8 | 'contextMenus', 9 | 'cookies', 10 | 'downloads', 11 | 'events', 12 | 'extension', 13 | 'extensionTypes', 14 | 'history', 15 | 'i18n', 16 | 'idle', 17 | 'notifications', 18 | 'pageAction', 19 | 'runtime', 20 | 'storage', 21 | 'tabs', 22 | 'webNavigation', 23 | 'webRequest', 24 | 'windows', 25 | ]; 26 | 27 | function Extension() { 28 | const self = this; 29 | 30 | apis.forEach((api) => { 31 | self[api] = null; 32 | 33 | try { 34 | if (chrome[api]) { 35 | self[api] = chrome[api]; 36 | } 37 | } catch (e) { 38 | return; 39 | } 40 | 41 | try { 42 | if (window[api]) { 43 | self[api] = window[api]; 44 | } 45 | } catch (e) { 46 | return; 47 | } 48 | 49 | 50 | try { 51 | if (browser[api]) { 52 | self[api] = browser[api]; 53 | } 54 | } catch (e) { 55 | return; 56 | } 57 | 58 | 59 | try { 60 | self.api = browser.extension[api]; 61 | } catch (e) { 62 | // I want application to not crush, but don't care about the message 63 | } 64 | }); 65 | 66 | try { 67 | if (browser && browser.runtime) { 68 | this.runtime = browser.runtime; 69 | } 70 | } catch (e) { 71 | return; 72 | } 73 | 74 | try { 75 | if (browser && browser.browserAction) { 76 | this.browserAction = browser.browserAction; 77 | } 78 | } catch (e) { 79 | // I want application to not crush, but don't care about the message 80 | } 81 | } 82 | 83 | module.exports = new Extension(); 84 | -------------------------------------------------------------------------------- /src/utils/hot-reload.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | 3 | const filesInDirectory = dir => new Promise(resolve => 4 | dir.createReader().readEntries(entries => 5 | Promise.all(entries.filter(e => e.name[0] !== '.').map(e => ( 6 | e.isDirectory 7 | ? filesInDirectory(e) 8 | : new Promise(resolvePromise => e.file(resolvePromise))))) 9 | .then(files => [].concat(...files)) 10 | .then(resolve))); 11 | 12 | const timestampForFilesInDirectory = dir => 13 | filesInDirectory(dir).then(files => 14 | files.map(f => f.name + f.lastModifiedDate).join()); 15 | 16 | const reload = () => { 17 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 18 | if (tabs[0]) { 19 | chrome.tabs.reload(tabs[0].id); 20 | } 21 | 22 | chrome.runtime.reload(); 23 | }); 24 | }; 25 | 26 | const watchChanges = (dir, lastTimestamp) => { 27 | timestampForFilesInDirectory(dir).then((timestamp) => { 28 | if (!lastTimestamp || (lastTimestamp === timestamp)) { 29 | setTimeout(() => watchChanges(dir, timestamp), 1000); 30 | } else { 31 | reload(); 32 | } 33 | }); 34 | }; 35 | 36 | if (chrome) { 37 | chrome.management.getSelf((self) => { 38 | if (self.installType === 'development') { 39 | chrome.runtime.getPackageDirectoryEntry(dir => watchChanges(dir)); 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | import ext from "./ext"; 2 | 3 | module.exports = (ext.storage.sync ? ext.storage.sync : ext.storage.local); -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const { getHTMLPlugins, getOutput, getCopyPlugins, getFirefoxCopyPlugins, getEntry } = require('./webpack.utils'); 2 | const config = require('./config.json'); 3 | 4 | const generalConfig = { 5 | mode: 'development', 6 | devtool: 'source-map', 7 | module: { 8 | rules: [ 9 | { 10 | loader: 'babel-loader', 11 | exclude: /node_modules/, 12 | test: /\.(js|jsx)$/, 13 | query: { 14 | presets: ['@babel/preset-env', '@babel/preset-react'], 15 | }, 16 | resolve: { 17 | extensions: ['.js', '.jsx'], 18 | }, 19 | }, 20 | { 21 | test: /\.(js|jsx)$/, 22 | exclude: /node_modules/, 23 | use: ['eslint-loader'], 24 | }, 25 | { 26 | test: /\.scss$/, 27 | use: [ 28 | { 29 | loader: 'style-loader', 30 | }, 31 | { 32 | loader: 'css-loader', 33 | }, 34 | { 35 | loader: 'sass-loader', 36 | }, 37 | ], 38 | }, 39 | ], 40 | }, 41 | }; 42 | 43 | module.exports = [ 44 | { 45 | ...generalConfig, 46 | entry: getEntry(config.chromePath), 47 | output: getOutput('chrome', config.devDirectory), 48 | plugins: [ 49 | ...getHTMLPlugins('chrome', config.devDirectory, config.chromePath), 50 | ...getCopyPlugins('chrome', config.devDirectory, config.chromePath), 51 | ], 52 | }, 53 | { 54 | ...generalConfig, 55 | entry: getEntry(config.operaPath), 56 | output: getOutput('opera', config.devDirectory), 57 | plugins: [ 58 | ...getHTMLPlugins('opera', config.devDirectory, config.operaPath), 59 | ...getCopyPlugins('opera', config.devDirectory, config.operaPath), 60 | ], 61 | }, 62 | { 63 | ...generalConfig, 64 | entry: getEntry(config.firefoxPath), 65 | output: getOutput('firefox', config.devDirectory), 66 | plugins: [ 67 | ...getFirefoxCopyPlugins('firefox', config.devDirectory, config.firefoxPath), 68 | ...getHTMLPlugins('firefox', config.devDirectory, config.firefoxPath), 69 | ], 70 | }, 71 | ]; 72 | -------------------------------------------------------------------------------- /webpack.config.dist.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | const { getHTMLPlugins, getOutput, getCopyPlugins, getZipPlugin, getFirefoxCopyPlugins, getEntry } = require('./webpack.utils'); 3 | const config = require('./config.json'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | 6 | const generalConfig = { 7 | mode: 'production', 8 | module: { 9 | rules: [ 10 | { 11 | loader: 'babel-loader', 12 | exclude: /node_modules/, 13 | test: /\.(js|jsx)$/, 14 | query: { 15 | presets: ['@babel/preset-env','@babel/preset-react'], 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx'], 19 | }, 20 | }, 21 | { 22 | test: /\.(js|jsx)$/, 23 | exclude: /node_modules/, 24 | use: ['eslint-loader'], 25 | }, 26 | { 27 | test: /\.scss$/, 28 | use: [ 29 | { 30 | loader: 'style-loader', 31 | }, 32 | { 33 | loader: 'css-loader', 34 | }, 35 | { 36 | loader: 'sass-loader', 37 | }, 38 | ], 39 | }, 40 | ], 41 | }, 42 | }; 43 | 44 | module.exports = [ 45 | { 46 | ...generalConfig, 47 | output: getOutput('chrome', config.tempDirectory), 48 | entry: getEntry(config.chromePath), 49 | plugins: [ 50 | new CleanWebpackPlugin(['dist', 'temp']), 51 | new UglifyJsPlugin(), 52 | ...getHTMLPlugins('chrome', config.tempDirectory, config.chromePath), 53 | ...getCopyPlugins('chrome', config.tempDirectory, config.chromePath), 54 | getZipPlugin('chrome', config.distDirectory), 55 | ], 56 | }, 57 | { 58 | ...generalConfig, 59 | output: getOutput('opera', config.tempDirectory), 60 | entry: getEntry(config.operaPath), 61 | plugins: [ 62 | new CleanWebpackPlugin(['dist', 'temp']), 63 | new UglifyJsPlugin(), 64 | ...getHTMLPlugins('opera', config.tempDirectory, config.operaPath), 65 | ...getCopyPlugins('opera', config.tempDirectory, config.operaPath), 66 | getZipPlugin('opera', config.distDirectory), 67 | ], 68 | }, 69 | { 70 | ...generalConfig, 71 | entry: getEntry(config.firefoxPath), 72 | output: getOutput('firefox', config.tempDirectory), 73 | plugins: [ 74 | new CleanWebpackPlugin(['dist', 'temp']), 75 | new UglifyJsPlugin(), 76 | ...getHTMLPlugins('firefox', config.tempDirectory, config.firefoxPath), 77 | ...getFirefoxCopyPlugins('firefox', config.tempDirectory, config.firefoxPath), 78 | getZipPlugin('firefox', config.distDirectory), 79 | ], 80 | }, 81 | ]; 82 | -------------------------------------------------------------------------------- /webpack.tests.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import EnzymeAdapter from 'enzyme-adapter-react-16'; 3 | 4 | Enzyme.configure({ adapter: new EnzymeAdapter() }); 5 | 6 | const context = require.context('./src', true, /spec\.js/); // make sure you have your directory and regex test set correctly! 7 | context.keys().forEach(context); 8 | -------------------------------------------------------------------------------- /webpack.utils.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const ZipPlugin = require('zip-webpack-plugin'); 4 | const path = require('path'); 5 | 6 | const getHTMLPlugins = (browserDir, outputDir = 'dev', sourceDir = 'src') => [ 7 | new HtmlWebpackPlugin({ 8 | title: 'Popup', 9 | filename: path.resolve(__dirname, `${outputDir}/${browserDir}/popup/index.html`), 10 | template: `${sourceDir}/popup/index.html`, 11 | chunks: ['popup'], 12 | }), 13 | new HtmlWebpackPlugin({ 14 | title: 'Options', 15 | filename: path.resolve(__dirname, `${outputDir}/${browserDir}/options/index.html`), 16 | template: `${sourceDir}/options/index.html`, 17 | chunks: ['options'], 18 | }), 19 | ]; 20 | 21 | const getOutput = (browserDir, outputDir = 'dev') => { 22 | return { 23 | path: path.resolve(__dirname, `${outputDir}/${browserDir}`), 24 | filename: '[name]/[name].js', 25 | }; 26 | }; 27 | 28 | const getEntry = (sourceDir = 'src') => { 29 | return { 30 | popup: path.resolve(__dirname, `${sourceDir}/popup/popup.jsx`), 31 | options: path.resolve(__dirname, `${sourceDir}/options/options.jsx`), 32 | content: path.resolve(__dirname, `${sourceDir}/content/content.js`), 33 | background: path.resolve(__dirname, `${sourceDir}/background/background.js`), 34 | hotreload: path.resolve(__dirname, `${sourceDir}/utils/hot-reload.js`), 35 | }; 36 | }; 37 | 38 | const getCopyPlugins = (browserDir, outputDir = 'dev', sourceDir = 'src') => [ 39 | new CopyWebpackPlugin([ 40 | { from: `${sourceDir}/assets`, to: path.resolve(__dirname, `${outputDir}/${browserDir}/assets`) }, 41 | { from: `${sourceDir}/_locales`, to: path.resolve(__dirname, `${outputDir}/${browserDir}/_locales`) }, 42 | { from: `${sourceDir}/manifest.json`, to: path.resolve(__dirname, `${outputDir}/${browserDir}/manifest.json`) }, 43 | ]), 44 | ]; 45 | 46 | const getFirefoxCopyPlugins = (browserDir, outputDir = 'dev', sourceDir = 'src') => [ 47 | new CopyWebpackPlugin([ 48 | { from: `${sourceDir}/assets`, to: path.resolve(__dirname, `${outputDir}/${browserDir}/assets`) }, 49 | { from: `${sourceDir}/_locales`, to: path.resolve(__dirname, `${outputDir}/${browserDir}/_locales`) }, 50 | { from: `${sourceDir}/manifest-ff.json`, to: path.resolve(__dirname, `${outputDir}/${browserDir}/manifest.json`) }, 51 | ]), 52 | ]; 53 | 54 | const getZipPlugin = (browserDir, outputDir = 'dist') => 55 | new ZipPlugin({ 56 | path: path.resolve(__dirname, `${outputDir}/${browserDir}`), 57 | filename: browserDir, 58 | extension: 'zip', 59 | fileOptions: { 60 | mtime: new Date(), 61 | mode: 0o100664, 62 | compress: true, 63 | forceZip64Format: false, 64 | }, 65 | zipOptions: { 66 | forceZip64Format: false, 67 | }, 68 | }); 69 | 70 | module.exports = { 71 | getHTMLPlugins, 72 | getOutput, 73 | getCopyPlugins, 74 | getFirefoxCopyPlugins, 75 | getZipPlugin, 76 | getEntry, 77 | }; 78 | --------------------------------------------------------------------------------