mk668a | Chrome Extension with React + CRXJS + Vite + Docker
header_image

5 min read

update: 2023/06/11 22:31:29

create: 2023/05/17 11:11:01

Chrome Extension with React + CRXJS + Vite + Docker

  • #react
  • #docker
  • #vite
  • #extension

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

Directory structure

.
├── 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

Create a project

npm init vite@latest

$ 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.

npm i vite-plugin-svgr

Change vite-env.d.ts

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

Update the Vite config

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.

{
	"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.

{
	"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/

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

FROM node:18.15.0-alpine3.16

WORKDIR /usr/src/app

COPY package*.json ./

RUN yarn install

COPY . .

docker-compose.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.

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.

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

Create Content Scripts

Add content_scripts section in manifest.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.

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.

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

{
	"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.

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.

{
	"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.

<!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.

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 in the Developer tool. If it works, I will update this post.

Thank you for reading.

Reference

GitHub Repository

Comments