@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 firefoxIf 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.zipdist/<browser>-<version>-dist.zipdist/<browser>-zip.zip
Example for chrome mode:
dist/chrome-1.2.3-source.zipdist/chrome-1.2.3-dist.zipdist/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.
