Bootstrapping complex Chrome/Firefox/Edge extensions with Create React App

Savr Goryaev
The Startup
Published in
13 min readApr 23, 2020

This post is for developers who already have some experience with creating browser extensions and would like to get advantages of using React.js along with some modern bundling tool, but wouldn’t like to deal with build configuration. This is where Create React App comes in.

Note: This post was originally designed to address the creation of Chrome extensions, however it is also relevant to almost all modern desktop browsers (except Safari) as their extensions’ APIs are compatible to a large extent with Chrome API, at least within the coverage of the post. So if you’re developing extensions for Firefox/Edge/Opera/any Chromium-based browser, this is for you too.

Note: If you’re a beginner in browser extension development, I encourage you to start with official tutorials: 1 (for Chrome), 2 (for Firefox/Edge).

Create React App is a popular environment for building React-driven web applications. Unfortunately, it can’t be used with complex extensions. The problem is that Create React App is designed solely for building single page applications, with no option to change it, whereas a complex extension usually comprises of multiple page-like components (such as background or content script), each of which can be considered as a separate web page. Sure, it’s not a problem if an extension is supposed to be consisted of content script alone, for example (apart from manifest and static resources), but this post doesn’t address such simple case. Furthermore, one can use Create React App to build a popup page only and build background or/and content scripts separately or even make them static JS files. But, obviously, such extension is hard to maintain. It would be much better to build all extension components within the same build process.

Specification

Our objective in this post is to get a boilerplate of complex extension. It has to contain all page-like components that may be present in an extension, namely background page, content script, popup page and options page. As already mentioned, each of them can be considered as a separate web page. Since we only specify a boilerplate, it doesn’t imply any functionality in each listed component, just a boilerplate, again. So let background and content scripts be empty (apart from some console output) and popup and options pages have the default Create React App appearance.

Create React App can run in 3 modes: development, unit tests runner and production. Unfortunately the development mode is unavailable to browser extensions as, unlike regular web pages, they can’t be hosted on server. Regarding unit testing, while this is generally possible, writing unit tests for browser extensions is pretty hard problem which is out of scope of this post. So the only option left is the production mode. In other words our extension boilerplate is only supposed to run in production mode.

This is our general specification to be implemented. Let’s get started.

Toolkit

Since Create React App doesn’t allow to build multiple pages at once “out the box”, we have to customize its build configuration. Create React App uses Webpack under the hood and the official docs suggest to run “eject” in order to use custom Webpack configuration, though that is one-way action unwanted to most of (inexperienced) Create React App users. Luckily there is another way.

react-app-rewired library allows to override Webpack configuration in Create React App project without ejecting it (this is not the only option, there are many similar libraries).

With this library one overrides Create React App configuration by creating config-overrides.js file in the project’s root directory, with specific content like the following:

config-overrides.js sample

Inside override function one can manipulate the configuration via the 1st argument, e.g.: add/replace/remove plugins, loaders etc.

So we know a way to change the configuration, but we still don’t know what to change exactly. In order to answer this question we have to see what’s inside the Create React App’s Webpack configuration.

Now you might feel misled by my implicit promise that you won’t have to deal with build configuration). Well.. You can scroll to the bottom and get the resulted boilerplate right now. However, I suggest you to try reading the rest of the post. A basic Webpack knowledge is enough, furthermore, I’ll comment in detail all key moments.

Inspecting/tweaking the configuration

So let’s look at the sources of Create React App. I suggest to use v3.2.0 as a sample (I’ll explain why exactly this version later). This is its built-in Webpack configuration. The file itself is too large, so I only posted a link to it here. Anyway, we don’t need all the code, actually we’re only interested in entry points, output, HtmlWebpackPlugin settings as well as a few specific settings to be changed/disabled.

Entry point

Create React App specifies single entry point in its configuration file. Below is how it looks in v3.2.0:

CRA webpack config structure

Note: The object returned from the function above is a Webpack config that is exported/passed to Webpack.

Since we only use the production mode, isEnvDevelopment is always false, so the above snippet is equivalent to:

