Developing a browser extension with Create React App and Tailwind CSS
1. Create new app
Create a new app via create-react-app
with the following command.
npx create-react-app my-browser-extension --template typescript
Or for Typescrip support run this command:
npx create-react-app my-browser-extension --template typescript
2. Setup the manifest
Create React App creates a public
dir, delete everything except the manifets.json
and index.html
. The current manifest is for Web Apps, but we need a Web Extensions manifest. So replace the public/manifest.json
content with for example the following one. An extension should have an icon, for the following manifest you would need to add a svg logo in the public
dir.
1{
2 "manifest_version": 2,
3 "name": "My-Browser-Extension",
4 "version": "0.1",
5 "description": "My browser extension",
6 "homepage_url": "https://example.org",
7 "icons": {
8 "48": "logo.svg",
9 "96": "logo.svg"
10 },
11 "permissions": [
12 ],
13 "web_accessible_resources": [
14 ]
15}
3. Setup Tailwind CSS
Install the Tailwind and its peer-dependencies using npm
. As Create React App doesn’t support PostCSS 8 yet we need to install the Tailwind CSS v2.0 PostCSS 7 compatibility build.
1npm install tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9 @craco/craco
We also installed @craco/craco
, because Create React App doesn’t let you override the PostCSS configuration natively. Therefore we need to replace react-scripts
with craco
in the package.json
like so:
1{
2 //...
3 "scripts": {
4 "start": "craco start",
5 "build": "craco build",
6 "test": "craco test",
7 "eject": "react-scripts eject",
8 },
9 //...
10}
Then, generate your tailwind config via npx tailwindcss init
. In the tailwind.config.js
file you need to change the line purge: []
to purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html']
.
Finally include Tailwind in your src/index.css
1/* src/index.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
4. Setup for production
Create React App embeds an inline script into index.html
in the production build. But this would break the extension usage as it goes against its CSP (Content Security Policy), so we just disable it.
Change the build script in package.json
to
1"build": "INLINE_RUNTIME_CHUNK=false craco build"
Setup for development
The goal is to create a live-reloading environment without ejecting.
First install the Webpack extension reloader plugin.
npm install -D webpack-extension-reloader
Then put the following script in scripts/watch.js
and make it executable chmod +x ./scripts/watch.js
.
1#!/usr/bin/env node
2
3// Based on: https://mmazzarolo.com/blog/2019-10-19-browser-extension-development/
4// Updated for: https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md#configuration
5
6// Force a "development" environment in watch mode
7process.env.BABEL_ENV = "development";
8process.env.NODE_ENV = "development";
9
10const fs = require("fs-extra");
11const paths = require("react-scripts/config/paths");
12const webpack = require("webpack");
13const colors = require("colors/safe");
14const ExtensionReloader = require("webpack-extension-reloader");
15
16// use Craco to create the Webpack development config
17const { createWebpackDevConfig } = require("@craco/craco");
18const cracoConfig = require("../craco.config.js");
19const config = createWebpackDevConfig(cracoConfig);
20
21// The classic webpack-dev-server can't be used to develop browser extensions,
22// so we remove the "webpackHotDevClient" from the config "entry" point.
23config.entry = !Array.isArray(config.entry) ? config.entry : config.entry.filter(function(entry) {
24 return !entry.includes("webpackHotDevClient");
25});
26
27// Edit the Webpack config by setting the output directory to "./build".
28config.output.path = paths.appBuild;
29paths.publicUrl = paths.appBuild + "/";
30
31// Add the webpack-extension-reloader plugin to the Webpack config.
32// It notifies and reloads the extension on code changes.
33config.plugins.push(new ExtensionReloader());
34
35// Start Webpack in watch mode.
36const compiler = webpack(config);
37const watcher = compiler.watch({}, function(err) {
38 if (err) {
39 console.error(err);
40 } else {
41 // Every time Webpack finishes recompiling copy all the assets of the
42 // "public" dir in the "build" dir (except for the index.html)
43 fs.copySync(paths.appPublic, paths.appBuild, {
44 dereference: true,
45 filter: file => file !== paths.appHtml
46 });
47 // Report on console the succesfull build
48 console.clear();
49 console.info(colors.green("Compiled successfully!"));
50 console.info("Built at", new Date().toLocaleTimeString());
51 console.info();
52 console.info("Note that the development build is not optimized.");
53 console.info("To create a production build, use yarn build.");
54 }
55});
Finally, add a watch
script to your package.json
.
1"watch": "./scripts/watch.js"
5. Start development
You can now replace the created example with your html that is styled with Tailwind CSS. To develop your extension with live-reloading just run npm run watch
or npm run build
for production.
To load an extension in Firefox, open about:debugging
, click This Firefox
then click Load Temporary Add-on...
and open your build/manifest.json
file.
References
This blog article is mainly based on Matteo’s article Developing a browser extension with Create React App. But as we replaced react-scripts
with cargo
, we need to make a few adjustments here.