feat: migrate firefox to manifest v3

This commit is contained in:
Abhijith Vijayan [FLUXON] 2026-01-03 21:40:04 +05:30
parent 9ef7f6ce9a
commit e14efb7c4b
4 changed files with 242 additions and 54 deletions

View File

@ -47,9 +47,15 @@
## Browser Support
This starter uses **Manifest V3** for all browsers.
| [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png)](/) | [![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png)](/) | [![Opera](https://raw.github.com/alrra/browser-logos/master/src/opera/opera_48x48.png)](/) | [![Edge](https://raw.github.com/alrra/browser-logos/master/src/edge/edge_48x48.png)](/) | [![Brave](https://raw.github.com/alrra/browser-logos/master/src/brave/brave_48x48.png)](/) |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| 88 & later | 109 & later | 36 & later | 88 & later | Latest |
| 88+ (Jan 2021) | 109+ (Jan 2023) | 74+ (Chromium-based) | 88+ (Chromium-based) | Latest (Chromium-based) |
> **Note**: Firefox 109+ is required for Manifest V3 support with ES modules in background scripts.
>
> Need to support older Firefox versions? See [Firefox MV2 Guide](docs/FIREFOX_MV2.md) for using Manifest V2 with Firefox.
## Used by extensions in production that has over 100,000+ users.

205
docs/FIREFOX_MV2.md Normal file
View File

@ -0,0 +1,205 @@
# Using Manifest V2 for Firefox
By default, this starter uses **Manifest V3** for all browsers. However, if you need to support older Firefox versions (< 109) or prefer MV2 for Firefox, follow this guide.
## Why Use MV2 for Firefox?
- **Older Firefox support**: Firefox 109+ is required for MV3
- **Extended support**: Mozilla has not announced a deprecation date for MV2
- **API differences**: Some APIs work differently between MV2 and MV3
## Required Changes
### 1. Update `source/manifest.json`
Replace the unified manifest with browser-specific versions:
```json
{
"__chrome__manifest_version": 3,
"__firefox__manifest_version": 2,
"name": "Sample WebExtension",
"version": "0.0.0",
"icons": {
"16": "assets/icons/favicon-16.png",
"32": "assets/icons/favicon-32.png",
"48": "assets/icons/favicon-48.png",
"128": "assets/icons/favicon-128.png"
},
"description": "Sample description",
"homepage_url": "https://github.com/abhijithvijayan/web-extension-starter",
"short_name": "Sample Name",
"__chrome__permissions": [
"activeTab",
"storage"
],
"__chrome__optional_permissions": [],
"__chrome__host_permissions": [],
"__chrome__optional_host_permissions": [
"http://*/*",
"https://*/*"
],
"__firefox__permissions": [
"activeTab",
"storage",
"http://*/*",
"https://*/*"
],
"__chrome__content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self';"
},
"__firefox__content_security_policy": "script-src 'self'; object-src 'self'",
"__chrome|firefox__author": "abhijithvijayan",
"__firefox__applications": {
"gecko": {
"id": "{754FB1AD-CC3B-4856-B6A0-7786F8CA9D17}"
}
},
"__chrome__minimum_chrome_version": "88",
"__chrome__action": {
"default_popup": "Popup/popup.html",
"default_icon": {
"16": "assets/icons/favicon-16.png",
"32": "assets/icons/favicon-32.png",
"48": "assets/icons/favicon-48.png",
"128": "assets/icons/favicon-128.png"
},
"default_title": "tiny title"
},
"__firefox__browser_action": {
"default_popup": "Popup/popup.html",
"default_icon": {
"16": "assets/icons/favicon-16.png",
"32": "assets/icons/favicon-32.png",
"48": "assets/icons/favicon-48.png",
"128": "assets/icons/favicon-128.png"
},
"default_title": "tiny title",
"browser_style": false
},
"__chrome__options_page": "Options/options.html",
"options_ui": {
"page": "Options/options.html",
"open_in_tab": true
},
"background": {
"__chrome__service_worker": "assets/js/background.bundle.js",
"__chrome__type": "module",
"__firefox__scripts": [
"assets/js/background.bundle.js"
]
},
"content_scripts": [
{
"run_at": "document_start",
"matches": [
"http://*/*",
"https://*/*"
],
"css": [],
"js": [
"assets/js/contentScript.bundle.js"
]
}
],
"__firefox__web_accessible_resources": [
"assets/*"
],
"__chrome__web_accessible_resources": [
{
"resources": ["assets/*"],
"matches": ["http://*/*", "https://*/*"]
}
]
}
```
### 2. Update `vite.config.ts`
Firefox MV2 background scripts don't support ES modules. Update the `buildIIFEScripts` plugin to also build the background script as IIFE for Firefox:
```typescript
// Build scripts as IIFE (no ES module imports)
// Content scripts can't use ES modules when injected via manifest
buildIIFEScripts({
scripts: [
{
name: 'contentScript',
entry: path.resolve(sourcePath, 'ContentScript/index.ts'),
},
// Firefox MV2 background scripts don't support ES modules
...(targetBrowser === 'firefox'
? [
{
name: 'background',
entry: path.resolve(sourcePath, 'Background/index.ts'),
},
]
: []),
],
outDir: getOutDir(),
isDevelopment,
}),
```
Also update the rollup input to exclude background for Firefox (since it's built as IIFE):
```typescript
rollupOptions: {
input: {
popup: path.resolve(sourcePath, 'Popup/popup.html'),
options: path.resolve(sourcePath, 'Options/options.html'),
// Background script: Chrome MV3 uses ES modules (service worker)
// Firefox MV2 is built separately as IIFE via buildIIFEScripts plugin
...(targetBrowser !== 'firefox'
? { background: path.resolve(sourcePath, 'Background/index.ts') }
: {}),
},
// ... rest of config
}
```
## Key Differences: MV2 vs MV3
| Feature | MV2 (Firefox) | MV3 (Chrome/Firefox) |
|---------|---------------|----------------------|
| Background | `background.scripts` | `background.service_worker` (Chrome) / `background.scripts` with `type: module` (Firefox) |
| Action | `browser_action` | `action` |
| Host permissions | In `permissions` array | Separate `host_permissions` array |
| Web accessible resources | String array | Object array with `resources` and `matches` |
| CSP | String | Object with `extension_pages` |
| ES modules in background | Not supported | Supported |
## Browser Support with MV2
When using MV2 for Firefox:
| Browser | Version | Manifest |
|---------|---------|----------|
| Chrome | 88+ | MV3 |
| Firefox | 48+ | MV2 |
| Edge | 88+ | MV3 |
| Opera | 74+ | MV3 |
## Notes
- The `webextension-polyfill` library handles API differences between browsers
- Content scripts work the same way in both MV2 and MV3 (IIFE format)
- Test thoroughly on both browsers when using mixed manifest versions

View File

@ -1,6 +1,5 @@
{
"__chrome__manifest_version": 3,
"__firefox__manifest_version": 2,
"manifest_version": 3,
"name": "Sample WebExtension",
"version": "0.0.0",
@ -14,12 +13,12 @@
"homepage_url": "https://github.com/abhijithvijayan/web-extension-starter",
"short_name": "Sample Name",
"__chrome__permissions": [
"permissions": [
"activeTab",
"storage"
],
"__chrome__optional_permissions": [],
"optional_permissions": [],
"__chrome__host_permissions": [],
@ -28,29 +27,27 @@
"https://*/*"
],
"__firefox__permissions": [
"activeTab",
"storage",
"__firefox__optional_host_permissions": [
"http://*/*",
"https://*/*"
],
"__chrome__content_security_policy": {
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self';"
},
"__firefox__content_security_policy": "script-src 'self'; object-src 'self'",
"__chrome|firefox__author": "abhijithvijayan",
"__firefox__applications": {
"__firefox__browser_specific_settings": {
"gecko": {
"id": "{754FB1AD-CC3B-4856-B6A0-7786F8CA9D17}"
"id": "{754FB1AD-CC3B-4856-B6A0-7786F8CA9D17}",
"strict_min_version": "109.0"
}
},
"__chrome__minimum_chrome_version": "88",
"__chrome__action": {
"action": {
"default_popup": "Popup/popup.html",
"default_icon": {
"16": "assets/icons/favicon-16.png",
@ -61,18 +58,6 @@
"default_title": "tiny title"
},
"__firefox__browser_action": {
"default_popup": "Popup/popup.html",
"default_icon": {
"16": "assets/icons/favicon-16.png",
"32": "assets/icons/favicon-32.png",
"48": "assets/icons/favicon-48.png",
"128": "assets/icons/favicon-128.png"
},
"default_title": "tiny title",
"browser_style": false
},
"__chrome__options_page": "Options/options.html",
"options_ui": {
"page": "Options/options.html",
@ -82,9 +67,8 @@
"background": {
"__chrome__service_worker": "assets/js/background.bundle.js",
"__chrome__type": "module",
"__firefox__scripts": [
"assets/js/background.bundle.js"
]
"__firefox__scripts": ["assets/js/background.bundle.js"],
"__firefox__type": "module"
},
"content_scripts": [
@ -101,14 +85,10 @@
}
],
"__firefox__web_accessible_resources": [
"assets/*"
],
"__chrome__web_accessible_resources": [
"web_accessible_resources": [
{
"resources": [ "assets/*" ],
"matches": [ "http://*/*", "https://*/*" ]
"resources": ["assets/*"],
"matches": ["http://*/*", "https://*/*"]
}
]
}

View File

@ -9,14 +9,15 @@ import WextManifest from 'vite-plugin-wext-manifest';
import type {Plugin} from 'vite';
// Custom plugin to build content scripts as IIFE (self-contained, no ES module imports)
function buildContentScripts(options: {
// Custom plugin to build scripts as IIFE (self-contained, no ES module imports)
// Used for scripts that can't use ES modules (e.g., content scripts injected via manifest)
function buildIIFEScripts(options: {
scripts: {name: string; entry: string}[];
outDir: string;
isDevelopment: boolean;
}): Plugin {
return {
name: 'build-content-scripts',
name: 'build-iife-scripts',
async writeBundle() {
for (const script of options.scripts) {
await build({
@ -94,7 +95,7 @@ export default defineConfig(({ mode }) => {
// delete previous built compressed file
clean({
targetFiles: [path.resolve(destPath, getExtensionZipFileName())],
}),
}) as Plugin,
// Run typescript checker in worker thread
checker({
@ -109,9 +110,9 @@ export default defineConfig(({ mode }) => {
usePackageJSONVersion: true,
}),
// Build content scripts as IIFE (no ES module imports)
// Content scripts can't use ES modules in manifest-injected scripts
buildContentScripts({
// Build scripts as IIFE (no ES module imports)
// Content scripts can't use ES modules when injected via manifest
buildIIFEScripts({
scripts: [
{
name: 'contentScript',
@ -146,9 +147,10 @@ export default defineConfig(({ mode }) => {
// Vite will find the <script> tag inside and bundle it.
popup: path.resolve(sourcePath, 'Popup/popup.html'),
options: path.resolve(sourcePath, 'Options/options.html'),
// Background script (service worker in MV3)
// Background script (service worker in Chrome, background script in Firefox)
// Both MV3 implementations support ES modules
background: path.resolve(sourcePath, 'Background/index.ts'),
// Note: contentScript is built separately as IIFE via buildContentScripts plugin
// Note: contentScript is built separately as IIFE via buildIIFEScripts plugin
},
output: {
@ -163,16 +165,11 @@ export default defineConfig(({ mode }) => {
},
},
terserOptions: {
mangle: true,
compress: {
drop_console: true,
drop_debugger: true,
},
format: {
comments: false,
},
},
},
// esbuild options - drop console/debugger in production
esbuild: mode === 'production' ? {
drop: ['console', 'debugger'],
} : {},
};
});