Web Extensions - you might have heard of it, most probably you're already using it - example? AdBlock, Grammarly, Save to Pocket, etc. In this article, we will build a Web Extension from scratch using Reactjs.
We are going to bake a New Tab Greeting Extension and publish it on Chrome Web Store
and Firefox Addon Dashboard
.
Step 1 - Creating the App:
Let's create a React app with CRA:
npx create-react-app greetings-web-extension
cd greetings-web-extension
Let's do some quick cleanup:
Under the src
folder, remove everything else other than App.js
, index.js
, and index.css
.
From the public
folder - remove everything else other than index.html
and favicon.ico
.
In the end, we should have the following directory structure:
- greetings-web-extension
- node_modules
- public
- favicon.ico
- index.html
- src
- App.js
- index.css
- index.js
- .gitignore
- package.json
- README.md
- others
Step 2 - Preparing the App:
Quickly modify a few files as follows:
- /src/index.js
import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") );
- /src/index.css
html, body, #root { height: 100%; } body { margin: 0; text-align: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .App { height: 100%; background: linear-gradient(to bottom right, #7b4397, #dc2430); color: white; text-align: center; display: flex; justify-content: center; align-items: center; }
- /src/App.js
import React from "react"; const greetings = { morning: "Good morning", noon: "Good afternoon", evening: "Good evening", night: "Good night", }; class App extends React.Component { constructor() { super(); this.state = { greeting: greetings.morning, }; this.startTime = this.startTime.bind(this); } componentDidMount() { this.startTime(); } startTime() { let today = new Date(); let h = today.getHours(); let greeting; if (h > 6 && h < 12) { greeting = greetings.morning; } else if (h >= 12 && h < 17) { greeting = greetings.noon; } else if (h >= 17 && h < 20) { greeting = greetings.evening; } else { greeting = greetings.night; } this.setState({ greeting }); setTimeout(this.startTime, 1000); } render() { return ( <div className="App"> <h1>{this.state.greeting}</h1> </div> ); } } export default App;
- /public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <title>Greetings</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>
Step 3 - Testing React App:
Let's do a quick test of what we have at hand.
Fire it up with npm start
.
You should see something like this in your browser:
Step 4 - preparing the Web Extension:
The first thing we need is a manifest.json
file - which is a descriptor file for the web extension we are building.
Create a directory extras
, create a new file manifest.json
under it and Copy/Paste the following content:
{ "name": "greetings", "offline_enabled": true, "short_name": "greetings", "description": "Everyday greetings", "version": "1.0.0", "chrome_url_overrides": { "newtab": "index.html" }, "manifest_version": 2 }
- offline_enabled: duh!
- chrome_url_overrides: we want this extension to override our
newtab
with theindex.html
. Hence...
Now, let's create a build for our react-app:
INLINE_RUNTIME_CHUNK=false react-scripts build
Why INLINE_RUNTIME_CHUNK=false
?
Because if we don't do this, the final index.html
will contain an inline script as a result of the build process. Extension recommends using external scripts and discourages using the inline script. With INLINE_RUNTIME_CHUNK
env variable, we will bypass the above-mentioned issue.
The above command will result in a build
folder generated at the root of our project.
Now, Copy/Paste the manifest.json
file we created into this new build folder. (Why didn't we create the file inside the build folder directly? Because the build folder is purged and re-created every time you run the build commands. Hence keeping it under extras
.)
Step 5 - testing the Extension:
Now fire up Chrome Browser, go to chrome://extensions/
and enable Developer mode.
Now click on the Load unpacked
Button and point to the build folder of our project.
Test by opening a new tab in Chrome.
Step 6 - packaging for Chrome and Firefox:
Chrome packaging is simply a zip command.
Firefox packaging can be done using a recommended tool called web-ext. Install this globally using npm i -g web-ext
.
To help you eliminate the manual steps of copying/pasting manifest.json file, running commands for Firefox and Chrome packaging, etc. I have created a small node.js
script that does the job.
Create a new file at the root of the project, let's call it buildPackage.js
. Put the following content into it.
const { execSync } = require("child_process"); // FILENAMES const manifestFileName = "manifest.json"; // DIRECTORIES const buildDir = "./build/"; const extensionDir = "./extras/"; const outputs = "./"; // OUTPUTS const chromeOutput = "greetings-chrome.zip"; console.log("Building Extension Packages"); console.log("***COPYING MANIFEST FILE***\n\n"); execSync( `cp ${extensionDir}${manifestFileName} ${buildDir}${manifestFileName}` ); execSync(`zip -r ${outputs}${chromeOutput} ${buildDir}`); console.log("***CHROME BUILT SUCCESSFULLY***\n\n"); execSync(`web-ext build -s ${buildDir} -a ${outputs} --overwrite-dest`); console.log("***FIREFOX BUILT SUCCESSFULLY***");
Modify the package.json
to run this script as a part of build process. In package.json
under scripts tag, modify as shown:
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build && node buildPackage.js",
Now trigger the build script using npm run build
and you should see 2 new files created at the root of the project:
- greetings-chrome.zip // for Chrome
- greetings-1.0.0.zip // for Firefox
Step 7 - Publishing:
Chrome:
- Go to Google Chrome Developer Dashboard
- Sign in with your Google account or create a new one
- Click on + New Item Button
- Add the
greetings-chrome.zip
file created in the previous step - Fill up the required details and submit for review
(Usually takes around 12 hours to publish)
Firefox:
- Go to Mozilla Add-ons Dashboard
- Sign in with your Mozilla account or create a new one
- Click on Submit a New Add-on Button
- For distribution, select
On this site
and continue - Add the
greetings-1.0.0.zip
file created in the previous step - Fill up the required details and submit for review
(Usually takes around 6-10 mins to publish)
Final notes:
- You need an icon for your extension, create one icon of size 128x128 (name it icon_128.png), and put it in the build directory. Add the following to
manifest.json
:
"icons": { "128": "icon_128.png" },
- In some cases, your Web Extension might need to store and retrieve data. Though you can use browser localStorage, it is highly discouraged for Web Extension. Instead, you should use a designated DB storage area for extensions which is more reliable. Read more here.
- This is a very basic extension that we created, generally, it can be a bit more complicated - with API calls, Storage, Background scripts, etc. All of this is possible!
- Try and keep the Web Extension light - meaning an Extension should load fast, should serve a single purpose, should be accessible, etc.
I've created a Web Extension for Chrome and Firefox - minimylist. Do check it out and share it if you liked.
![]()
Yash Soni@iyash_soni
Is Dark Mode your poison?
minimylist - new update live. π¨βπ»π©βπ»
Download now:
Chrome: rb.gy/vdhlo2
Firefox: rb.gy/ot5bba08:26 AM - 13 Jun 2020
Top comments (1)
how to use chrome permissions at localhost:3000?
like chrome.bookmarks is undefined (just at localhost)