How the CRXJS Browser Option Configuration Impacts Firefox vs. Chrome Extension Builds

The browser option in the CRXJS Vite plugin defaults to "chrome" but can be set to "firefox" to automatically transform service workers into background scripts and strip Chrome-specific flags like use_dynamic_url from the generated manifest.

The browser option configuration is a critical setting in crxjs/chrome-extension-tools that determines how your extension manifest and background scripts are generated during the build process. By passing this option to defineManifest, you instruct the Vite plugin to emit browser-specific assets that comply with either Chrome's Manifest V3 service worker model or Firefox's background page requirements. This single configuration value triggers a cascade of transformations across multiple internal plugins to ensure cross-browser compatibility from a single source codebase.

What the Browser Option Controls

Setting the browser configuration value to "firefox" or "chrome" (the default) directly impacts four major areas of the build output:

Background Script Architecture

  • Chrome (browser: "chrome"): Generates a service worker using manifest.background.service_worker with optional ES module support. The plugin emits a loader script that imports the worker file directly.
  • Firefox (browser: "firefox"): Generates a background page using manifest.background.scripts because Firefox does not support service workers for extensions. The plugin switches the loader to import a standard background page script instead.

Web-Accessible Resources Handling

The web_accessible_resources array is constructed differently based on the target browser:

  • Chrome: Adds the use_dynamic_url flag to resources, allowing the extension origin to change on each reload for security purposes.
  • Firefox: Strips use_dynamic_url from every entry because Firefox forbids this property and treats resources as globally accessible by default.

Manifest Type Definitions

The plugin uses distinct TypeScript interfaces defined in packages/vite-plugin/src/node/manifest.ts (lines 19‑28):

  • ChromeManifestBackground for Chrome builds
  • FirefoxManifestBackground for Firefox builds

Loader File Generation

During development and production builds, the plugin emits different loader scripts in packages/vite-plugin/src/node/plugin-background.ts (lines 62‑88). For Chrome, it imports the service worker environment; for Firefox, it imports the background page environment with HMR support.

How Browser Detection Works Under the Hood

The browser option configuration is processed during three distinct phases of the Vite lifecycle:

1. Configuration Resolution (config Hook)

Both the background and web-accessible resources plugins read the option during the config hook using opts.browser || 'chrome' as seen in packages/vite-plugin/src/node/plugin-webAccessibleResources.ts (lines 36‑39) and packages/vite-plugin/src/node/plugin-background.ts (lines 48‑52). If you omit the option, it safely defaults to Chrome.

2. Development Mode (apply: 'serve')

In serve mode, the plugins emit temporary loader scripts that inject environment variables and HMR clients. The background plugin checks the browser value to determine whether to import a service worker or a background page script (packages/vite-plugin/src/node/plugin-background.ts, lines 62‑88). Meanwhile, the web-accessible resources plugin adds a generic <all_urls> entry but conditionally deletes use_dynamic_url for Firefox targets.

3. Production Build (apply: 'build')

During the build phase, the web-accessible resources plugin analyzes the Vite file manifest, deduplicates assets, and iterates over the resource list to strip use_dynamic_url when targeting Firefox (packages/vite-plugin/src/node/plugin-webAccessibleResources.ts, lines 25‑30 and 63‑66). The final manifest is returned from renderCrxManifest with all Firefox-specific normalizations applied.

Code Examples

Configuring the Browser Option

Define your target browser in the manifest configuration file:

// manifest.config.ts
import { defineManifest } from '@crxjs/vite-plugin'

export default defineManifest({
  manifest_version: 3,
  name: 'My Extension',
  version: '1.0.0',
  // Target Firefox instead of Chrome
  browser: 'firefox',
  background: {
    scripts: ['src/background.ts'],
  },
})

When browser: 'firefox' is present, the generated manifest.json will contain background.scripts instead of background.service_worker, and web_accessible_resources entries will omit the use_dynamic_url property.

Generated Output for Chrome

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "web_accessible_resources": [
    {
      "matches": ["<all_urls>"],
      "resources": ["**/*", "*"],
      "use_dynamic_url": false
    }
  ]
}

Generated Output for Firefox

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "background": {
    "scripts": ["background.js"]
  },
  "web_accessible_resources": [
    {
      "matches": ["<all_urls>"],
      "resources": ["**/*", "*"]
    }
  ],
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com"
    }
  }
}

Note the absence of use_dynamic_url and the presence of background.scripts in the Firefox output.

Key Source Files and Implementation Details

The browser option configuration logic is distributed across these critical files in the crxjs/chrome-extension-tools repository:

Summary

  • The browser option configuration defaults to "chrome" but accepts "firefox" to trigger cross-browser compatibility transformations.
  • Firefox builds use background.scripts instead of service workers and strip use_dynamic_url from web-accessible resources.
  • The option is read during the Vite config hook in plugin-background.ts and plugin-webAccessibleResources.ts, with logic applied during both serve and build phases.
  • Type definitions in manifest.ts enforce distinct manifest schemas for each browser target.
  • A single source configuration can output valid Manifest V3 extensions for both Chrome and Firefox by changing one configuration value.

Frequently Asked Questions

How do I switch from Chrome to Firefox in my CRXJS project?

Set browser: 'firefox' in your defineManifest configuration. According to the source code in packages/vite-plugin/src/node/types.ts, this optional field accepts either "chrome" (default) or "firefox". The plugin automatically transforms service workers into background scripts and sanitizes the manifest for Firefox compatibility.

Why does my Firefox build fail with "use_dynamic_url is not allowed"?

Firefox forbids the use_dynamic_url property in web_accessible_resources. When you set browser: 'firefox', the plugin in packages/vite-plugin/src/node/plugin-webAccessibleResources.ts (lines 63‑66) automatically strips this flag during the build. If you see this error, verify that you have explicitly set the browser option to "firefox" in your manifest config.

Can I build for both browsers simultaneously from the same source?

Not in a single build command. The browser option configuration is a build-time directive that generates a single target-specific manifest. To support both browsers, maintain separate Vite config files or use environment variables to switch the browser value between build runs, allowing you to output distinct Chrome and Firefox packages from the same codebase.

Does the browser option affect the HMR (Hot Module Replacement) behavior?

Yes. During development mode (apply: 'serve'), the background plugin in packages/vite-plugin/src/node/plugin-background.ts (lines 62‑88) emits different loader scripts based on the browser value. Chrome receives a service worker loader with HMR support, while Firefox receives a background page loader that handles HMR for standard scripts rather than service workers.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →