├── .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 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | © Extension Boilerplate
28 |
29 |
30 |
31 |
32 |
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 => { props.label } ;
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 |
16 |
17 |
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 |
--------------------------------------------------------------------------------