DEV Community

Jack Steam
Jack Steam

Posted on • Updated on

Advanced Config for CRXJS Vite Plugin

The CRXJS Vite plugin has been in beta long enough to get a good sample of developer use cases. Thanks to everyone who has helped by creating issues and participating in discussions! Your input is super valuable.

These are some answers to the most common developer questions we've fielded. I'm Jack Steam, creator of CRXJS. So, if you're looking to add extra HTML pages, extend the manifest at build time, use the new Chrome Scripting API, and inject main world scripts, read on!

Table of Contents

Extra HTML pages

It's pretty common for an extension to have web pages that you can't declare in the manifest. For example, you might want to change the popup once the user signs in or open a welcome page when the user installs the extension. In addition, devtool extensions like React Developer Tools don't declare their inspector panels in the manifest.

Given the following file structure and manifest, index.html and src/panel.html will be available during development but not in a production build. We can fix this in vite.config.ts.

.
├── vite.config.ts
├── manifest.json
├── index.html
└── src/
    ├── devtools.html
    └── panel.html
Enter fullscreen mode Exit fullscreen mode
// manifest.json
{
  "manifest_version": 3,
  "version": "1.0.0",
  "name": "example",
  "devtools_page": "src/devtools.html"
}
Enter fullscreen mode Exit fullscreen mode

To build extra HTML pages, follow the pattern from the Vite Documentation for Multi-Page Apps:

// vite.config.js
import { resolve } from 'path';
import { defineConfig } from 'vite';
import { crx } from '@crxjs/vite-plugin';
import manifest from './manifest.json';

export default defineConfig({
  build: {
    rollupOptions: {
      // add any html pages here
      input: {
        // output file at '/index.html'
        welcome: resolve(__dirname, 'index.html'),
        // output file at '/src/panel.html'
        panel: resolve(__dirname, 'src/panel.html'),
      },
    },
  },
  plugins: [crx({ manifest })],  
});
Enter fullscreen mode Exit fullscreen mode

Dynamic manifest with TypeScript

CRXJS treats the manifest as a config option and transforms it during the build process. Furthermore, since the manifest is a JavaScript Object, it opens up some exciting ways to extend it.

Imagine writing your manifest in TypeScript. Use different names for development and production. Keep the version number in sync with package.json. 🤔

The Vite plugin provides a defineManifest function that works like Vite's defineConfig function and provides IntelliSense, making it easy to extend your manifest at build time.

// manifest.config.ts

import { defineManifest } from '@crxjs/vite-plugin'
import { version } from './package.json'

const names = {
  build: 'My Extension',
  serve: '[INTERNAL] My Extension'
}

// import to `vite.config.ts`
export default defineManifest((config, env) => ({
  manifest_version: 3,
  name: names[env.command],
  version,
}))
Enter fullscreen mode Exit fullscreen mode

Manifest icons and public assets

If you've used Vite for a website, you might be familiar with the public directory. Vite copies the contents of public to the output directory.

You can refer to public files in the manifest. If CRXJS doesn't find a matching file in public, it will look for the file relative to the Vite project root and add the asset to the output files.

You're free to put your icons in public or anywhere else that makes sense!

// manifest.json 
{
  "icons": {
    // from src/icons/icon-16.png
    "16": "src/icons/icon-16.png",
    // from public/icons/icon-24.png 
    "24": "icons/icon-24.png"
  },
  "web_accessible_resources": [{
    matches: ['https://www.google.com/*'],
    // copies all png files in src/images
    resources: ["src/images/*.png"]
  }]
}
Enter fullscreen mode Exit fullscreen mode

The plugin will also copy files that match globs in web_accessible_resources.

