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
Top comments (1)
Thank's for your effort buddy,