Building a Chrome Extension Using React and Vite: Part 1 - Setting things up
Tags: vite, react, extensions
Published: 2023-07-03 13:12:57

Hello everyone 👋

For a while, I've been toying with the idea of building an extension for the ChatGPT website. I wanted to enhance its functionality by allowing multiple chat deletions and adding other useful features. But every time I started the development process, I found myself hitting a wall, particularly around Vite configuration for extension bundling or communication between different extension contexts.

After many attempts, I finally succeeded! And I thought I'd share my experience with you all ❤️. This is the first part in a series of blog posts where I will guide you through my process.

In this initial part, we'll cover the setup process: initializing the project, creating the popup, content script and background worker, and incorporating them into our `manifest.json` file. 

The second part which shows how to connect different parts of the extension together is available [here](https://dev.to/bnn1/how-do-they-talk-to-each-other-2p9)

## Setup

Let's start by setting up a Vite project. Use your favorite package manager to establish a fresh Vite project. Here, I'll be using yarn:

```sh
yarn create vite
```

During the installation, I select `React -> Typescript with SWC`. 

Once done, I add a few Vite plugins in the root directory of the newly created project:

```sh
yarn add -D vite-tsconfig-paths @crxjs/vite-plugin@beta
```
The `vite-tsconfig-paths` plugin supports path aliases and `@crxjs/vite-plugin@beta` is a library for bundling the extension. 

Next, we need to register these plugins in the `vite.config.ts` file:

```ts
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

import react from "@vitejs/plugin-react-swc";
import { ManifestV3Export, crx } from "@crxjs/vite-plugin";

import manifestJson from "./manifest.json";

const manifest = manifestJson as ManifestV3Export;

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [tsconfigPaths(), react(), crx({ manifest })],
});
```
As you can see, I've imported the manifest file and asserted it to the `ManifestV3Export` type. The `crx` plugin's typings require this to function correctly 😅. A quick note - I needed to add the `manifest.json` file to my `tsconfig.node.json`'s `include` field to avoid a resolution error.

Now let's discuss the `manifest.json` file. This file provides an overview of my extension: its name, description, permissions, as well as the locations of entry files for the popup, content script, and background worker.

```json
{
  "manifest_version": 3,
  "name": "ChatGPT Extension",
  "version": "0.0.1",
  "description": "Improve your ChatGPT experience with this extension! Bulk exports, bulk deletes, save prompts and more!",
  "permissions": [
    "activeTab",
    "cookies",
    "declarativeContent",
    "scripting",
    "storage",
    "tabs"
  ],
  "host_permissions": ["https://chat.openai.com/*"],
  "action": {
    "default_popup": "index.html"
  },
  "background": {
    "service_worker": "src/background/background.ts",
    "type": "module",
    "persistent": false
  },
  "content_scripts": [
    {
      "matches": ["https://chat.openai.com/*"],
      "js": ["src/contentScript/main.tsx"],
      "run_at": "document_end"
    }
  ]
}
```
Here, permissions like

 `activeTab`, `cookies`, and `tabs` grant the extension access to various aspects of the browser's functionalities. For example, `activeTab` allows the extension to interact with the current tab while `cookies` and `tabs` allow access to cookie data and tab details respectively. 

Next, we organize our application structure. The setup process has provided us with several files. Here's how we arrange them:

1) I create a new `popup` directory inside my `src` folder, move there `main.tsx` and `App.tsx` files, and update the `index.html` file to point to the new entry file location: 

```html
...
    <script type="module" src="/src/popup/main.tsx"></script>
...
```
The popup runs in its own context, independently from the background workers or content scripts. It doesn't have access to the DOM, which is why we'll be using a content script for that purpose.

2) I duplicate the `popup` directory, rename it to `contentScript`, and within the `main.tsx` script, create a new `div` element to append it to the document's `body`. Then, I render my application into the created `div`:

```tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "../index.css";

const root = document.createElement("div");
root.id = "chatgpt-content-script-root";

document.body.appendChild(root);

ReactDOM.createRoot(root).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```
This approach ensures: a) there is a root element to render our application into; b) we will not have conflicts with the website's existing DOM elements, ensuring our extension doesn't interfere with the website's functionality or vice versa.

3) Lastly, we create a background script inside the `background` folder. For now, let's insert a dummy `console.log` just to ensure everything is working correctly.

Now comes the exciting part: testing our setup:

1) `yarn build` - this command builds my extension into a `dist` directory.
2) Enable developer mode in Google Chrome extensions.
3) Load the extension by clicking the `Load unpacked` button and selecting the `dist` directory.
4) Open `https://chat.openai.com/` - at this point, you should notice the website's user interface has been modified by a Vite counter. When you click the extension button, guess what pops up? 😏 The last thing we need to test is the background script. Right-click anywhere inside the extension popup and select `Inspect popup`. Here, you should see your dummy console.log.

Yay! Congratulations to us! 🎉 We've just created our first Google Chrome extension. It currently doesn't do much more than adjusting the website's layout, but it's just the beginning.

In case you face any issues during setup, ensure to double-check your file structure and the content of your `manifest.json` file. Most errors at this stage come from misconfigurations in these areas. 

Stay tuned for the next part where I'll set up communication between different contexts of our extension. 

In the meantime, if you've tried these steps and have feedback, or if you'd like me to explain anything in more detail, please leave a comment below. I'd love to hear from you!