Create React App is a great tool for developing React applications for the web.
Did you know that with a couple of tweaks, it can also become one of the best ways to create browser extensions?
Here's how:
1. Create a new app with Create React App
Let's start by creating a new React app:
npx create-react-app my-extension
npx create-react-app my-extension
2. Setup the manifest
By default Create React App creates a Web App manifest in the /public
dir.
We don't need it: a browser extension requires a WebExtension API manifest, which follows a completely different standard.
Replace the content of public/manifest.json
with your own extension manifest.
For example:
{
"name": "My Extension",
"version": "1.0.0",
"manifest_version": 2,
"browser_action": {
"default_popup": "index.html"
}
}
{
"name": "My Extension",
"version": "1.0.0",
"manifest_version": 2,
"browser_action": {
"default_popup": "index.html"
}
}
P.S.: While we're at it, I would also clean up the public
dir, making sure we keep there only manifest.json
and index.html
.
3. Setup the production build step
Creating a production build of the browser extensions works almost out of the box with Create React App, we have to make a small change to the build
step.
By default, Create React App embeds an inline script into index.html
of the production build.
This is a small chunk of Webpack runtime logic used to load and run the application. It is embedded in your build/index.html
file by default to save an additional network request on web apps... but it also breaks the extension usage because it goes against the its CSP (Content Security Policy).
The easiest way to solve this issue is by turning off the inline script.
Setting the INLINE_RUNTIME_CHUNK
environment variable to false
is enough to tell Create React App to not embed the script.
In your package.json
, change your build step from:
"build": "react-scripts build"
"build": "react-scripts build"
to
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build"
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build"
This is enough for creating a production build of your extension 👍
4. Setup the development environment
There's one last important step we need to take care of: setting up the development environment for our extension.
There are several tutorials online about building browser extensions using Create React App, but I wasn't able to find one that explains you how to develop the extension without ejecting and without forcing you to manually refresh the extension from the browser extension page.
By mixing a few different approaches, I created a short script that you can use to get a live-reloading environment without ejecting.
First, install the Webpack extension reloader plugin, a great plugin to automatically reload browser extensions during development:
yarn add webpack-extension-reloader --dev
yarn add webpack-extension-reloader --dev
Then, put the following script in scripts/watch.js
.
I won't delve deep into details, but I think the comments should be enough to give you an high-level idea of what it does.
#!/usr/bin/env node
// A script for developing a browser extension with live-reloading
// using Create React App (no need to eject).
// Run it instead of the "start" script of your app for a nice
// development environment.
// P.S.: Install webpack-extension-reloader before running it.
// Force a "development" environment in watch mode
process.env.BABEL_ENV = "development";
process.env.NODE_ENV = "development";
const fs = require("fs-extra");
const paths = require("react-scripts/config/paths");
const webpack = require("webpack");
const configFactory = require("react-scripts/config/webpack.config");
const colors = require("colors/safe");
const ExtensionReloader = require("webpack-extension-reloader");
// Create the Webpack config usings the same settings used by the "start" script
// of create-react-app.
const config = configFactory("development");
// The classic webpack-dev-server can't be used to develop browser extensions,
// so we remove the "webpackHotDevClient" from the config "entry" point.
config.entry = config.entry.filter(function (entry) {
return !entry.includes("webpackHotDevClient");
});
// Edit the Webpack config by setting the output directory to "./build".
config.output.path = paths.appBuild;
paths.publicUrl = paths.appBuild + "/";
// Add the webpack-extension-reloader plugin to the Webpack config.
// It notifies and reloads the extension on code changes.
config.plugins.push(new ExtensionReloader());
// Start Webpack in watch mode.
const compiler = webpack(config);
const watcher = compiler.watch({}, function (err) {
if (err) {
console.error(err);
} else {
// Every time Webpack finishes recompiling copy all the assets of the
// "public" dir in the "build" dir (except for the index.html)
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: (file) => file !== paths.appHtml,
});
// Report on console the succesfull build
console.clear();
console.info(colors.green("Compiled successfully!"));
console.info("Built at", new Date().toLocaleTimeString());
console.info();
console.info("Note that the development build is not optimized.");
console.info("To create a production build, use yarn build.");
}
});
#!/usr/bin/env node
// A script for developing a browser extension with live-reloading
// using Create React App (no need to eject).
// Run it instead of the "start" script of your app for a nice
// development environment.
// P.S.: Install webpack-extension-reloader before running it.
// Force a "development" environment in watch mode
process.env.BABEL_ENV = "development";
process.env.NODE_ENV = "development";
const fs = require("fs-extra");
const paths = require("react-scripts/config/paths");
const webpack = require("webpack");
const configFactory = require("react-scripts/config/webpack.config");
const colors = require("colors/safe");
const ExtensionReloader = require("webpack-extension-reloader");
// Create the Webpack config usings the same settings used by the "start" script
// of create-react-app.
const config = configFactory("development");
// The classic webpack-dev-server can't be used to develop browser extensions,
// so we remove the "webpackHotDevClient" from the config "entry" point.
config.entry = config.entry.filter(function (entry) {
return !entry.includes("webpackHotDevClient");
});
// Edit the Webpack config by setting the output directory to "./build".
config.output.path = paths.appBuild;
paths.publicUrl = paths.appBuild + "/";
// Add the webpack-extension-reloader plugin to the Webpack config.
// It notifies and reloads the extension on code changes.
config.plugins.push(new ExtensionReloader());
// Start Webpack in watch mode.
const compiler = webpack(config);
const watcher = compiler.watch({}, function (err) {
if (err) {
console.error(err);
} else {
// Every time Webpack finishes recompiling copy all the assets of the
// "public" dir in the "build" dir (except for the index.html)
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: (file) => file !== paths.appHtml,
});
// Report on console the succesfull build
console.clear();
console.info(colors.green("Compiled successfully!"));
console.info("Built at", new Date().toLocaleTimeString());
console.info();
console.info("Note that the development build is not optimized.");
console.info("To create a production build, use yarn build.");
}
});
And finally, add a watch
script to your package.json
:
"watch": "./scripts/watch.js"
"watch": "./scripts/watch.js"
If you're not able to run the script, try running
chmod +x ./scripts/watch.js
to make it executable.
Start the development
That's it! 🎉
From now on you can run yarn watch
to develop your extension with live-reloading, or yarn build
to create a production build.
Acknowledgments
Thanks to:
- Hitesh Kumar for the
watch
script idea - Rubens Pinheiro for the Webpack extension reloader plugin
- Nina Shahri for this article on CSP
- JP Pincheira for the
chmod -x
tip