/vite-plugin-web-extension

Vite plugin for bundling Chrome/Browser Extensions

Primary LanguageTypeScriptMIT LicenseMIT

Vite Plugin Web Extension

A simple but powerful Vite plugin for developing browser extensions

// vite.config.ts
import webExtension from "vite-plugin-web-extension";

export default defineConfig({
  plugins: [
    webExtension({
      manifest: path.resolve(__dirname, "manifest.json"),
      assets: "assets",
    }),
  ],
});

Features

  • 🔧 Automatically build inputs from in your manifest.json
  • ⚡ Super fast watch mode that automatically reloads your extension
  • 🌐 Supports all browsers
  • 🔥 Frontend frameworks for the popup, options page, and content scripts!
  • 🤖 Typescript support out of the box!
  • ✅ Manifest validation

Contributing

Special thanks to the contributors!

aklinker1
Aaron Klinker
KentoNishi
Kento Nishi
r2dev2
Ronak Badhe

See the contributing docs to setup the project for development.

Installation

npm i -D vite-plugin-web-extension

Roadmap

  • v0.1.0 Build for production
  • v0.2.0 CSS inputs & generated files
  • v0.3.0 Dev mode with automatic reload
  • v0.5.0 Manifest V3 support
  • v0.6.0 Frontend framework support in content scripts
  • v0.7.0 Browser specific flags in the manifest
  • HMR for html pages

Setup and Usage

Lets say your project looks like this:

dist/
   build output...
src/
   assets/
      icon-16.png
      icon-48.png
      icon-128.png
   background/
      index.ts
   popup/
      index.html
   manifest.json
package.json
vite.config.ts
...

Here's the minimal setup required:

// vite.config.ts
import webExtension from "vite-plugin-web-extension";

export default defineConfig({
  root: "src",
  // Configure our outputs - nothing special, this is normal vite config
  build: {
    outDir: path.resolve(__dirname, "dist"),
    emptyOutDir: true,
  },
  // Add the webExtension plugin
  plugins: [
    webExtension({
      manifest: path.resolve(__dirname, "src/manifest.json"),
      assets: "assets",
    }),
  ],
});

Note that the assets option is relative to your Vite root. In this case, it's pointing to src/assets, not just assets.

You don't need to specify a root if you don't want to. When excluded, it defaults to the directory your vite.config.ts is in.

For the input manifest option, all paths should use their real file extension and the paths should be relative to your vite root.

// src/manifest.json
{
  "name": "Example",
  "version": "1.0.0",
  "manifest_version": "2",
  "icons": {
    // Relative to "src"
    "16": "assets/icon-16.png",
    "48": "assets/icon-48.png",
    "128": "assets/icon-128.png"
  },
  "browser_action": {
    "default_icon": "assets/icon-128.png",
    // Relative to "src"
    "default_popup": "popup/index.html"
  },
  "background": {
    // Relative to "src", using real .ts file extension
    "scripts": "background/index.ts"
  }
}

And there you go!

Run vite build and you should see a fully compiled and working browser extension in your dist/ directory!

How does this work?

The build process happens in 2 steps:

  1. Bundle all the HTML entry-points as a multi-page app
  2. Bundle everything else (background scripts/service worker, content scripts, etc) individually in library mode

Scripts have to be bundled individually, separate from each other and the HTML entry-points, because they cannot import additional JS files. Each entry-point needs to have everything it needs inside that one file listed in the final manifest.

Adding Frontend Frameworks

If you want to add a framework like Vue or React, just add their Vite plugin!

import vue from '@vitejs/plugin-vue'

export default defineConfig({
  ...
  plugins: [
    vue(),
    webExtension({ ... }),
  ],
});

You can now use the framework anywhere! In your popup, options page, content scripts, etc.

See demos/vue for a full example.

Advanced Features

Configuring Browser Startup

This plugin uses web-ext under the hood to startup a browser and install the extension in dev mode. You can configure web-ext via the webExtConfig option.

