Chrome Extension with React + CRXJS + Vite + Docker
mk668a
Posted on May 9, 2023
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
- Introduction | CRXJS Vite Plugin
- Create a project | CRXJS Vite Plugin
- vite-plugin-svgr
- Chrome Extensions Declare permissions
GitHub Repository
Posted on May 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.