We have to replace the original single entry point with multiple ones that will be associated with extension components we specified above: popup, options, background and content, wherein popup will replace the original entry point. The value (path) of popup entry point should be the same as of the original single entry point (i.e. src/index.js); it can’t be changed as it’s used internally by Create React App. Regarding the rest of entry points, we’re free to assign any values (paths), but, for the sake of simplicity, let them be based on their names. So, according to Webpack entry syntax, the above array expression has to be replaced with following object notation:

Note that paths.appSrc refers to /src directory.

This is the result we want to be in the Create React App’s configuration. As mentioned above, one modifies the configuration via config argument of override function. So we have to replace entry property of config object with the above object. Below is how it should look in config-overrides.js file:

Multiple entry points setup in config-overrides.js

Note that we have to specify full path to config’s paths upon import, as our root directory is obviously different from Create React App’s one.

Output

Regarding output, Create React App configures it with a lot of options. Below is how it looks in v3.2.0:

CRA webpack config: original output specification

We’re only interested in filename option that specifies template for destination of each compiled file. As we only use the production mode, the resulted value is static/js/[name].[contenthash:8].js. In this value contenthash is used for cache control purposes, but it is useless in our case as caching is inapplicable to browser extensions. Moreover, this is even unwanted as compiled JS files (at least background script) have to be statically linked to manifest. So we have to get rid of contenthash in filename template and the resulted value has to be: static/js/[name].js. This can be done by simply assigning filename property of output the new value. Below is how it should look in our override function:

Disabling optimization options

Furthermore, we have to disable specific optimization options incompatible with our environment: optimization.splitChunks and optimization.runtimeChunk. optimization.runtimeChunk option can be disabled by simply assigning it false, whereas optimization.splitChunks has to be set to special object literal:

{ cacheGroups: {default: false} }

Below is a snippet that has to be added into our override function:

Setting up popup page

HtmlWebpackPlugin is a Webpack plugin for creating HTML pages. In Create React App, by default, it generates index HTML page from the page template public/index.html. This is how HtmlWebpackPlugin is set up in v3.2.0:

CRA webpack config: original HtmlWebpackPlugin setup

Note: The above is not exact snippet from the sources, but an equivalent code in the case of production mode.

template option of HtmlWebpackPlugin constructor specifies path to the page template and it is set to public/index.html. We can’t change this value as it’s used internally by Create React App (like entry point path), but we can change the name of the resulted page. The appropriate HtmlWebpackPlugin option — filename, defaulted to index.html. Let’s set it to popup.html as the resulted page is supposed to be a popup in our extension. Then, we have to associate this page with specific entry point as we have multiple ones. The appropriate HtmlWebpackPlugin option — chunks, which expects an array value. We have to set it to [‘popup’]. inject and minify options above have to be left as is. So below is a HtmlWebpackPlugin instance that has to be in the plugins property:

We cannot just set the plugins property to a new array containing the instance above (like we did it before) as there might be other plugin instances in the original plugins array. Instead we have to replace specific element/plugin in the original array with a new one. However, we can’t rely on some fixed index (position) in the array. Sure, we already noticed in the config of v3.2.0 that HtmlWebpackPlugin instance is at index 0 in the plugins array, but this is not guaranteed and may be different in other versions. The best solution for this problem is to find the index of the needed plugin (HtmlWebpackPlugin) in the array by testing each plugin’s name for a match, then replace the plugin at the found index with a new one. Below is a function that does this job:

replacePlugin function

We need this code to be in a function as it will be reused later. plugins argument expects an array of plugins, nameMatcher — a testing function to match the plugin to be replaced, newPlugin — a new plugin to replace. 3rd argument is optional; omitting it makes the found plugin to be removed from the plugins array. This feature will be useful later.

Now we have to call replacePlugin function passing config.plugins, an inline function with regular expression to test and the new HtmlWebpackPlugin instance (we defined above) as arguments. Below is how it should look in our override function:

Setting up options page

