How CRXJS Handles Multi-Browser Support: Building Chrome and Firefox Extensions with One Config
The CRXJS Vite plugin accommodates multi-browser support through a configurable browser option that conditionally transforms manifest background scripts, web-accessible resources, and browser-specific settings to meet Chrome and Firefox requirements.
The crxjs/chrome-extension-tools repository provides a browser-agnostic architecture for developing browser extensions using Vite. By specifying a single configuration property, developers can target both Chromium-based browsers and Firefox without maintaining separate codebases or manifest files.
The Browser Configuration Option
At the heart of CRXJS multi-browser support lies the browser option defined in packages/vite-plugin/src/node/types.ts (lines 90-97). This option accepts a literal type of 'chrome' | 'firefox' and defaults to 'chrome' when unspecified.
// packages/vite-plugin/src/node/types.ts
export type Browser = 'chrome' | 'firefox'
export interface CrxOptions {
// ... other options
browser?: Browser // defaults to 'chrome'
}
When you initialize the plugin in your vite.config.ts, this value propagates through the entire build pipeline:
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
export default defineConfig({
plugins: [
crx({
manifest: './src/manifest.json',
browser: 'firefox', // Target Gecko engine
}),
],
})
Centralized Option Distribution
The pluginOptionsProvider (packages/vite-plugin/src/node/plugin-optionsProvider.ts, lines 33-48) acts as the distribution hub for the browser setting. During Vite's config hook, this provider injects the CrxOptions object into the plugin API, making opts.browser accessible to every internal sub-plugin.
This centralized approach ensures consistent behavior across the build process. Rather than each plugin parsing configuration independently, they retrieve the target browser from a single source of truth via the options provider.
Background Script Adaptation
One of the most significant differences between Chrome and Firefox lies in background script handling. The plugin-background.ts file (lines 58-71) contains conditional logic that detects the target browser and emits the appropriate manifest structure.
Chrome requires a service worker specified under manifest.background.service_worker:
{
"background": {
"service_worker": "loader.js",
"type": "module"
}
}
Firefox utilizes a background page with a scripts array instead:
{
"background": {
"scripts": ["loader.js"],
"type": "module"
}
}
The plugin automatically generates the correct entry in manifest.json based on the opts.browser value, ensuring compatibility with each browser's extension architecture.
Web-Accessible Resources and Feature Toggles
Chrome and Firefox handle web-accessible resources differently, particularly regarding the use_dynamic_url flag. Chrome requires this flag for runtime URL modifications, while Firefox automatically grants web-accessible URLs and rejects manifests containing Chrome-specific flags.
The plugin-webAccessibleResources.ts module (lines 63-71) manages these discrepancies:
- Strips the
use_dynamic_urlflag whenbrowser === 'firefox' - Merges duplicate resource entries to prevent conflicts
- Removes the entire
web_accessible_resourcesfield for Firefox builds when appropriate
Additionally, the plugin generates default development entries with matches: ['<all_urls>'] to facilitate HMR (Hot Module Replacement) during development, adapting the output based on the target browser's permission model.
Manifest Schema Adaptations
The manifest.ts file (lines 82-88) defines the complete ManifestV3 type structure, including the optional browser_specific_settings.gecko block. When building for Firefox, CRXJS automatically includes this section containing Gecko-specific permissions, IDs, and update URLs.
For Chrome builds, the plugin omits the browser_specific_settings field entirely, producing a clean manifest that complies with the Chrome Web Store's strict schema validation. This conditional emission prevents validation errors that would otherwise cause extension submission rejections.
Practical Configuration Examples
Targeting Chrome (Default Behavior)
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
export default defineConfig({
plugins: [
crx({
manifest: './src/manifest.json',
// browser defaults to 'chrome'
}),
],
})
This configuration generates:
background.service_workerentriesuse_dynamic_urlflags in web-accessible resources- No
browser_specific_settingsblock
Targeting Firefox Explicitly
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
export default defineConfig({
plugins: [
crx({
manifest: './src/manifest.json',
browser: 'firefox',
}),
],
})
This configuration generates:
background.scriptsarrays instead of service workersbrowser_specific_settings.geckoblocks- Stripped Chrome-specific flags from resources
Summary
- Single Configuration Point: The
browseroption inCrxOptionsserves as the single source of truth for target browser selection. - Automatic Manifest Transformation: CRXJS rewrites
manifest.jsonbackground sections, resource flags, and browser-specific settings based on the target. - Background Architecture Handling: The plugin switches between Chrome's service workers and Firefox's background pages automatically.
- Feature-Level Compatibility: Chrome-only flags like
use_dynamic_urlare removed for Firefox builds to prevent validation errors. - Type Safety: The
Browsertype restricts inputs to'chrome'|'firefox', preventing invalid target specifications.
Frequently Asked Questions
Does CRXJS support Safari or Edge extensions?
CRXJS explicitly supports Chrome and Firefox through the browser option. While Microsoft Edge utilizes Chromium and generally accepts Chrome-compatible manifests, Safari requires significant architectural differences and is not currently supported by the browser configuration type.
What happens if I omit the browser option in my Vite config?
If you omit the browser property, the plugin defaults to 'chrome' as defined in packages/vite-plugin/src/node/types.ts (line 38). All Firefox-specific transformations are bypassed, and the build generates a standard Chrome extension manifest with service workers and dynamic URL flags intact.
Can I build for both browsers simultaneously in one Vite command?
The current architecture requires separate build processes for each browser. You should create distinct Vite configuration files (e.g., vite.chrome.config.ts and vite.firefox.config.ts) with their respective browser values, then run builds sequentially or in parallel using separate CLI commands.
Why does Firefox use background scripts instead of service workers?
Firefox's implementation of Manifest V3 maintains support for background pages with persistent scripts rather than adopting Chrome's non-persistent service worker model. CRXJS detects the Firefox target and generates the scripts array format that Gecko requires, while Chrome receives the standard service_worker entry.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →