Enhancing Chrome Extension developer experience with CRA (create-react-app)
Tags: javascript, react, browser, extension
Published: 2021-06-24 16:00:42

## Introduction

Hello again, I hope all of you are doing okay and getting vaccinated so we can get rid of this effing pandemic. 

Recently I've been coding a Chrome extension to scratch my itch with the way Chrome switches to the next tab when you close a tab and here I'll be documenting some useful things I learned along the way.

I won't delve into the basics of how a Chrome extension works, so you if you're new to it you can read these posts that explain it in a better way:

* [Build Your First Chrome Extension](https://dev.to/ganeshjaiwal/build-your-first-chrome-extension-458g) by @ganeshjaiwal
* [Creating a simple Chrome extension](https://dev.to/paulasantamaria/creating-a-simple-chrome-extension-36m) by @paulasantamaria

## Table of Contents
* [Creating aliases for node](#section-1)
* [Creating browser extension project with CRA] (#section-2)
* [Add sourcemaps during development] (#section-3)
* [Add eslintrc to change linting rules] (#section-4)
* [Configure project for stagin/release](#section-5)*


## Creating aliases for node <a name="section-1"></a>

If you're like me, you don't like typing the same commands again and again and again. Since we're going to use npm to install the packages, I have some aliases for the most used commands. 

You can install these aliases by just running the command in your terminal, but they'll be lost once that session ends. To make them permanent, add them to your ~/.bashrc or ~/.zshrc profile. 

To install a package globally:
```
alias npi='npm i -g'
```

To install and save a package as a dev dependency: 
```
alias npd='npm i -D'
```

To uninstall a package: 
```
alias npu='npm un'
```

To run a custom script in your package.json: 
```
alias npr='npm run'
```

To reload the profile from the terminal I use this command (for zsh):
```
alias ssz='source ~/.zshrc'
```

## Creating browser extension project with CRA <a name="section-2"></a>

We're going to create the project using the [create-react-extension](https://github.com/VasilyShelkov/create-react-extension) script:
```
npx create-react-app --scripts-version react-browser-extension-scripts --template browser-extension <project name>
```

This will configure the tools and file structure needed for the extension, namely the .html files (options, popup) as well their javascript files and the manifest.json.

You can run the extension with `npm start` then, once it builds you can go to your browser and open the `chrome://extensions` page. Once there you can click the "Developer mode" switch, click the "Load unpacked" button and select the `dev` folder generated by CRA. 

## Configuring the project to enhance the experience
Now that the extension is installed and you can test it, it's time to configure the project to suit our needs.

We are going to:
* Install react-app-rewired
* Configure VSCode and Webpack for alias support
* Configure react-devtools
* Add sourcemaps during development
* Add eslintrc to change linting rules
* Configure project for release

### Installing and configuring react-app-rewired
Since CRA abstracts all the configuration, webpack and whatnot from you, if you want to modify or tweak a setting you need to `eject` the project and this is an irreversible operation. And once you do, you need to maintain the configuration and keep it updated by yourself, so this isn't recommended.

Enter [react-app-rewired](https://github.com/timarney/react-app-rewired). What this package does is it allows you to hook into the Webpack config process so you can change settings, add loaders or plugins, and so on. It's like having all the pros of ejecting (mainly, access to the webpack.config.js) without actually ejecting.

Install the package by running `npd react-app-rewired` if you're using my alias from the previous section, otherwise:
```
npm install react-app-rewired --save-dev
```
Now you need to add a `config-overrides.js` at the root of your project (i.e: at the same level as the node_modules and src folders) where we will put our custom configuration.

Finally, change the `scripts` section of your package.json to use react-app-rewired instead of the react-scripts package:
```json
/* in package.json */
"scripts": {
  "start": "react-app-rewired start",  
  "build": "react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-scripts eject"
}
```

### Configure VSCode and Webpack for alias support

Now that react-app-rewired is configured, let's start hacking away. 

#### Configuring VSCode for alias support
If you have a deep components structure, sometimes you may get tired of writing `./MyComponent` or `../../MyParentComponent`. VSCode has support for using aliases, so you can import your package with an alias, get intellisense and go to definition:
```javascript
import MyComponent from "@Components/MyComponent"
```
In order to do so, add a `jsconfig.json` in the `src` folder of your project, which will tell the TypeScript Language Server from VSCode to do some nice things for us:
```json
{
	"compilerOptions": {
		"baseUrl": ".",
		"module": "commonJS",
		"target": "es6",
		"sourceMap": true,
		"paths": {
			"@Config/*": ["config/*"],
			"@Components/*": ["components/*"],
			"@Containers/*": ["containers/*"],
			"@Handlers/*": ["handlers/*"],
			"@Utils/*": ["utils/*"],
			"@Style": ["style/style.js"]
		}
	},
	"typeAcquisition": {
		"include": ["chrome"]
	},
	"include": ["./**/*"],
	"exclude": ["node_modules"]
}
```

You can read about the `compilerOptions` [here](https://www.typescriptlang.org/tsconfig), but have a brief description of the most important ones:
* `baseUrl` indicates the base path used for the `paths` property, the src folder in this case
* `paths` is an array of in which you will configure how aliases are resolved when importing
* `typeAcquisition` is required if you want intellisense for some packages, such as chrome apis in this case
* `include` and `exclude` tells TypeScript which files are to be used for resolving and compiling

In order for the changes to take effect, you need to restart VSCode.

#### Configuring Webpack for alias support
Once the jsconfig.json is configured, you can import your packages using the alias import and get intellisense from VSCode as well as clicking F12 to go the file definition. But since webpack doesn't know about those alias, the project will not compile. 

Let's modify our `config-overrides.js` to tell webpack about those aliases. 

```javascript
const path = require("path");

module.exports = function override(config) {
	config.resolve = {
		...config.resolve,
		alias: {
			...config.alias,
			"@Config": path.resolve(__dirname, "src/config"),
			"@Components": path.resolve(__dirname, "src/components"),
			"@Containers": path.resolve(__dirname, "src/containers"),			
			"@Utils": path.resolve(__dirname, "src/utils"),
			"@Style$": path.resolve(__dirname, "src/style/style.js"),
		},
	};

	return config;
};
```

What we are doing is getting a config object from the `webpack.config.js` used by react when compiling and running the app, and appending our custom aliases to the aliases collection in case any exists. Now you can save the file and run `npm start` in the console and you can start using your aliases. 

**Note**:
Most aliases allow you to import by writing 
```javascript
import MyFileInsideTheFolder from "@MyAliasName/MyFileInsideTheFolder"
```

but if you want to import a specific file you can append '$' at the end and include the full path of the file as is seen with the styles.js file. 
And then you can import file like this:
```javascript
import Styles from "@Styles"
```

### Configure react-devtools
Due to Chrome security policies, other extensions cannot access the code or markup of an extension. So if you want to use the React dev-tools with your extension, you need to install the stand-alone version of the tool:
```
npx react-devtools
```
This will install and run the dev-tools in a new Chrome frame, which is a web socket that will be listening in the port 8097.
But to actually use it, we need to do two things: add the script to the relevant html page and tell chrome to connect to it. 

Copy the script and paste in the head of the html you want to use, in my case it's public/options.html:
```html
<script src="http://localhost:8097"></script>
```
Now go into the `public/manifest.json` and paste this line at the end:
```json
"content_security_policy": "script-src 'self' 'unsafe-eval' http://localhost:8097; object-src 'self'; connect-src ws://localhost:4000 ws://localhost:8097"
```

This line tells Chrome a few things related to our environment:
* `script-src` refers to the origin of the scripts to be used by the extension
  * `self` tells to load scripts from the same origin
  * `unsafe-eval` tells to allow code to be run by eval (this is used by webpack to generate the sourcemaps)
  * `http://localhost:8097` allow scripts coming from the React dev-tools

* `connect-src` tells Chrome to allow some protocols (like websockets in this case) to connect to our app
  * `http://localhost:8097` again, allow the React dev-tools to connect to our extension
  * `ws://localhost:4000` this is used by webpack for the hot reload

You can read more about the Content Security Policy [here](https://developers.google.com/web/fundamentals/security/csp).


## Add sourcemaps during development <a name="section-3"></a>
By default, webpack only emits the bundled files to the dev folder, in order to debug your code directly from chrome we can tel webpack to generate the source map from our code.

To do this, go to the `config-overrides.js` and add this line before returning the config:
```javascript
config.devtool = "eval-source-map";
```
This will make our build slower, but will allow you to see you full source code in the Chrome dev tools. 
More information about the different options for the source map generation [here](https://survivejs.com/webpack/building/source-maps/).

## Add eslintrc to change linting rules <a name="section-4"></a>
Sometimes ESLint complains about things it could ignore, like discards not being used or a parameter not being used, among other things. If you're a bit obsessive and don't like those complaints, you can add a `.eslintrc.js` (it may be a json, js or yaml) at the root of your project to configure the rules and behavior of ESLint. 

if you haven't done so, install with: 
```
npm install --save-dev eslint
```
Then run with npx to fire the assistant:
```
npx eslint --init
```
Once you're done configuring the options, ESLint will generate the .eslintrc for you (or you can manually add it if you already had ESLint installed). 

To change a rule, simply add the rule to the `rules` array with the desired options. In my case, I modified the `no-unused-vars` to ignore discards (_):
```javascript
rules: {
		"no-unused-vars": [
			"warn",
			{
				vars: "all",
				args: "after-used",
				ignoreRestSiblings: false,
				varsIgnorePattern: "_",
				argsIgnorePattern: "_",
			},
		],
```

You can see a list of all the rules [here](https://eslint.org/docs/rules/). 

## Configure project for stagin/release <a name="section-5"></a>
Finally, once you're ready to build and publish your app, we need to tell webpack to make some changes. I use a lot of `console.log()` during development to keep track of things like windows or tabs id, but I want them removed from the production script. 

To do this, we're going to:
* Add the `customize-cra` package to allow the injection of plugins and loaders
* Add the `transform-remove-console` babel plugin to remove all console.* calls from our code
* Disable the sourcemap generation

Install the packages with 
```
npm install --save-dev customize-cra babel-plugin-transform-remove-console
```

Now, for [customize-cra](https://github.com/arackaf/customize-cra) to work we need to modify the `config-overrides.js` file once again. The `override` method from customize-cra receives a list of functions, so we need to change the signature like this:
```javascript
const path = require("path");
const { override, addBabelPlugin } = require("customize-cra");

module.exports = override(
);
```
Inside, we will tell it to load the `transform-remove-console` plugin:
```javascript
const path = require("path");
const { override, addBabelPlugin } = require("customize-cra");

module.exports = override(
  addBabelPlugin("transform-remove-console")
);
```
Now, we are going to move the code we had before to a new function and add a call to it as part of the override list:
```javascript
const path = require("path");
const { override, addBabelPlugin } = require("customize-cra");

module.exports = override(
  addBabelPlugin("transform-remove-console"), 
  (config, env) => customOverride(config, env)
);

function customOverride(config, env) {
	config.devtool = "eval-source-map";
	config.resolve = {
		...config.resolve,
		alias: {
			...config.alias,
			"@Config": path.resolve(__dirname, "src/config"),
			"@Components": path.resolve(__dirname, "src/components"),
			"@Containers": path.resolve(__dirname, "src/containers"),
			"@Handlers": path.resolve(__dirname, "src/handlers"),
			"@Utils": path.resolve(__dirname, "src/utils"),
			"@Style$": path.resolve(__dirname, "src/style/style.js"),
		},
	};	

	return config;
}

```

Finally, we need to tell webpack to remove the sourcemaps when we are building for an environment that isn't `development`, so our final `config-overrides.js` will look like this:
```javascript
const path = require("path");
const { override, addBabelPlugin } = require("customize-cra");

module.exports = override(
  addBabelPlugin("transform-remove-console"),
  (config, env) => customOverride(config, env)
);

function customOverride(config, env) {
	config.devtool = "eval-source-map";
	config.resolve = {
		...config.resolve,
		alias: {
			...config.alias,
			"@Config": path.resolve(__dirname, "src/config"),
			"@Components": path.resolve(__dirname, "src/components"),
			"@Containers": path.resolve(__dirname, "src/containers"),
			"@Handlers": path.resolve(__dirname, "src/handlers"),
			"@Utils": path.resolve(__dirname, "src/utils"),
			"@Style$": path.resolve(__dirname, "src/style/style.js"),
		},
	};

	if (env !== "development") {
		config.devtool = false;
	}

	return config;
}

```

## Conclusion
I spent many nights fighting with the packages until I finally got it working the way I wanted, so I hope this article is useful to you. Stay safe.