Besides popup, we have to generate one more HTML page — options page. We already have a pattern defined above and can use it for options page. The only difference is, we have to append the new plugin into the array, instead of replacing existing one. Also we have to accordingly change template, filename and chunks options of HtmlWebpackPlugin constructor. Furthermore, since we use the same minify options for two HtmlWebpackPlugin instances, it’d be nice to save these options in a variable to (re)use it in both constructors. So, applying the above pattern, we get the following snippet inside our override function:

Note that template option refers to options.html file, which has to be created in /public directory later.

Fixing asset manifest

Besides building application files, Create React App also generates an asset manifest using webpack-manifest-plugin. Since v3.2 Create React App changed the structure of asset-manifest.json by adding a new property entrypoints. Below is how it is set up in v3.2.0:

CRA webpack config: original webpack-manifest-plugin setup

generate function above is a bit of a problem for us, as it relies on presence of single entry point in Webpack, whereas we changed it to multiple ones. So we’ll get an error as a result. The simplest way to fix this issue is to coerce asset manifest to old plain format by omitting generate option. So the above ManifestPlugin instance has to be replaced with the one below:

So, applying the pattern defined above, we get the following snippet to be inside our override function:

Fixing output CSS file names

There are also a few Webpack plugins in Create React App config, which although not crucial to successful build in our case, are useless in our environment and better to be disabled or changed to more appropriate settings.

Like for compiled JS files, contenthash is also used in compiled CSS file names, but, unlike JS files, CSS file names are specified by special Webpack plugin — MiniCssExtractPlugin. Below is how it is set up in v3.2.0:

CRA webpack config: original MiniCssExtractPlugin setup

In order to get rid of contenthash in CSS filename template, we have to replace existing MiniCssExtractPlugin instance with the following one:

So, applying the pattern defined above, we get the following snippet inside our override function:

Disabling service worker

Create React App offers special tools for production mode to make a Progressive Web App. By default, these tools generate a service worker script using special Webpack plugin — GenerateSW. Below is how it is set up in v3.2.0:

CRA webpack config: original GenerateSW plugin setup

Although this service worker is opt-in and doing nothing by default, it is useless in our case. In order to disable this feature, we have to remove any found plugin with a name containing GenerateSW. So, applying the pattern defined above, we get the following snippet inside our override function:

Result

So, merging the above snippets, we get the following code to be placed into config-overrides.js file:

The resulted content of config-overrides.js

All modifications in the config we made above are compatible with any version of Create React App since v2.0. Sure, the Create React App config had been changed many times since v2.0, but all these changes are covered by the above overriding function (or just have no affect on our environment). At least this is so at the moment (April, 2020).

Customizing a new Create React App project

So we have a config overrider that makes Create React App suitable to our needs. Now we only have to utilize it in a Create React App project. Below v3.2.0 is used, but you’re free to use any version since v2.0.0. So let’s create a new project with Create React App v3.2.0 using special undocumented CLI syntax (for specific version selection):

npx create-react-app my-app — scripts-version=react-scripts@3.2.0

Project structure

After creation, the folder structure of the project looks like this:

my-app/
README.md
package.json
public/
favicon.ico
index.html
logo192.png
logo512.png
manifest.json
robots.txt
src/
App.css
App.js
App.test.js
index.css
index.js
logo.svg
serviceWorker.js

Let’s make this structure ready to be used with our config overrider.

favicon.ico, robots.txt and serviceWorker.js are definitely unnecessary in our case. favicon.ico and robots.txt are just inappropriate for browser extensions. Service worker feature has already been disabled in our config overrider. So we may remove these files.

Regarding images in public folder, one logo image is enough for our needs as we’ll only use it as a boilerplate for the extension and toolbar icons (and won’t use it on a web page). Furthermore, the larger image (logo512.png) is too large to be an icon. So we may remove it and only leave the smaller one (logo192.png).

According to the specification, our boilerplate will have two HTML pages: popup and options page, both with the same appearance as of default Create React App’s web page. Let’s create two folders to contain React component files of our HTML pages: src/views/Popup and src/views/Options. As popup page replaces the index web page, we can move existing src/App.* files to src/views/Popup folder, then copy these files to src/views/Options folder. Also, for the sake of simplicity, let’s remove index.css file and copy/paste its content into src/views/Popup/App.css and src/views/Options/App.css files.