For a list of options, you'll have to look at web-ext's source code, and search for .command('run', then camelCase each flag. If it's type is array, set it equal to an array of the values.

Here are some examples (with their CLI equivalents above):

webExtension({
  webExtConfig: {
    // --chromium-binary /path/to/google-chrome
    "chromiumBinary": "/path/to/google-chrome",
    // --start-url google.com --start-url duckduckgo.com
    "startUrl": ["google.com", "duckduckgo.com"],
    // --watch-ignored *.md *.log
    "watchIgnored": ["*.md", "*.log"],
  }
});

Also see #22 for a real use case of changing the startup chrome window size

Watch Mode

To reload the extension when a file changes, run vite with the --watch flag

vite build --watch

To reload when you update files other than source files (config files like tailwind.config.js) pass the watchFilePaths option. Use absolute paths for this option:

import path from "path";

export default defineConfig({
  ...
  plugins: [
    webExtension({
      watchFilePaths: [
        path.resolve(__dirname, "tailwind.config.js")
      ],
      disableAutoLaunch: false // default is false
    }),
  ],
});

Watch mode will not reload properly when the manifest changes. You'll need to restart the vite build --watch command use the updated manifest.

This is a limitation of web-ext

Set disableAutoLaunch to true to skip the automatic installation of the extension.

Additional Inputs

If you have have HTML or JS files that need to be built, but aren't listed in your manifest.json, you can add them via the additionalInputs option.

The paths should be relative to the Vite's root, just like the assets option.

export default defineConfig({
  plugins: [
    webExtension({
      ...
      additionalInputs: [
        "onboarding/index.html",
        "content-scripts/injected-from-background.ts",
      ]
    }),
  ],
});

CSS

For HTML entry points like the popup or options page, css is automatically output and referenced in the built HTML. There's nothing you need to do!

Manifest content_scripts

For content scripts listed in your manifest.json, it's a little more difficult. There are two ways to include CSS files:

  1. You have a CSS file in your project
  2. The stylesheet is generated by a framework like Vue or React or is imported by the code

For the first case, it's simple! Make sure you have the relevant plugin installed to parse your stylesheet (like scss), then list the file in your manifest.json. The plugin will look at the css array and output all inputs as plain CSS.

{
  "content_scripts": [
    {
      "matches": [...],
      "css": ["content-scripts/some-style.scss"]
    }
  ]
}

For the second case, it's a little more involved. Say your content script is at content-scripts/overlay.ts and is responsible for binding a Vue/React app to a webpage. When Vite compiles it, it will output two files: dist/content-scripts/overlay.js and dist/content-scripts/overlay.css. Check what is output, then update your manifest to point towards the output files, prefixed with generated:.

{
  "content_scripts": [
    {
      "matches": [...],
      "scripts": "content-scripts/overlay.ts",
      "css": ["generated:content-scripts/overlay.css"]
    }
  ]
}

This will tell the plugin that the file is already being generated for us, but that we still need it in the final manifest.

Browser API tabs.executeScripts

For content scripts injected programmatically, include the script's path in the plugin's additionalInputs option

Dynamic Manifests

The manifest option also accepts a function. This function should return a javascript object containing the same thing as manifest.json. It should include real file paths, as well as any browser specific flags (see next section).

Often times this is used to pull in details from your package.json like the version so they only have to be maintained in a single place

import webExtension from "vite-plugin-web-extension";

export default defineConfig({
  plugins: [
    webExtension({
      manifest: () => {
        // Generate your manifest
        const packageJson = require("./package.json");
        return {
          ...require("./manifest.json"),
          name: packageJson.name,
          version: packageJson.version,
        };
      },
      assets: "assets",
    }),
  ],
});

Browser Specific Manifest Fields

Either the file or object returned by the manifest option can include flags that specify certain fields for certain browsers, and the plugin will strip out any values that aren't for a specific browser

Here's an example: Firefox doesn't support manifest V3 yet, but chrome does!

{
  "{{chrome}}.manifest_version": 3,
  "{{firefox}}.manifest_version": 2,
  "{{chrome}}.action": {
    "default_popup": "index.html"
  },
  "{{firefox}}.browser_action": {
    "default_popup": "index.html",
    "browser_style": false
  },
  "options_page": "options.html",
  "permissions": ["activeTab", "{{firefox}}.<all_urls>"]
}

To build for a specific browser, simply pass the browser option and prefix any field name or string value with {{browser-name}}.. This is not limited to just chrome and firefox, you can use any string inside the double curly braces as long as your pass it into the plugin's browser option.

You can pass this option in a multitude of ways. Here's one way via environment variables!

# In package.json or via CLI
cross-env TARGET_BROWSER=chrome vite build
export default defineConfig({
  plugins: [
    webExtension({
      manifest: "manifest.json",
      assets: "assets",
      browser: process.env.TARGET_BROWSER,
    }),
  ],
});

Manifest Validation

Whenever your manifest is generated, it gets validated against Google's JSON schema: https://json.schemastore.org/chrome-manifest

To disable validation, pass the skipManifestValidation option:

export default defineConfig({
  plugins: [
    webExtension({
      skipManifestValidation: true,
    }),
  ],
});