rollup-plugin-chrome-extension
Build Chrome Extensions using Rollup, with minimal configuration.
Here are some of the things this Rollup plugin does:
Use manifest.json
as the input. Every file in the manifest will
be bundled or copied to the output folder.
Looking for Vite support?
Vite support is in beta! Get started with HMR and React in 90 seconds. The documentation on this site does not cover Vite, but you can get help on GitHub.
Getting started
Chrome Extension Boilerplates
We have TypeScript and JavaScript boilerplates available.
Get started fast with the JavaScript React boilerplate:
git clone https://github.com/extend-chrome/js-react-boilerplate.git
Or use the TypeScript React boilerplate if you're feeling fancy:
git clone https://github.com/extend-chrome/ts-react-boilerplate.git
Special thanks to @kyrelldixon for this Svelte and Tailwind CSS boilerplate with optional TypeScript support:
git clone https://github.com/kyrelldixon/svelte-tailwind-extension-boilerplate.git
Install
npm i rollup rollup-plugin-chrome-extension@latest -D
Install the plugins Node Resolve and CommonJS if you plan to use npm modules.
npm i @rollup/plugin-node-resolve @rollup/plugin-commonjs -D
Usage
Create a rollup.config.js
file in your project root.
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { chromeExtension, simpleReloader } from 'rollup-plugin-chrome-extension'
export default {
input: 'src/manifest.json',
output: {
dir: 'dist',
format: 'esm',
},
plugins: [
// always put chromeExtension() before other plugins
chromeExtension(),
simpleReloader(),
// the plugins below are optional
resolve(),
commonjs(),
],
}
Add these scripts to your package.json
file.
// package.json
{
"scripts": {
"build": "rollup -c",
"start": "rollup -c -w"
}
}
Put your Chrome extension source code in a folder named src
in
the root of your project and build with the following command:
npm run build
Your extension build will be in the dist
folder. It has
everything it needs: manifest, scripts, and assets (images, CSS,
etc.).
Load it in Chrome chrome://extensions/
to test drive your extension!
Features
It's all in the manifest
Why does the rollup.config.js
only need the manifest as an entry point?
rollup-plugin-chrome-extension
parses your manifest and bundles the scripts in
your background page, content scripts, options page, and popup page
What about HTML pages, like popup or options?
rollup-plugin-chrome-extension
uses the JS or even TS files in your HTML files
as entry points. Shared code is split out into chunks automatically, so
libraries like React and Lodash aren't bundled into your extension multiple
times.
What about the assets? Like images, icons, or CSS files?
All assets declared in the manifest (including files in
web_accessible_resources
, any image, icon, font, and even CSS files) are
automatically copied into the output folder. Even the images in your HTML files
get copied over. NOTE: This only includes assets in the HTML itself. If you
import images or CSS in a JavaScript file, you will need an additional plugin.
Is the Manifest validated?
rollup-plugin-chrome-extension
validates your output manifest, so you discover
mistakes when you build, not in a cryptic Chrome alert later.
Does it detect permissions automatically?
rollup-plugin-chrome-extension
statically analyzes your bundled code, detects
any required permissions, and adds them to the manifest in the dist
folder. Any
permissions in the source manifest are always included.
Do I have to copy/paste the package.json fields to the manifest?
You can omit manifest_version
, version
, name
, and description
from your source
manifest.json
. We'll fill them out automatically from your package.json
if you
use an npm script to run Rollup. Just manage your version number in package.json
and it will reflect in your extension build.
Don't worry. Any value in your source manifest will be used first! 😉
What about Manifest Version 3?
Manifest Version 3 is here! Google is recommending that developers adopt the new format as soon as possible, and rollup-plugin-chrome-extension
is fully compatible with Manifest Version 3.
Just follow the migration guide, setting manifest_version
to 3
, and we've got your back! No additional Rollup configuration is required.
Reload Your Extension Automatically
Does this mean I don't have to manually reload my extension during development?
Improve your development experience with our reloader! You won't have to reload your Chrome extension every time you make a change to your code. We know what a pain it can be to forget and wonder, "Why isn't this change working? 😟".
Does it also reload the pages I am injecting content scripts?
Ever got the error "Extension context invalidated"
in your content script?
It happens when the extension reloads, but the content script doesn't. Our
reloader makes sure that doesn't happen by reloading your content scripts when
it reloads your extension automatically.
How do I enable the reloader?
If you include the helper plugin simpleReloader
in your config, your background page will include an auto-reloader script when Rollup is in watch mode. This
will reload your extension every time Rollup produces a new build.
Write Chrome Extensions In TypeScript
If you use
@rollup/plugin-typescript
,
you can write your Chrome extension in TypeScript. That's right. It bundles the scripts in your manifest and in your HTML script
tags.
Be sure to install @types/chrome
to add TypeScript support for the Chrome API.
Can I write my Manifest in TypeScript?
Yes, you can! You can even use environment variables. Just make sure your manifest file name starts with manifest
so we don't include it in the build files.
import { ManifestV3 } from 'rollup-plugin-chrome-extension'
const manifest: ManifestV3 = {
manifest_version: 3,
background: {
service_worker: 'service-worker.ts',
},
content_scripts: [
{
js: ['content-script.ts'],
matches: ['https://*.example.com/*'],
},
],
}
export default manifest
Use ES2015 Modules In Your Scripts
Chrome extensions don't support modules in content scripts. We've created a module loader specifically for Chrome extension scripts, so you can take advantage of Rollup's great code-splitting features. It's enabled by default!
What About Firefox Support?
Until v89, Firefox did not support dynamic imports in web extensions, so any scripts needed to be in another format, like IIFE. The suggested solution was to run Parcel on the Rollup output, but this won’t be necessary once Firefox v89 is released.
Use Promises like it's 2021
Add the excellent promisified Browser API polyfill by Mozilla to your Chrome extension with one easy option:
chromeExtension({ browserPolyfill: true })
This option adds browser
to the global scope, so you don't need to import anything.
Install this type package to get Intellisense. It's automatically updated on a regular basis.
Plugins Take It To The Next Level
Take advantage of other great Rollup plugins to do awesome things with your Chrome extensions!
Some of our favorites are:
- Write your extension in TS with
@rollup/plugin-typescript
- Import CSS in JS files with
rollup-plugin-postcss
- Zip your extension when you build with
rollup-plugin-zip
. - Copy any assets not included in the manifest.json
rollup-plugin-copy
.
Two of our own plugins:
- Import a module as a string of code to use in
chrome.runtime.executeScript
withrollup-plugin-bundle-imports
- Empty your output folder before a new build with
rollup-plugin-empty-dir
Outputs a Chrome Web Store friendly bundle
Every time you publish your Chrome extension to the Web Store,
your extension will be reviewed by a robot and then a human to
make sure it meets their guidelines. Even if you pass when you
first publish, your extension may be flagged at any time.
rollup-plugin-chrome-extension
helps you put your best foot
forward.
Wrong permissions are the number one reason that Chrome
extensions are rejected from the Chrome Web Store.
rollup-plugin-chrome-extension
can detect most of the commonly
used permissions in your code automatically, so you only need to
add a permission manually if you absolutely know that you need
it.
Imagine the person who reviews the code you submit. Common bundling options like webpack and Parcel produce code that is really hard to read. Rollup produces code that is easy to read! When you submit your extension for review, you want to avoid misunderstandings.
Rollup produces a nice clean bundle using code splitting, ES modules, and tree-shaking. If you don't use some piece of code, Rollup removes it. If you use a module in more than once place, Rollup splits it out into a chunk, so that it's only in your extension once.
All of this means a smaller Chrome extension. We've seen Chrome
extensions go from over 8Mb to less than 1Mb just by switching
from create-react-app
to Rollup. A smaller bundle means less
code to review, and less room for error during the review
process.
API
rollup-plugin-chrome-extension
works out of the box, but
sometimes you need more.
Exports
chromeExtension
Call this function to initialize
rollup-plugin-chrome-extension
. Always put it first in the
plugins array, since it converts the manifest json file to an
array of input files. See Options API for config
details.
// rollup.config.js
import { chromeExtension } from 'rollup-plugin-chrome-extension'
export default {
input: 'src/manifest.json',
output: {
dir: 'dist',
format: 'esm',
},
plugins: [chromeExtension()],
}
simpleReloader
This reloader simply uses setInterval
to fetch a local
timestamp file every few seconds. When Rollup completes a new
build, it changes the timestamp and the Chrome extension reloads
itself.
If Rollup is not in watch mode, simpleReloader
disables
itself`.
Make sure to do your final build outside of watch mode so that it doesn't include the reloader.
Usage for simpleReloader
import { chromeExtension, simpleReloader } from 'rollup-plugin-chrome-extension'
export default {
input: 'src/manifest.json',
output: {
dir: 'dist',
format: 'esm',
},
plugins: [
chromeExtension(),
// Reloader goes after the main plugin
simpleReloader(),
],
}
Start Rollup in watch mode. Enjoy auto-reloading whenever Rollup makes a new build.
Manifest API
[permissions]
If a wrong permission has been detected
Sometimes a third-party module will reference a Chrome API to detect its environment, but you don't need the permission in your manifest.
// wrong permissions in output manifest.json
{
"permissions": [
"alarms", // This should not be here
"storage"
]
}
Solution: Prefix unwanted permissions in the manifest with
"!"
, for example, "!alarms"
.
// source manifest.json
{
"permissions": [
"!alarms", // This permission will be excluded
"storage"
]
}
// correct permissions in output manifest.json
{
"permissions": ["storage"]
}
[web_accessible_resources]
If you have files that are not imported to a script, or
referenced directly in the manifest or an HTML file, add them to
web_accessible_resources
.
They will be written to output.dir
with the same folder
structure as the source folder (the folder with the manifest
file). Relative paths may not lead outside of the source folder.
{
"web_accessible_resources": [
"fonts/some_font.oft",
// HTML files are parsed like any other HTML file.
"options2.html",
// Globs are supported too!
"**/*.png"
]
}
Options API
You can use an options object with any of the following properties. Everything is optional.
[browserPolyfill]
Add the excellent promisified Browser API by Mozilla to your Chrome extension with one easy option:
chromeExtension({
browserPolyfill: true,
})
Don't forget to install types if you want Intellisense to work!
[dynamicImportWrapper]
We use dynamic imports to support ES2015 modules and code splitting for JS files.
Use modules in Chrome extension scripts. Only disable if you
know what you're doing, because code splitting won't work if
dynamicImportWrapper === false
.
Why do we need to use dynamic import in scripts? Two things are going on here: This Rollup plugin leverages two Rollup features to parse the manifest into inputs:
- It adds multiple parsed files to options.input
- It uses options.output.dir to support multiple output files. This means that Rollup will use code-splitting. This is great because it makes a smaller bundle with no overlapping code, but we need a way to load those chunks into our content and background scripts. After some experimentation, I found that ES modules are the best format for web extensions, but they don’t support ES modules in background or content scripts out of the box.
The solution is to use dynamic imports in extension scripts. All Chromium browsers and Firefox 89+ (coming May 2021) support this.
[dynamicImportWrapper.wakeEvents]
Events that wake (reactivate) an extension may be lost if that extension uses dynamic imports to load modules or asynchronously adds event listeners.
The script module loader will defer them until after all the background script modules have fully loaded. Once this is complete, the listeners are removed.
By default the module loader will iterate through the events available on the Chrome API object and add listeners to each one. Since the manifest permissions determine which Chrome namespaces are available, only a small superset of the events you use will be used.
If you want to limit the number of events that the module loader
uses, you can list the events that should wake your background page
(for example, 'chrome.tabs.onUpdated'
, or 'chrome.runtime.onInstalled'
).
// Example usage
chromeExtension({
dynamicImportWrapper: {
wakeEvents: ['chrome.contextMenus.onClicked'],
},
})
[dynamicImportWrapper.eventDelay]
Delay Event page wake events by n
milliseconds after the all
background page modules have finished loading. This may be useful
for event listeners that are added asynchronously.
chromeExtension({
dynamicImportWrapper: {
eventDelay: 50,
},
})
[verbose]
Set to false
to suppress "Detected permissions" message.
// Example usage
chromeExtension({
verbose: false,
})
// Default value
chromeExtension({
verbose: true,
})
[pkg]
Only use this field if you will not run Rollup using npm scripts
(for example, $ npm run build
), since npm provides scripts with
the package info as an environment variable.
The fields name
, description
, and version
are used.
These values are used to derive certain values from the
package.json
for the extension manifest. A value set in the
source manifest.json
will override a value from package.json
.
// Example usage
const packageJson = require('./package.json')
chromeExtension({
// Not needed if you use npm to run Rollup
pkg: packageJson,
})
// Default value
chromeExtension({
// Can be omitted if run using an npm script
})
[publicKey]
(deprecated)
If truthy, manifest.key
will be set to this value. Use this
feature to
stabilize the extension id during development.
Note that this value is not the actual id. An extension id is derived from this value.
const p = process.env.NODE_ENV === 'production'
// Example usage
chromeExtension({
publicKey: !p && 'mypublickey',
})