Then, we have to create entry points for our extension components. We already have existing one for popup/index page: src/index.js. We can copy this file to src/options.js and use it for options page. In both these files we have to change path to App React component: from ./App to ./views/Popup/App and ./views/Options/App respectively. Let’s also create entry point files for background and content scripts: src/background.js and src/content.js. According to the specification, they both only have to contain console output. So let’s insert a console.log call into each file.

Furthermore, we have to create HTML template for options page. Like with options entry point, we can copy existing HTML template public/index.html to public/options.html and use it for options page.

Summing up the above changes, the folder structure of our project should look like this:

my-app/
README.md
package.json
public/
index.html
options.html
manifest.json
src/
background.js
content.js
index.js
options.js
views/
Popup/
App.css
App.js
App.test.js
Options/
App.css
App.js
App.test.js

Manifest

Every browser extension must have a manifest.json file providing important information for its startup. A new Create React App project already includes manifest.json file in `public` folder, but it is used for other purpose. As manifest.json in our extension is supposed to be a static file, we can use existing public/manifest.json file for our purpose. We only need to remove its content and replace with a new, appropriate one. I assume that you’re already familiar with manifest.json format. So I just post below the content to be placed in manifest.json:

The resulted manifest.json

Above is a manifest.json boilerplate that specifies the structure of our extension boilerplate. You’re free to change it according to your needs.

Toolkit installation

Our project is almost ready to utilize the config overrider. But first we have to install react-app-rewired library in the project:

npm install react-app-rewired — save-dev

Applying config overrider

Now we only have to place our config-overrides.js file into in the root folder of the project as well as make a slight change in `package.json` file replacing react-scripts call for the build mode with appropriate react-app-rewired call:

{
...
"scripts": {
...
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
...
}
...
}

Note that react-scripts calls for development and test modes are left unchanged.

Now we can build our extension by appropriate command:

npm run build

Fruition

Ta-da! We have an extension ready to use. Sure, it’s just a boilerplate doing nothing useful, however it can already be run without errors in (almost) any browser. Below is how it looks in Chrome:

Popup page
Options page

Here is the source code of the boilerplate we just made. You can use it for your needs. However I wouldn’t recommend it because there is a better option.

By the way, now it’s time to reveal why we chose v3.2.0 exactly for config code inspection and project creation. The point is that with v3.3.0 Create React App introduced a new attractive feature — custom templates, which allow to apply custom features to a new project upon creation, while still retaining all of the features of Create React App. I’ve already made custom template that doing the same as the boilerplate we made above. Apart from educational purposes, the above boilerplate should only be used with fixed Create React App version within range from v2.0 to v3.2 when update is unavailable or unwanted (in this case you can use the special CLI syntax for specific version selection or just directly edit dependencies field in package.json). Otherwise I encourage you to use the custom template cra-template-complex-browserext with latest Create React App version, by the following command:

npx create-react-app my-app --template complex-browserext

Usage mini-FAQ

Q: I want to use an old version of Create React App under v2.0.

A: My commiserations(. There were too many crucial changes made since v2.x, which makes the config overrider, used in both the boilerplate and the custom template, incompatible with versions under v2.x.

Q: I don’t want an options page (background/content script) to be in my extension.

A: You may exclude unneeded extension component (except popup page) from compilation by removing respective entry point. See config-overrides.js file for details.

Q: I want to add an extra HTML page to compilation.

A: You can use options page setup code use as an example. See config-overrides.js file for details.

Q: I want my extension to support multiple languages.

A: Not a problem. Internationalization feature may be easily added to a project using the Public folder. Just create _locales subfolder inside /public and all its contents (incl. nested files/folders) will be copied to the build folder.

Q: I want TypeScript to be used with my Create React App project.

A: Use special TypeScript version of Create React App template upon project creation:

npx create-react-app my-app --template complex-browserext-typescript

--

--