DEV Community

Jack Steam
Jack Steam

Posted on • Updated on

Real Vite-React HMR in Chrome Extension Content Scripts

A content script is JavaScript from a Chrome Extension that the browser executes on a designated host page. It shares the DOM with the host page but has a separate JavaScript environment.

If you've done any work on Chrome Extensions, you know that content scripts are one of the essential tools at your disposal. However, you also know they are a PITA to develop.

The traditional content script developer experience is as follows:

  1. Make changes to your code
  2. Reload the extension in chrome://extensions
  3. Reload the host page
  4. Check that things work right
  5. Repeat

Forget a step, and your changes don't show up. Hopefully, you realize what's happening before you start debugging. 😅

Adding DOM elements to a website is a standard content script use case. Unfortunately, you need to bundle your code to use a framework like React or Vue in a content script.

Vite does a great job serving code for the browser, but content scripts load their code from the file system, so Vite's sweet HMR experience doesn't work out of the box. Until now.

Good News for Content Script DX

You can have HMR in content scripts; you can say goodbye to the tedious workflow they represent. My name is Jack Steam, and I'm the creator of the CRXJS Vite plugin. Today CRXJS brings an authentic Vite HMR experience to content scripts for the first time. Let me show you how to get started.

If you're coming from my first article, Create a Vite-React Chrome Extension in 90 seconds, you already know how to get started; you can skip this next bit. Instead, scroll down to the next heading, "Add a content script".

Getting Started

Using your favorite package manager, initialize a new Vite project. Follow the prompts to set up your project. CRXJS works with React and Vue, but we'll use React for this guide.

CRXJS doesn't yet work with Vite 3, but support is coming soon.

npm init vite@^2
cd <your-project-name>
npm install
npm i @crxjs/vite-plugin@latest -D
Enter fullscreen mode Exit fullscreen mode

Open vite.config.js and add CRXJS:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+ import { crx } from '@crxjs/vite-plugin'

export default defineConfig({
- plugins: [react()]
+ plugins: [react(), crx()]
})
Enter fullscreen mode Exit fullscreen mode

Chrome Extensions declare their resources using a manifest.json file. Create your manifest next to vite.config.js with these fields:

{
  "manifest_version": 3,
  "name": "Vite React Chrome Extension",
  "version": "1.0.0"
}
Enter fullscreen mode Exit fullscreen mode

Go back to vite.config.js and add the manifest:

// other imports...
+ import manifest from './manifest.json'

export default defineConfig({
- plugins: [react(), crx()]
+ plugins: [react(), crx({ manifest })]
})
Enter fullscreen mode Exit fullscreen mode

Add a content script

We declare content scripts with a list of JavaScript files and match patterns for the pages where Chrome should execute our content script. In manifest.json, create the field content_scripts with an array of objects:

{
  // other fields...
  "content_scripts": [{
    "js": ["src/main.jsx"],
    "matches": ["https://www.google.com/*"]
  }]
}
Enter fullscreen mode Exit fullscreen mode

Here we're telling Chrome to execute src/main.jsx on all pages that start with https://www.google.com.

Create the root element

Content scripts don't use an HTML file, so we need to create our root element and append it to the DOM before mounting our React app. Open src/main.jsx and add a root element.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'

+ const root = document.createElement('div')
+ root.id = 'crx-root'
+ document.body.append(root)

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
- document.getElementById('root')
+ root
)
Enter fullscreen mode Exit fullscreen mode

Get the right URL

Content scripts share the origin of their host page. We need to get a URL with our extension id for static assets like images. Let's go to src/App.jsx and do that now.

<img
- src={logo}
+ src={chrome.runtime.getURL(logo)}
  className="App-logo"
  alt="logo"
/>
Enter fullscreen mode Exit fullscreen mode

Now our content script is ready for action! But, first, let's load our extension in Chrome.

Load the extension

Start Vite in the terminal.

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open the extensions dashboard at the URL chrome://extensions and turn on development mode using the switch in the top right corner. Next, load your extension by dragging the dist folder onto the extensions dashboard.

