Mazzarolo MatteoMazzarolo Matteo

Developing a browser extension with Create React App

By Mazzarolo Matteo


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: