rollup-plugin-chrome-extension logo

rollup-plugin-chrome-extension

NPMGitHubGitHub ActionTypeScript


Build Chrome Extensions using Rollup, with minimal configuration.

Here's 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.

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, option 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 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? That 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, when Rollup is in watch mode your background page will include an auto-reloader script. 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:

Two of our own plugins:

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',
})