Profit with HMR

Navigate to https://www.google.com and scroll to the bottom of the page. There's our familiar Vite Hello World!

Notice how the counter button doesn't look like a button. That's because Google's styles affect our content script elements. The same goes the other way: our styles change Google's styles.

Let's fix that. Replace everything in src/index.css with this:

#crx-root {
  position: fixed;
  top: 3rem;
  left: 50%;
  transform: translate(-50%, 0);
}

#crx-root button {
  background-color: rgb(239, 239, 239);
  border-color: rgb(118, 118, 118);
  border-image: initial;
  border-style: outset;
  border-width: 2px;
  margin: 0;
  padding: 1px 6px;
}
Enter fullscreen mode Exit fullscreen mode

CRXJS will quickly rebuild the content script, and our CSS changes will take effect. Now our div position is fixed, and that button looks more like a button! Click the count button and play around with src/App.jsx to see Vite HMR at work.

We need your feedback! If something doesn't work for you, please create an issue.

Conversely, if CRXJS has improved your developer experience, please consider sponsoring me on GitHub or give me a shoutout on Twitter. See you next time.

Good luck building your Chrome Extension!

Oldest comments (13)

Collapse
 
dodobird profile image
Caven

Vite + vue, css is not applied.

github.com/keyding/vite-crx-vue-test

Collapse
 
jacksteamdev profile image
Jack Steam • Edited

Thanks for creating an issue! I'm glad we were able to get things resolved.

Collapse
 
moinulmoin profile image
Moinul Moin

Great!

how to work with background.js for service worker? you should include it in the doc

Collapse
 
jacksteamdev profile image
Jack Steam

Great idea! We're working on the docs, you can take peek here: crxjs.dev/vite-plugin

Check out this comment for instructions on the background service worker: dev.to/lichenglu/comment/1od4p

Collapse
 
moinulmoin profile image
Moinul Moin

I checked, I did the same in maniest json. this code will work fine inside it?

Thread Thread
 
moinulmoin profile image
Moinul Moin
// path: src/background/index.js

console.log('I am background.js');

chrome.action.onClicked.addListener((tab) => {
    chrome.scripting.executeScript({
        target: { tabId: tab.id },
        files: ['src/main.jsx'],
    });
});

Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
moinulmoin profile image
Moinul Moin

I am actuallyy trying to inject the content script by clicking on the icon of extension. so, kindly tell me how it will work, I tried with this code. but It throw error at this line

files: ['src/main.jsx'],
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
jacksteamdev profile image
Jack Steam

You can import the script with a script query:

// note how the import path ends with `?script`
import scriptPath from './content-script?script'

chrome.action.onClicked.addListener((tab) => {  
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: [scriptPath]
  });
});
Enter fullscreen mode Exit fullscreen mode

This will also provide HMR for the imported script. More info: Dynamic content scripts

Collapse
 
sanujbansal profile image
sanuj bansal

I see this error

Image description
when I try to add:-
"content_scripts": [
{
"js": ["src/main.jsx"],
"matches": ["google.com/*"]
}
]
Any solutions for this?

Collapse
 
jacksteamdev profile image
Jack Steam

This is a known issue with the new Vite 3.0, but you can install the latest versions of Vite 2 and plugins and things should work as expected:

npm i -D vite@^2 @vitejs/plugin-react@^1
Enter fullscreen mode Exit fullscreen mode

I'm updating the articles so this doesn't continue to trip people up.

Collapse
 
shivraj97 profile image
Shivraj97 • Edited

Hi Jack, This resolved the issue for content script hot reloading but the pop up always shows Waiting for service worker..

Image description

and this is the Error shown in Brave extension page

Image description

Collapse
 
derekzyl profile image
derekzyl

I need information on how to setup content script with Tsx. thank you

Collapse
 
kobimantzur profile image
Kobi Mantzur

I just spent 2 days of trying to develop a content script with Webpack and HMR with no luck. Thank you!! It works like a charm 🪄