├── .babelrc.js ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── README.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── src ├── background │ └── background.ts ├── components │ ├── Options │ │ ├── Options.scss │ │ ├── Options.tsx │ │ └── index.tsx │ ├── Popup │ │ ├── Popup.scss │ │ ├── Popup.tsx │ │ └── index.tsx │ └── Welcome │ │ ├── Welcome.scss │ │ ├── Welcome.tsx │ │ └── index.tsx ├── contentScripts │ └── example.ts ├── images │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-48.png │ └── icon.svg ├── index.html ├── manifest.json ├── types │ └── files.d.ts └── utils │ └── platform.ts ├── tsconfig.json └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Presets are run in reversed order: https://babeljs.io/docs/en/presets#preset-ordering 3 | presets: [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | targets: "last 2 Chrome versions", 8 | }, 9 | ], 10 | "@babel/preset-react", 11 | ], 12 | plugins: [ 13 | "@babel/plugin-proposal-class-properties", 14 | "@babel/plugin-proposal-nullish-coalescing-operator", 15 | "@babel/plugin-proposal-optional-chaining", 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: ["react-app"], 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist-dev/ 3 | dist-prod/ 4 | key.pem 5 | .eslintcache -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore build artifacts 2 | dist-dev 3 | dist-prod 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extension Starter Kit 2 | 3 | 🔥 Starter kit for building Chrome/Firefox extensions 4 | 5 | ### Supported 6 | 7 | - Typescript 8 | - React 9 | - Bootstrap 10 | - CSS Modules 11 | - Live reloading 12 | 13 | ## How It Works 14 | 15 | The extension starter kit uses [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) which allows you to build extensions that automatically work on both Chrome and Firefox! 16 | 17 | ### Content Scripts 18 | 19 | An example content script is included in `src/contentScripts/example.ts`. Webpack outputs processed files as `[name].bundle.js`, so in `manifest.json`, the content script is loaded as `example.bundle.js`. 20 | 21 | After loading Extension Starter Kit, visit http://example.com and open the console to see the example content script in action. 22 | 23 | ## Development 24 | 25 | ### Chrome 26 | 27 | 1. Run `npm run develop` to start webpack-dev-server 28 | 2. Open `chrome://extensions/` and load the extension in the `dist-dev` directory 29 | 30 | The following parts of the extension will be automatically updated through live reload / hot module replacement: 31 | 32 | - Background script 33 | - Extension pages (e.g. options.html) 34 | 35 | Content scripts will **not** be automatically updated (unless loaded into non-SSL pages) and will require a refresh. 36 | 37 | ### Firefox 38 | 39 | 1. Run `npm run develop-firefox` to start webpack-dev-server 40 | 2. Load `build/manifest.json` at `about:debugging#/runtime/this-firefox` 41 | 42 | ## Releasing 43 | 44 | 1. Bump the version number in `package.json` 45 | 2. Create a git tag called `v${version}` using `git tag -a v` 46 | 3. Run `npm run build` (Chrome) or `npm run build-firefox` (Firefox) to create a production build inside the `dist-prod` directory. 47 | 4. Upload the zip to the Chrome developer dashboard and Firefox addons site 48 | 49 | If a copy of the source code is required, run `npm run zip-source` 50 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src/" 4 | }, 5 | "exclude": ["node_modules", "build"] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension-starter-kit", 3 | "version": "0.0.1", 4 | "description": "Extension Starter Kit", 5 | "private": true, 6 | "scripts": { 7 | "tsc": "tsc --noEmit", 8 | "build": "webpack --mode=production", 9 | "develop": "webpack-dev-server --mode=development", 10 | "develop-firefox": "npm run develop -- --env.platform=firefox", 11 | "build-firefox": "npm run build -- --env.platform=firefox", 12 | "crx": "mkdir -p dist && crx pack dist-prod/ -o dist-prod/build.crx", 13 | "zip-source": "zip -r dist-prod/source.zip . -x '*.git*' 'node_modules/*' 'dist-dev/*' 'dist-prod/*' key.pem" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/dkthehuman/extension-starter-kit.git" 18 | }, 19 | "author": "DK", 20 | "bugs": { 21 | "url": "https://github.com/dkthehuman/extension-starter-kit/issues" 22 | }, 23 | "homepage": "https://github.com/dkthehuman/extension-starter-kit#readme", 24 | "devDependencies": { 25 | "@babel/cli": "^7.12.8", 26 | "@babel/core": "^7.12.9", 27 | "@babel/plugin-proposal-class-properties": "^7.12.1", 28 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 29 | "@babel/plugin-proposal-optional-chaining": "^7.12.7", 30 | "@babel/preset-env": "^7.12.7", 31 | "@babel/preset-react": "^7.12.7", 32 | "@types/animejs": "^3.1.2", 33 | "@types/react": "^17.0.0", 34 | "@types/react-dom": "^17.0.0", 35 | "@types/react-router-dom": "^5.1.6", 36 | "@typescript-eslint/eslint-plugin": "^4.8.2", 37 | "@typescript-eslint/parser": "^4.8.2", 38 | "autoprefixer": "^10.0.4", 39 | "babel-eslint": "^10.1.0", 40 | "babel-loader": "^8.2.2", 41 | "clean-webpack-plugin": "^3.0.0", 42 | "copy-webpack-plugin": "^6.3.2", 43 | "crx": "^5.0.1", 44 | "css-loader": "^5.0.1", 45 | "eslint": "^7.14.0", 46 | "eslint-config-react-app": "^6.0.0", 47 | "eslint-loader": "^4.0.2", 48 | "eslint-plugin-flowtype": "^5.2.0", 49 | "eslint-plugin-import": "^2.22.1", 50 | "eslint-plugin-jsx-a11y": "^6.4.1", 51 | "eslint-plugin-react": "^7.21.5", 52 | "eslint-plugin-react-hooks": "^4.2.0", 53 | "file-loader": "^6.2.0", 54 | "html-webpack-plugin": "^4.5.0", 55 | "husky": "^4.3.5", 56 | "lint-staged": "^10.5.3", 57 | "postcss-loader": "^4.1.0", 58 | "prettier": "^2.2.1", 59 | "sass": "^1.29.0", 60 | "sass-loader": "^10.1.0", 61 | "style-loader": "^2.0.0", 62 | "ts-loader": "^8.0.11", 63 | "typescript": "^4.1.2", 64 | "webextension-polyfill": "^0.7.0", 65 | "webextension-polyfill-ts": "^0.22.0", 66 | "webpack": "^4.44.2", 67 | "webpack-cli": "^3.3.12", 68 | "webpack-dev-server": "^3.11.0", 69 | "zip-webpack-plugin": "^3.0.0" 70 | }, 71 | "dependencies": { 72 | "bootstrap": "^4.5.3", 73 | "jquery": "^3.5.0", 74 | "react": "^17.0.1", 75 | "react-dom": "^17.0.1" 76 | }, 77 | "husky": { 78 | "hooks": { 79 | "pre-commit": "lint-staged" 80 | } 81 | }, 82 | "lint-staged": { 83 | "*.{js,ts,tsx}": "eslint --cache --fix", 84 | "*.{js,ts,tsx,css,md}": "prettier --write" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/background/background.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "webextension-polyfill-ts"; 2 | 3 | import "images/icon-16.png"; 4 | import "images/icon-48.png"; 5 | import "images/icon-128.png"; 6 | 7 | browser.runtime.onInstalled.addListener(async ({ reason }) => { 8 | if (reason === "install") { 9 | return browser.tabs.create({ 10 | url: browser.runtime.getURL("welcome.html"), 11 | }); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/Options/Options.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | 3 | #root { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | min-height: 100vh; 8 | } 9 | 10 | .Options { 11 | max-width: 700px; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Options/Options.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./Options.scss"; 4 | 5 | function Options() { 6 | return ( 7 |
8 |

