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 by @ganeshjaiwal
- Creating a simple Chrome extension by @paulasantamaria
Table of Contents
- Creating aliases for node
- Creating browser extension project with CRA
- Add sourcemaps during development
- Add eslintrc to change linting rules
- Configure project for stagin/release*
Creating aliases for node
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
We're going to create the project using the 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. 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:
/* 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:
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:
{
"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, but have a brief description of the most important ones:
-
baseUrl
indicates the base path used for thepaths
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
andexclude
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.
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
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:
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:
<script src="http://localhost:8097"></script>
Now go into the public/manifest.json
and paste this line at the end:
"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.
Add sourcemaps during development
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:
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.
Add eslintrc to change linting rules
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 (_):
rules: {
"no-unused-vars": [
"warn",
{
vars: "all",
args: "after-used",
ignoreRestSiblings: false,
varsIgnorePattern: "_",
argsIgnorePattern: "_",
},
],
You can see a list of all the rules here.
Configure project for stagin/release
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 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:
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:
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:
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:
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.
Top comments (4)
Great write-up. And thanks for shedding some light on
react-browser-extension-scripts
. This looks very interesting. Developer experience for browser extension development wasn't as sexy as this a few years ago. I'll definitely check it out.Great article. This helps a lot, thank you!
Are you using manifest V3 here?
How can we add the support for the Typescript?