Chrome Extension with React + CRXJS + Vite + Docker
Tags: typescript, webdev, vite, react
Published: 2023-05-09 12:42:36

## Introduction
If you are trying to develop a chrome extension in react, CRXJ is very very useful.

CRXJS provides speedy extension development experience with vite.

You can write the file name in manifest.json, and each file update is reflected instantly because the build directory directly references the file you are editing.

Let's take an example of the actual development process.

### GitHub Repository
- [mk668a/react-vite-crxjs-chrome-boilerplate](https://github.com/mk668a/react-vite-crxjs-chrome-boilerplate)

### Directory structure

```shell
.
├── Dockerfile
├── docker-compose.yml
├── index.html
├── manifest.config.ts
├── manifest.json
├── options.html
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── assets
│   │   ├── favicon.svg
│   │   └── logo.svg
│   ├── background.ts
│   ├── components
│   │   └── Button.tsx
│   ├── content_scripts
│   │   └── content_script.tsx
│   ├── options.tsx
│   ├── popup.tsx
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts

```


## Create a project with React + CRXJS + Vite

Proceed by referring to the CRXJS documentation.

I have rewritten the steps in this document, modified to fit my environment.
[Create a project | CRXJS Vite Plugin](https://crxjs.dev/vite-plugin/getting-started/react/create-project)


### Create a project
`npm init vite@latest`

```shell
$ npm init vite@latest
Need to install the following packages:
  create-vite@4.3.1
Ok to proceed? (y) y
✔ Project name: … vite-project
✔ Select a framework: › React
✔ Select a variant: › TypeScript
```


### Install CRXJS Vite plugin
`npm i @crxjs/vite-plugin@beta -D`


### Install SVGR Vite plugin (Option)

If you want to use svg with React components, install [vite-plugin-svgr](https://github.com/pd4d10/vite-plugin-svgr).

`npm i vite-plugin-svgr`

Change `vite-env.d.ts`

```typescript
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />

```



### Update the Vite config
```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx, ManifestV3Export } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
import svgr from "vite-plugin-svgr";

export default defineConfig({
	plugins: [
		svgr(),
		react(),
		crx({ manifest: manifest as unknown as ManifestV3Export }),
	],
});

```

Create `manifest.json` in the root directory.

```json
{
	"name": "Extension App",
	"description": "",
	"version": "0.0.1",
	"manifest_version": 3,
        "action": {
		"default_popup": "index.html",
		"default_title": "Open Extension App"
	}
}

```


### Merge tsconfig(Option)

For simplicity, I merged `tsconfig.node.json` into `tsconfig.json`.

```json
{
	"compilerOptions": {
		"composite": true,
		"module": "ESNext",
		"moduleResolution": "Node",
		"allowSyntheticDefaultImports": true,
		"target": "ESNext",
		"useDefineForClassFields": true,
		"lib": ["DOM", "DOM.Iterable", "ESNext"],
		"allowJs": false,
		"skipLibCheck": true,
		"esModuleInterop": true,
		"strict": true,
		"forceConsistentCasingInFileNames": true,
		"resolveJsonModule": true,
		"isolatedModules": true,
		"noEmit": true,
		"jsx": "react-jsx"
	},
	"include": ["src", "vite.config.ts", "*.json"]
}

```


### Start project
`npm run dev`

Open `Manage Extensions` page in your browser.
[chrome://extensions/](chrome://extensions/)

Turn on `dveloper mode` switch in the upper right corner.

Click the `Load unpacked` button in the upper left corner and select the `dist` directory in your project root directory.

### Build project

This project must be built if it is to be actually used and uploaded.

`npm run build`

## Create Dockerfile

Build Docker to quickly create an environment ready to start development.

`Dockerfile`
```Dockerfile
FROM node:18.15.0-alpine3.16

WORKDIR /usr/src/app

COPY package*.json ./

RUN yarn install

COPY . .
```

`docker-compose.yml`
```yml
version: '3'

services:
  extension:
    container_name: extension
    hostname: extension
    restart: always
    tty: true
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 5173:5173
    volumes:
      - .:/usr/src/app
    command: yarn dev --host
    networks:
      - default
    platform: linux/amd64

networks:
  default:
```

I am using an M1 MacBook, so I have written `platform: linux/amd64` in the `docker-compose.yml` file and turned on `Use Rosetta for x86/amd64 emulation of Apple Silicon` of Docker setting is turned on.

### Run Docker

`docker compose up -d --build`

## Fixed Popup

This is a bit confusing because the file name and function do not match, so we will modify it a bit.

Delete `App.tx` and rename `main.tx` to `popup.tx`.

Modify `popup.tsx`.

```typescript
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import logo from "./assets/logo.svg";

function Popup() {
	const [count, setCount] = useState(0);

	return (
		<div className="App" style={{ height: 300, width: 300 }}>
			<header className="App-header">
				<img
					src={chrome.runtime.getURL(logo)}
					className="App-logo"
					alt="logo"
				/>
				<p>Hello Vite + React!</p>
				<p>
					<button type="button" onClick={() => setCount((count) => count + 1)}>
						count is: {count}
					</button>
				</p>
				<p>
					Edit <code>App.tsx</code> and save to test HMR updates.
				</p>
				<p>
					<a
						className="App-link"
						href="https://reactjs.org"
						target="_blank"
						rel="noopener noreferrer"
					>
						Learn React
					</a>
					{" | "}
					<a
						className="App-link"
						href="https://vitejs.dev/guide/features.html"
						target="_blank"
						rel="noopener noreferrer"
					>
						Vite Docs
					</a>
				</p>
			</header>
		</div>
	);
}

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
	<React.StrictMode>
		<Popup />
	</React.StrictMode>
);
```


Fix `src` attribute of script tag in `index.html`.

```html
<script type="module" src="/src/popup.tsx"></script>
```


## Create Content Scripts

Add `content_scripts` section in `manifest.json`.
```json
{
	"name": "Extension App",
	"description": "",
	"version": "0.0.1",
	"manifest_version": 3,
	"action": {
		"default_popup": "index.html",
		"default_title": "Open Extension App"
	},
	"content_scripts": [
		{
			"matches": ["<all_urls>"],
			"js": ["src/content_scripts/content_script.tsx"]
		}
	]
}
```

Make `content_scripts` directory in `src` directory.

Create sample content_script component in `src/content_scripts/content_script.tsx`.

```typescript
import React from "react";
import ReactDOM from "react-dom/client";
import Button from "../components/Button";

function ContentScript() {
	return (
		<div className="App">
			<header className="App-header">
				<h1>ContentScript</h1>
				<Button>button</Button>
			</header>
		</div>
	);
}

const index = document.createElement("div");
index.id = "content-script";
document.body.appendChild(index);

ReactDOM.createRoot(index).render(
	<React.StrictMode>
		<ContentScript />
	</React.StrictMode>
);

```


At the same time, create a `component` directory and `Button.tsx` file.

```typescript
import React from "react";

const Button = (props: any) => <button {...props} />;

export default Button;
```

## Create Background
Add a definition of `background` to `manifest.json`.

Note that if you want to use certain features of the chrome API, it is necessary to add some permissions to `permissions`.

[Chrome Extensions Declare permissions](https://developer.chrome.com/docs/extensions/mv3/declare_permissions/)

```json
{
	"name": "Extension App",
	"description": "",
	"version": "0.0.1",
	"manifest_version": 3,
	"action": {
		"default_popup": "index.html",
		"default_title": "Open Extension App"
	},
	"content_scripts": [
		{
			"matches": ["<all_urls>"],
			"js": ["src/content_scripts/content_script.tsx"]
		}
	],
	"background": {
		"service_worker": "src/background.ts",
		"type": "module"
	},
	"permissions": [
		"background",
		"contextMenus",
		"bookmarks",
		"tabs",
		"storage",
		"history"
	]
}
```

Make `background.ts` file in `src` directory.

This is the sapmle code to add event listener of changing tab and to get the bookmarks.

```typescript
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
	console.log(`Change URL: ${tab.url}`);
});

chrome.bookmarks.getRecent(10, (results) => {
	console.log(`bookmarks:`, results);
});

console.log(`this is background service worker`);

export {};

```

## Create Options

This is an options page which can be accessed by right-clicking the extension icon on the toolbar and selecting Options.

Add `options_page` section to `manifest.json`.

```json
{
	"name": "Extension App",
	"description": "",
	"version": "0.0.1",
	"manifest_version": 3,
	"action": {
		"default_popup": "index.html",
		"default_title": "Open Extension App"
	},
	"content_scripts": [
		{
			"matches": ["<all_urls>"],
			"js": ["src/content_scripts/content_script.tsx"]
		}
	],
	"background": {
		"service_worker": "src/background.ts",
		"type": "module"
	},
	"options_page": "options.html",
	"permissions": [
		"background",
		"contextMenus",
		"bookmarks",
		"tabs",
		"storage",
		"history"
	]
}

```

Create `options.html`.

It is almost the same as `index.html`, but the `src` attribute of the `script` tag must be changed.

```html
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<link rel="icon" type="image/svg+xml" href="/vite.svg" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Extension App</title>
	</head>
	<body>
		<script type="module" src="/src/options.tsx"></script>
	</body>
</html>
```

Make `options.tsx` file in `src` directory.

```typescript
import React from "react";
import ReactDOM from "react-dom/client";
import Button from "./components/Button";

function Options() {
	console.log(`this is options page`);

	return (
		<div className="App">
			<header className="App-header">
				<h1>Title</h1>
				<Button>button</Button>
			</header>
		</div>
	);
}

const index = document.createElement("div");
index.id = "options";
document.body.appendChild(index);

ReactDOM.createRoot(index).render(
	<React.StrictMode>
		<Options />
	</React.StrictMode>
);
```

---


## Conclusion

CRXJS improves the experience of developing extensions with React.
Also, you can easily set it up using Docker.

I am now trying to create [a new panel](https://developer.chrome.com/docs/extensions/mv3/devtools/) in the Developer tool. If it works, I will update this post.

Thank you for reading.



## Reference

- [Introduction | CRXJS Vite Plugin](https://crxjs.dev/vite-plugin/)
- [Create a project | CRXJS Vite Plugin](https://crxjs.dev/vite-plugin/getting-started/react/create-project)
- [vite-plugin-svgr](https://github.com/pd4d10/vite-plugin-svgr)
- [Chrome Extensions Declare permissions](https://developer.chrome.com/docs/extensions/mv3/declare_permissions/)


## GitHub Repository
- [mk668a/react-vite-crxjs-chrome-boilerplate](https://github.com/mk668a/react-vite-crxjs-chrome-boilerplate)