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
- Dynamic manifest with TypeScript
- Manifest icons and public assets
- Web accessible resources
- Dynamic content scripts
- Injected main world scripts
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
// manifest.json
{
"manifest_version": 3,
"version": "1.0.0",
"name": "example",
"devtools_page": "src/devtools.html"
}
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 })],
});
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,
}))
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"]
}]
}
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)
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]
});
});
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/*'],
})
]
})
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)
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)
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...
The best way to get support is to start a discussion on GitHub:
github.com/crxjs/chrome-extension-...
See you there!
Do you have an github example project for this?
Check out this devtools extension starter:
github.com/jacksteamdev/crx-react-...
how to implement/create a background script?
From my understanding, you will just need to specify your background script in your manifest file. Something like
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.This is exactly correct! No extra setup needed for production.
this will work perfectly?
Very nice!
i have a question
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.
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 ?
How can we dynamically add and remove CSS files?
import mainWorld from './main-world?script&module'
I don't think the
script
andmodule
params are a Vite feature. I guess it's handled by the CRX plugin?