CRXJS ignores the globs * and **/*. You probably don't want to copy package.json and everything in node_modules. The real question is, should a website have access to every single file in your extension?

What are web-accessible resources, anyways?

Web Accessible Resources

The files in your Chrome Extension are private by default. So, for example, if your extension has the file icon.png, extension pages can access it, but random websites cannot (it's not a web-accessible resource). If you want an extension resource to be web-accessible, you need to declare the file in the manifest under web_accessible_resources.

What if I want to use an image in a content script? It has to be web-accessible. Why? Content scripts share the origin of the host page, so a web request from a content script on https://www.google.com is the same as a request from https://www.google.com itself.

It can get tedious to update the manifest with every file you're using. We're using build tools, so why do more manual work than necessary? When you import an image into a content script, CRXJS updates the manifest automatically. ✨

All you need to do is wrap the import path with a call to chrome.runtime.getURL to generate the extension URL:

import logoPath from './logo.png'

const logo = document.createElement('img')
logo.src = chrome.runtime.getURL(logo)
Enter fullscreen mode Exit fullscreen mode

These files are accessible anywhere the content script runs. Also, these assets use a dynamic url, so malicious websites can't use it to fingerprint your extension!

Dynamic Content Scripts

The Chrome Scripting API lets you execute content scripts from the background of a Chrome Extension.

The manifest doesn't have a place to declare dynamic content scripts, so how do we tell Vite about them? Of course, we could add them to the Vite config like an extra HTML page, but how does CRXJS know that we intend the added script to be a content script? Does it need the unique flavor of HMR that CRXJS provides? What about web-accessible resources?

CRXJS uses a unique import query to designate that an import points to a content script. When an import name ends with the query ?script, the default export is the output filename of the content script. You can then use this filename with the Chrome Scripting API to execute that content script and profit from Vite HMR.

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

The resources of a dynamic content script are available to all URLs by default, but you can tighten that up using the defineDynamicResource function:

import { defineManifest, defineDynamicResource } from '@crxjs/vite-plugin'

export default defineManifest({
  ...manifest,
  web_accessible_resources: [
    defineDynamicResource({
      matches: ['https://www.google.com/*'],
    })
  ]
})
Enter fullscreen mode Exit fullscreen mode

Main world scripts

Content scripts run in an isolated world, but sometimes a script needs to modify the execution environment of the host page. Content scripts usually do this by adding a script tag to the DOM of their host page. The main world script must be web-accessible like any other content script asset.

A dynamic content script import gets us close, but a script imported using ?script includes a loader file that adds Vite HMR. Unfortunately, the loader relies on the Chrome API only available to content scripts; it won't work in the host page execution environment. What we need is a simple ES module.

You can skip the loader file using the ?script&module import query:

// content-script.ts
import mainWorld from './main-world?script&module'

const script = document.createElement('script')
script.src = chrome.runtime.getURL(mainWorld)
script.type = 'module'
document.head.prepend(script)
Enter fullscreen mode Exit fullscreen mode

Now get out there and read global variables, reroute fetch requests, and decorate class prototypes to your heart's content!

Roadmap

The next thing on CRXJS's roadmap is proper documentation and a better release process. But, don't worry, we're not done adding features and fixing bugs; you can look forward to Shadow DOM in content scripts and better Vue support. I'm also incredibly excited about adding official support for Svelte and Tailwind!

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

Top comments (13)

Collapse
 
ljcravero profile image
ljcravero

I've a question... I can't generate correctly my extension using crxjs plugin... when I trying to submit my zip generated after running npm run build, later I can't view my extension running correctly.
I'm actually using React, Vite and CRXJS plugin.
Can you help me to publish my extension? I'm trying to publish it for my actually company...

Collapse
 
jacksteamdev profile image
Jack Steam • Edited

The best way to get support is to start a discussion on GitHub:

github.com/crxjs/chrome-extension-...

See you there!

Collapse
 
moshontongit profile image
Muslimin Ontong

Do you have an github example project for this?

Collapse
 
jacksteamdev profile image
Jack Steam

Check out this devtools extension starter:
github.com/jacksteamdev/crx-react-...

Collapse
 
moshontongit profile image
Muslimin Ontong

how to implement/create a background script?

Thread Thread
 
lichenglu profile image
Chenglu Li

From my understanding, you will just need to specify your background script in your manifest file. Something like

{
  ...[this is your custom manifest, not the generated one in dist]
  "background": {
      "service_worker": "src/background/index.ts",
      "type": "module"
    }
}
Enter fullscreen mode Exit fullscreen mode

Then in the dist folder, if you check service-worker-loader.js, you will see your background script imported there. I have only tested it in the dev command, not sure if extra set up is needed for production build.

Thread Thread
 
jacksteamdev profile image
Jack Steam

This is exactly correct! No extra setup needed for production.

Thread Thread
 
moinulmoin profile image
Moinul Moin

this will work perfectly?

Collapse
 
dodobird profile image
Caven

Very nice!

Collapse
 
derekzyl profile image
derekzyl • Edited

i have a question

{
  "manifest_version": 3,
  "name": "CRXJS React Vite Example",
  "version": "1.0.0",
  "action": { "default_popup": "index.html" },
  "content_scripts": [
    {
      "js": ["src/content.jsx"],
      "matches": [

        "https://developer.chrome.com/docs/webstore/*"
      ]
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

how do I handle this in typescipt, the content has to be in tsx in typescript whats the hack i can use.
i.e content.tsx.

Collapse
 
cruelengine profile image
B Abhineeth Bhat

I have an error with the dynamic import of a content script in background script.
I'm trying to do :
import myScript from './content?script' but i get a compilation error as well as "Cannot find module './content?script' or its corresponding type declarations.ts(2307)"

How do i solve it ?

Collapse
 
prakhartiwari0 profile image
Prakhar Tiwari

How can we dynamically add and remove CSS files?

Collapse
 
lookrain profile image
Lu Yu

import mainWorld from './main-world?script&module'

I don't think the script and module params are a Vite feature. I guess it's handled by the CRX plugin?