├── .gitattributes ├── .github └── workflows │ ├── build.yml │ ├── pr.yml │ ├── prerelease.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── README.md ├── images ├── devmode.png └── loaded.png ├── package-lock.json ├── package.json ├── scripts ├── build.js ├── env.js └── webpack.js ├── src ├── background-wrapper.js ├── background.js ├── content.js ├── manifest.json ├── pages │ └── help.html ├── popup.html └── popup.js └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: push 3 | 4 | jobs: 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node_version: [8.x, 10.x, 12.x] 12 | 13 | steps: 14 | - name: Checkout the code 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup Node ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build 26 | run: npm run build 27 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | pull_request: 7 | name: Build PR 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout the code 11 | uses: actions/checkout@v2 12 | 13 | - name: Setup Node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12.x 17 | 18 | - name: Install dependencies 19 | run: npm ci 20 | 21 | - name: Build 22 | run: npm run build 23 | 24 | - name: Upload artifacts 25 | uses: actions/upload-artifact@v2 26 | with: 27 | name: Build pr-${{ github.event.pull_request.number }} 28 | path: dist/ 29 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Beta Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "b*" 7 | 8 | jobs: 9 | pre-release: 10 | name: Beta Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout the code 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Node 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12.x 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Build 25 | run: npm run build 26 | 27 | - name: Packaged Extension Release 28 | uses: thedoctor0/zip-release@master 29 | with: 30 | path: dist/ 31 | filename: prerelease.zip 32 | exclusions: "*.git* *.github* .gitignore" 33 | 34 | - name: Create prerelease and Upload zip 35 | id: create_release 36 | uses: svenstaro/upload-release-action@2.1.0 37 | with: 38 | repo_token: ${{ secrets.TOKEN }} 39 | tag: ${{ github.ref }} 40 | release_name: ${{ github.ref }} 41 | file: prerelease.zip 42 | prerelease: true 43 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | release: 4 | types: [released] 5 | 6 | jobs: 7 | deploy: 8 | name: Deploy to chrome devconsole 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Start deployment 13 | uses: bobheadxi/deployments@v0.4.3 14 | id: deployment 15 | with: 16 | step: start 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | env: chrome-devconsole 19 | 20 | - name: Download assets 21 | env: 22 | FILE_NAME: release.zip 23 | run: | 24 | response=$(curl -sb -H ${{ github.event.release.assets_url }}) 25 | url=$(echo $response | jq -r --arg FILE_NAME "$FILE_NAME" '.[] | select(.name==$FILE_NAME) .browser_download_url') 26 | wget $url 27 | 28 | - name: Publish to chrome devconsole 29 | uses: Klemensas/chrome-extension-upload-action@master 30 | with: 31 | refresh-token: ${{ secrets.REFRESH_TOKEN }} 32 | client-id: ${{ secrets.CLIENT_ID }} 33 | client-secret: ${{ secrets.CLIENT_SECRET }} 34 | file-name: release.zip 35 | app-id: ${{ secrets.APP_ID }} 36 | publish: true 37 | 38 | - name: Update deployment status 39 | uses: bobheadxi/deployments@v0.4.3 40 | if: always() 41 | with: 42 | step: finish 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | status: ${{ job.status }} 45 | env_url: ${{ secrets.WEBSTORE_URL }} 46 | deployment_id: ${{ steps.deployment.outputs.deployment_id }} 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Chekout the code 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Node 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12.x 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Build 25 | run: npm run build 26 | 27 | - name: Packaged Extension Release 28 | uses: thedoctor0/zip-release@master 29 | with: 30 | path: dist/ 31 | filename: release.zip 32 | exclusions: "*.git* *.github* .gitignore" 33 | 34 | - name: Create release and Upload zip 35 | id: create_release 36 | uses: svenstaro/upload-release-action@2.1.0 37 | with: 38 | repo_token: ${{ secrets.TOKEN }} 39 | tag: ${{ github.ref }} 40 | release_name: ${{ github.ref }} 41 | file: release.zip 42 | prerelease: false 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # Dependency directories 26 | node_modules/ 27 | jspm_packages/ 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional eslint cache 33 | .eslintcache 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | 38 | # Output of 'npm pack' 39 | *.tgz 40 | 41 | # Yarn Integrity file 42 | .yarn-integrity 43 | 44 | # dotenv environment variables file 45 | .env 46 | 47 | # next.js build output 48 | .next 49 | 50 | # Lerna 51 | lerna-debug.log 52 | 53 | # System Files 54 | .DS_Store 55 | Thumbs.db 56 | 57 | # compiled output 58 | /dist 59 | /tmp 60 | /out-tsc 61 | 62 | # Runtime data 63 | pids 64 | *.pid 65 | *.seed 66 | *.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | lib-cov 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | 74 | # nyc test coverage 75 | .nyc_output 76 | 77 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 78 | .grunt 79 | 80 | # Bower dependency directory (https://bower.io/) 81 | bower_components 82 | 83 | # node-waf configuration 84 | .lock-wscript 85 | 86 | # IDEs and editors 87 | .idea 88 | .project 89 | .classpath 90 | .c9/ 91 | *.launch 92 | .settings/ 93 | *.sublime-workspace 94 | 95 | # IDE - VSCode 96 | .vscode/* 97 | !.vscode/settings.json 98 | !.vscode/tasks.json 99 | !.vscode/launch.json 100 | !.vscode/extensions.json 101 | 102 | # misc 103 | .sass-cache 104 | connect.lock 105 | typings 106 | 107 | # Logs 108 | logs 109 | *.log 110 | npm-debug.log* 111 | yarn-debug.log* 112 | yarn-error.log* 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-manifest-v3-webpack-hotreload-template 2 | Chrome Extension Template (Manifest v3) Webpack Hot reload 3 | 4 | ## Pre-requisite 5 | - [NodeJs](https://nodejs.org/) `v17.4.0` 6 | 7 | ## Project setup 8 | - Install dependencies 9 | ``` 10 | npm install 11 | ``` 12 | - Build for production 13 | ``` 14 | npm run build 15 | ``` 16 | - Start working on locally 17 | ``` 18 | npm run start 19 | ``` 20 | 21 | ## Load extension in browser locally 22 | - Turn on developer mode in (`chrome://extensions` or `edge://extensions`) 23 | ![Turn on developer mode](images/devmode.png) 24 | 25 | - Then click on `Load unpacked` 26 | - Select `dist` folder inside this repo folder(It will get generated after running either `build` or `start` command) 27 | 28 | - If you have run `start` command, you can see something like this in browser console 29 | ![Locally working](images/loaded.png) 30 | 31 | ## Features 32 | - Extension is working for `Chrome` and `Edge`. 33 | - Content Scripts 34 | - Popup 35 | - Service Worker (Background script) 36 | - Webpack Hot reload 37 | 38 | ## Note 39 | - Hot reload isn't working for content script in Manifest v3 -------------------------------------------------------------------------------- /images/devmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidehustlelab/chrome-manifest-v3-webpack-hotreload-template/070bd49c179cf45cbaae39da14d36f2dd9893188/images/devmode.png -------------------------------------------------------------------------------- /images/loaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidehustlelab/chrome-manifest-v3-webpack-hotreload-template/070bd49c179cf45cbaae39da14d36f2dd9893188/images/loaded.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-manifest-v3-webpack-hotreload-template", 3 | "version": "1.0.0", 4 | "description": "Template for Chrome Extension (Manifest v3) with webpack hot reloading", 5 | "main": "src/popup.js", 6 | "devDependencies": { 7 | "@babel/core": "^7.15.8", 8 | "@babel/plugin-proposal-class-properties": "^7.14.5", 9 | "@babel/preset-env": "^7.15.8", 10 | "@babel/preset-react": "^7.14.5", 11 | "@types/chrome": "^0.0.159", 12 | "babel-loader": "^8.2.2", 13 | "babel-preset-react-app": "^10.0.0", 14 | "clean-webpack-plugin": "^4.0.0", 15 | "copy-webpack-plugin": "^9.0.1", 16 | "css-loader": "^6.4.0", 17 | "file-loader": "^6.2.0", 18 | "html-loader": "^2.1.2", 19 | "html-webpack-plugin": "^5.3.2", 20 | "sass-loader": "^12.1.0", 21 | "style-loader": "^3.3.0", 22 | "webpack": "^5.58.1", 23 | "webpack-cli": "^4.9.0", 24 | "webpack-dev-server": "^4.3.1", 25 | "write-file-webpack-plugin": "^4.5.1" 26 | }, 27 | "scripts": { 28 | "build": "node scripts/build.js", 29 | "start": "node scripts/webpack.js" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/hirenchalodiya1/chrome-manifest-v3-webpack-hotreload-template.git" 34 | }, 35 | "keywords": [ 36 | "Chrome", 37 | "Extension", 38 | "Manifest", 39 | "v3", 40 | "Webpack", 41 | "hot", 42 | "reloading" 43 | ], 44 | "author": "Hiren Chalodiya", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/hirenchalodiya1/chrome-manifest-v3-webpack-hotreload-template/issues" 48 | }, 49 | "homepage": "https://github.com/hirenchalodiya1/chrome-manifest-v3-webpack-hotreload-template#readme" 50 | } 51 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | process.env.BABEL_ENV = "production"; 2 | process.env.NODE_ENV = "production"; 3 | 4 | var webpack = require("webpack"), 5 | config = require("../webpack.config"); 6 | 7 | delete config.chromeExtension; 8 | 9 | config.mode = "production"; 10 | 11 | webpack(config, function (err) { 12 | if (err) throw err; 13 | }); 14 | -------------------------------------------------------------------------------- /scripts/env.js: -------------------------------------------------------------------------------- 1 | // tiny wrapper with default env vars 2 | module.exports = { 3 | NODE_ENV: process.env.NODE_ENV || "development", 4 | PORT: process.env.PORT || 3000, 5 | }; 6 | -------------------------------------------------------------------------------- /scripts/webpack.js: -------------------------------------------------------------------------------- 1 | process.env.BABEL_ENV = "development"; 2 | process.env.NODE_ENV = "development"; 3 | 4 | var WebpackDevServer = require("webpack-dev-server"), 5 | webpack = require("webpack"), 6 | config = require("../webpack.config"), 7 | env = require("./env"), 8 | path = require("path"); 9 | 10 | var options = config.chromeExtension || {}; 11 | var excludeEntriesToHotReload = options.notHotReload || []; 12 | 13 | for (var entryName in config.entry) { 14 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) { 15 | config.entry[entryName] = [ 16 | "webpack/hot/dev-server", 17 | "webpack-dev-server/client?hot=true&hostname=localhost&port=" + env.PORT, 18 | ].concat(config.entry[entryName]); 19 | } 20 | } 21 | 22 | config.plugins = [new webpack.HotModuleReplacementPlugin()].concat( 23 | config.plugins || [] 24 | ); 25 | 26 | delete config.chromeExtensionBoilerplate; 27 | 28 | var compiler = webpack(config); 29 | 30 | var server = new WebpackDevServer( 31 | { 32 | https: false, 33 | hot: false, 34 | client: false, 35 | port: env.PORT, 36 | host: "localhost", 37 | static: { 38 | directory: path.join(__dirname, "../dist"), 39 | watch: false, 40 | }, 41 | headers: { 42 | "Access-Control-Allow-Origin": "*", 43 | }, 44 | devMiddleware: { 45 | publicPath: `http://localhost:${env.PORT}`, 46 | writeToDisk: true, 47 | }, 48 | allowedHosts: "all", 49 | }, 50 | compiler 51 | ); 52 | 53 | if (process.env.NODE_ENV === "development" && module.hot) { 54 | module.hot.accept(); 55 | } 56 | 57 | (async () => { 58 | await server.start(); 59 | })(); 60 | -------------------------------------------------------------------------------- /src/background-wrapper.js: -------------------------------------------------------------------------------- 1 | try { 2 | importScripts("background.bundle.js"); 3 | } catch (e) { 4 | console.error(e); 5 | } 6 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | console.log("Backgroup Script") 2 | 3 | chrome.runtime.onInstalled.addListener(() => { 4 | chrome.tabs.create({ url: chrome.runtime.getURL("/pages/help.html")}); 5 | }); 6 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | console.log('content.js injected'); 2 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chrome Extension Webpack Template", 3 | "manifest_version": 3, 4 | "action": { 5 | "default_popup": "popup.html", 6 | "default_title": "Open" 7 | }, 8 | "background": { 9 | "service_worker": "background-wrapper.js" 10 | }, 11 | "content_scripts": [ 12 | { 13 | "matches": ["https://google.com/*"], 14 | "js": ["content.bundle.js"] 15 | } 16 | ], 17 | "content_security_policy": { 18 | "extension_pages": "script-src 'self'; object-src 'self'" 19 | }, 20 | "permissions": ["scripting"], 21 | "host_permissions": ["https://google.com/"] 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Help !! 4 | 5 | 6 | Help 7 | 8 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | Popup Opened !! 11 |
12 | 13 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | console.log("Popup script loaded") -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"), 2 | path = require("path"), 3 | env = require("./scripts/env"), 4 | { CleanWebpackPlugin } = require("clean-webpack-plugin"), 5 | CopyWebpackPlugin = require("copy-webpack-plugin"), 6 | HtmlWebpackPlugin = require("html-webpack-plugin"), 7 | WriteFilePlugin = require("write-file-webpack-plugin"); 8 | 9 | var fileExtensions = [ 10 | "jpg", 11 | "jpeg", 12 | "png", 13 | "gif", 14 | "eot", 15 | "otf", 16 | "svg", 17 | "ttf", 18 | "woff", 19 | "woff2", 20 | ]; 21 | 22 | var options = { 23 | mode: process.env.NODE_ENV || "development", 24 | entry: { 25 | popup: path.join(__dirname, "src", "popup.js"), 26 | background: path.join(__dirname, "src", "background.js"), 27 | content: path.join(__dirname, "src", "content.js"), 28 | }, 29 | output: { 30 | globalObject: "this", 31 | path: path.resolve(__dirname, "dist"), 32 | filename: "[name].bundle.js", 33 | }, 34 | module: { 35 | rules: [ 36 | // { 37 | // // look for .css or .scss files 38 | // test: /\.(css|scss)$/, 39 | // // in the `src` directory 40 | // use: [ 41 | // { 42 | // loader: 'style-loader', 43 | // }, 44 | // { 45 | // loader: 'css-loader', 46 | // }, 47 | // { 48 | // loader: 'sass-loader', 49 | // options: { 50 | // sourceMap: true, 51 | // }, 52 | // }, 53 | // ], 54 | // }, 55 | // { 56 | // test: new RegExp('.(' + fileExtensions.join('|') + ')$'), 57 | // loader: 'file-loader?name=[name].[ext]', 58 | // exclude: /node_modules/, 59 | // }, 60 | { 61 | test: /\.html$/, 62 | loader: "html-loader", 63 | exclude: /node_modules/, 64 | }, 65 | { 66 | test: /\.(js|jsx)$/, 67 | loader: "babel-loader", 68 | exclude: /node_modules/, 69 | }, 70 | ], 71 | }, 72 | resolve: { 73 | extensions: fileExtensions 74 | .map((extension) => "." + extension) 75 | .concat([".jsx", ".js", ".css"]), 76 | }, 77 | plugins: [ 78 | new webpack.ProgressPlugin(), 79 | // clean the build folder 80 | new CleanWebpackPlugin({ 81 | verbose: true, 82 | cleanStaleWebpackAssets: false, 83 | }), 84 | // expose and write the allowed env vars on the compiled bundle 85 | new webpack.EnvironmentPlugin(["NODE_ENV"]), 86 | new CopyWebpackPlugin({ 87 | patterns: [ 88 | { 89 | from: "src/manifest.json", 90 | to: path.join(__dirname, "dist"), 91 | force: true, 92 | transform: function (content, path) { 93 | // generates the manifest file using the package.json informations 94 | return Buffer.from( 95 | JSON.stringify( 96 | { 97 | description: process.env.npm_package_description, 98 | version: process.env.npm_package_version, 99 | ...JSON.parse(content.toString()), 100 | }, 101 | null, 102 | "\t" 103 | ) 104 | ); 105 | }, 106 | }, 107 | { 108 | from: "src/background-wrapper.js", 109 | to: path.join(__dirname, "dist"), 110 | }, 111 | { 112 | from: "src/pages", 113 | to: path.join(__dirname, "dist", "pages"), 114 | }, 115 | ], 116 | }), 117 | new HtmlWebpackPlugin({ 118 | template: path.join(__dirname, "src", "popup.html"), 119 | filename: "popup.html", 120 | chunks: ["popup"], 121 | }), 122 | new WriteFilePlugin(), 123 | ], 124 | }; 125 | 126 | if (env.NODE_ENV === "development") { 127 | options.devtool = "cheap-module-source-map"; 128 | } 129 | 130 | module.exports = options; 131 | --------------------------------------------------------------------------------