Settings

9 |
10 | ); 11 | } 12 | 13 | export default Options; 14 | -------------------------------------------------------------------------------- /src/components/Options/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Options from "./Options"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /src/components/Popup/Popup.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | 3 | .Popup { 4 | min-width: 300px; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Popup/Popup.tsx: -------------------------------------------------------------------------------- 1 | import { browser } from "webextension-polyfill-ts"; 2 | import React from "react"; 3 | 4 | import "./Popup.scss"; 5 | 6 | function Popup() { 7 | return ( 8 |
9 | Popup! 10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | function SettingsButton() { 18 | return ( 19 | 29 | ); 30 | } 31 | 32 | export default Popup; 33 | -------------------------------------------------------------------------------- /src/components/Popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Popup from "./Popup"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /src/components/Welcome/Welcome.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | 3 | #root { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | min-height: 100vh; 8 | } 9 | 10 | .Welcome { 11 | max-width: 700px; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Welcome/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { isFirefox } from "utils/platform"; 4 | import logo from "images/icon.svg"; 5 | 6 | import "./Welcome.scss"; 7 | 8 | function Welcome() { 9 | return ( 10 |
11 | 12 |

Welcome to Extension Starter Kit

13 | Welcome {isFirefox ? "Firefox" : "Chrome"} user! 14 |
15 | ); 16 | } 17 | 18 | function Logo() { 19 | return ( 20 | logo 21 | ); 22 | } 23 | 24 | export default Welcome; 25 | -------------------------------------------------------------------------------- /src/components/Welcome/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Welcome from "./Welcome"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /src/contentScripts/example.ts: -------------------------------------------------------------------------------- 1 | console.log("🔥 Hello from Extension Starter Kit! 🔥"); 2 | -------------------------------------------------------------------------------- /src/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namukang/extension-starter-kit/57a8ec24093a449e1477cef22ab57ddd2cb4515f/src/images/icon-128.png -------------------------------------------------------------------------------- /src/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namukang/extension-starter-kit/57a8ec24093a449e1477cef22ab57ddd2cb4515f/src/images/icon-16.png -------------------------------------------------------------------------------- /src/images/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namukang/extension-starter-kit/57a8ec24093a449e1477cef22ab57ddd2cb4515f/src/images/icon-48.png -------------------------------------------------------------------------------- /src/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Extension Starter Kit 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Extension Starter Kit", 3 | "background": { 4 | "scripts": ["background.bundle.js"] 5 | }, 6 | "permissions": ["storage"], 7 | "manifest_version": 2, 8 | "icons": { 9 | "16": "icon-16.png", 10 | "48": "icon-48.png", 11 | "128": "icon-128.png" 12 | }, 13 | "browser_action": { 14 | "default_icon": "icon-16.png", 15 | "default_title": "Extension Starter Kit", 16 | "default_popup": "popup.html" 17 | }, 18 | "options_ui": { 19 | "page": "options.html", 20 | "open_in_tab": true 21 | }, 22 | "content_scripts": [ 23 | { 24 | "matches": ["*://example.com/"], 25 | "js": ["example.bundle.js"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/types/files.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.mp4" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.module.scss" { 12 | const content: { [className: string]: string }; 13 | export default content; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/platform.ts: -------------------------------------------------------------------------------- 1 | // Defined by webpack to be used at compile time 2 | declare const PLATFORM: "firefox" | "chrome"; 3 | 4 | export const isFirefox = PLATFORM === "firefox"; 5 | export const isChrome = PLATFORM === "chrome"; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Target latest version of ECMAScript 4 | "target": "esnext", 5 | // Generate module code using latest version of ECMAScript 6 | "module": "esnext", 7 | // Keep JSX as part of output to be consumed by Babel 8 | "jsx": "preserve", 9 | // Search under node_modules for non-relative imports. 10 | "moduleResolution": "node", 11 | // Base directory to resolve non-relative module names 12 | "baseUrl": "src/", 13 | // Import non-ES modules as default imports 14 | "esModuleInterop": true, 15 | // Enable strictest settings 16 | "strict": true, 17 | // Generate corresponding .map file 18 | "sourceMap": true 19 | }, 20 | "include": ["src", "tests"], 21 | "compileOnSave": false 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const packageJson = require("./package.json"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 6 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 7 | const ZipPlugin = require("zip-webpack-plugin"); 8 | 9 | // Regexes for style files 10 | const cssRegex = /\.css$/; 11 | const cssModuleRegex = /\.module\.css$/; 12 | const sassRegex = /\.(scss|sass)$/; 13 | const sassModuleRegex = /\.module\.(scss|sass)$/; 14 | 15 | function getStyleLoaders({ cssOptions = {}, preProcessor } = {}) { 16 | const loaders = [ 17 | // Turn CSS into JS modules that inject