npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@taisan11/vite-plugin-webext

v0.6.4

Published

Vite plugin for cross-browser web extension builds

Readme

@taisan11/vite-plugin-webext

Create a minimal WebExtension project in an empty directory:

bunx @taisan11/vite-plugin-webext init .

A simple Vite plugin to easily create lightweight extensions.

Browser target by --mode

Use Vite mode to pick the browser:

vite build --mode chrome
vite build --mode firefox

If webext({ defaultBrowser }) or webext({ browser }) is also set, mode value takes precedence.

Default browser without --mode

You can set a fallback browser when build mode is not chrome / firefox:

webext({
  defaultBrowser: 'chrome',
})

browser is still supported as a backward-compatible alias of defaultBrowser.

Plugin usage

import { defineConfig } from 'vite'
import { webext } from '@taisan11/vite-plugin-webext'

export default defineConfig({
  plugins: [
    webext({
      manifest: {
        manifest_version: 3,
        name: 'My Extension',
        version: '1.2.3',
        background: { service_worker: 'src/background.ts', type: 'module' },
      },
    }),
  ],
})

manifest can be:

  • WebExtensionManifest
  • (browser) => WebExtensionManifest

Injecting unlisted scripts

Configure scripts that should be bundled as extension resources without adding them to content_scripts:

webext({
  unlistedScripts: {
    mainWorld: 'src/main-world.ts',
  },
  manifest: {
    manifest_version: 3,
    name: 'My Extension',
    version: '1.0.0',
    content_scripts: [{ matches: ['*://*/*'], js: ['src/content.ts'] }],
  },
})

From a content script, call injectScript('mainWorld') to inject the generated mainWorld.js into the page's main world:

The unlisted script itself can use defineUnlistedScript:

import { defineUnlistedScript } from '@taisan11/vite-plugin-webext/inject-script'

export default defineUnlistedScript(() => {
  console.log('running in the page')
})
import { injectScript } from '@taisan11/vite-plugin-webext/inject-script'

await injectScript('mainWorld')

The plugin automatically adds the configured scripts to web_accessible_resources.

build.rolldownOptions.input example

The plugin automatically collects bundle entry inputs from the manifest you pass (background service worker/scripts, popup, options, devtools, side panel, sidebar, Chrome URL overrides, sandbox pages). You normally do not need to configure build.rolldownOptions.input manually:

import { defineConfig } from 'vite'
import { webext } from '@taisan11/vite-plugin-webext'

export default defineConfig({
  plugins: [
    webext({
      manifest: {
        manifest_version: 3,
        name: 'My Extension',
        version: '1.0.0',
        background: { service_worker: 'src/background.ts', type: 'module' },
        action: { default_popup: 'src/popup/index.html' },
        options_ui: { page: 'src/options/index.html' },
      },
    }),
  ],
})

To add extra inputs not declared in the manifest, pass them via build.rolldownOptions.input. User-provided inputs are merged on top of the auto-derived ones (user keys win):

import { defineConfig } from 'vite'
import { resolve } from 'node:path'
import { webext } from '@taisan11/vite-plugin-webext'

export default defineConfig({
  plugins: [webext({ manifest: (browser) => ({ manifest_version: 3, name: browser, version: '1.0.0' }) })],
  build: {
    rolldownOptions: {
      input: {
        content: resolve(__dirname, 'src/content/index.html'),
      },
    },
  },
})

Extension API namespaces

Write extension code with browser.* by default. The plugin rewrites API namespace references to the active browser target during the build:

  • Chrome builds use chrome.*.
  • Firefox builds use browser.*.
  • No runtime shim is injected.

Static rewriting is enabled by default. Set staticTransform: false to keep the source namespace unchanged. The legacy injectGlobals option is accepted as an alias.

When running on Rolldown with experimental.nativeMagicString: true, the plugin automatically uses the native meta.magicString object when it is available and falls back to the JavaScript magic-string package otherwise.

TypeScript setup for browser.*

Create src/env.d.ts in your extension project and add:

/// <reference types="@taisan11/vite-plugin-webext/types" />

This enables typings for the global browser.* API and import.meta.env.BROWSER / IS_CHROME / IS_FIREFOX.

i18n helper (t(id))

Enable i18n transform in plugin options:

webext({
  i18n: true,
})

Export defineLocale(...) from src/locale/[localeName].ts to register message ids:

import { defineLocale } from '@taisan11/vite-plugin-webext/i18n'

export default defineLocale({
  appTitle: 'My Extension',
  openSettings: 'Open Settings',
  notificationContent: {
    message: 'You clicked $URL$.',
    description: 'Tells the user which link they clicked.',
    placeholders: {
      url: {
        content: '$1',
        example: 'https://developer.mozilla.org',
      },
    },
  },
})

Use t(id) in extension code:

import { t } from '@taisan11/vite-plugin-webext/i18n'

const title = t('appTitle')

At build time, t('appTitle') is statically rewritten to browser.i18n.getMessage('appTitle'), and message id types are derived from src/locale/*.ts. Nested keys such as notificationContent.message are not collected as message ids; only top-level ids like notificationContent are collected.

messaging helpers (sendMessage, sendMessageToTab)

You can define request/response contracts by augmenting WebextMessageMap:

declare global {
  interface WebextMessageMap {
    getProfile: { request: { userId: string }; response: { name: string } }
  }
}

Use typed helpers from @taisan11/vite-plugin-webext/messaging:

import { sendMessage, sendMessageToTab } from '@taisan11/vite-plugin-webext/messaging'

const profile = await sendMessage('getProfile', { userId: '42' })
await sendMessageToTab(1, 'getProfile', { userId: '42' })

At build time, helper calls are statically rewritten to native APIs:

  • sendMessage(type, payload, options?) → browser.runtime.sendMessage({ type, payload }, options?)
  • sendMessageToTab(tabId, type, payload, options?) → browser.tabs.sendMessage(tabId, { type, payload }, options?)

Output layout

Build output is placed under:

  • dist/chrome/
  • dist/firefox/

Zip artifacts

When building with --mode chrome-zip or --mode firefox-zip, the plugin creates:

  • dist/<browser>-<version>-source.zip
  • dist/<browser>-<version>-dist.zip
  • dist/<browser>-zip.zip

Example for chrome mode:

  • dist/chrome-1.2.3-source.zip
  • dist/chrome-1.2.3-dist.zip
  • dist/chrome-zip.zip

Regular --mode chrome / --mode firefox builds do not generate zip files. Set zipArtifacts: false to disable zip generation even in a -zip mode.

If the browser output directory is missing (for example, build.write: false), dist zip generation is skipped with a warning instead of failing the build.