Chrome Extension with React + CRXJS + Vite + Docker

mk668a

mk668a

Posted on May 9, 2023

Chrome Extension with React + CRXJS + Vite + Docker

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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 }),
    ],
});

Enter fullscreen mode Exit fullscreen mode

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"
    }
}

Enter fullscreen mode Exit fullscreen mode

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"]
}

Enter fullscreen mode Exit fullscreen mode

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 . .
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

Fix src attribute of script tag in index.html.

<script type="module" src="/src/popup.tsx"></script>
Enter fullscreen mode Exit fullscreen mode

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"]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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>
);

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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"
    ]
}
Enter fullscreen mode Exit fullscreen mode

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 {};

Enter fullscreen mode Exit fullscreen mode

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"
    ]
}

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
mk668a
mk668a

Posted on May 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related