Skip to main content
See which browsers Extension.js supports and validate your extension across Chrome, Edge, Firefox, and custom binaries from a single CLI workflow within the same project.

Choose the right target

How it works

Use --browser to choose a target in dev, start, preview, and build. If you do not specify a browser, the CLI defaults to chromium. --browser accepts exactly these values, alone or comma-separated:
--browser=all is also accepted and expands to chrome, edge, firefox.
Prefer chromium (or Chrome for Testing via npx extension install chrome) over branded Chrome for development. Recent branded Chrome builds (150+) drop the --load-extension switch unless a policy disables that behavior. A dropped switch looks exactly like a healthy launch. When Extension.js cannot confirm the load, it warns and points you at chrome://extensions.
safari (and its webkit-based alias) is the exception: it is a macOS-only target supported by build and dev, not by preview or start. A Safari dev session streams logs and reloads on save. See Building Safari extensions. chromium-emulator (from 4.1.38) is a dev target too: dev opens an emulated Chromium that runs your extension inside a web page, and build writes dist/chromium-emulator. start and preview refuse it with E_UNSUPPORTED_BROWSER, because the page reads your build from the dev server. See Emulated Chromium.

Requested target vs. launch binary

The browser you request determines the artifact. The binary is just the runtime. When you run extension dev --browser=chromium, Extension.js always:
  • Writes output to dist/chromium (the folder is named after the requested target, never after the binary that launches).
  • Resolves browser-specific manifest fields for the requested target.
If the requested browser is not installed, Extension.js does not change the target. For chrome and chromium it looks for another managed Chromium-family binary (one previously downloaded by npx extension install) and uses it as the runtime instead:
  • Requested chromium missing: falls back to managed Chrome, then managed Edge.
  • Requested chrome missing: falls back to managed Chromium, then managed Edge.
When this happens, the CLI prints a warn naming the missing browser and the fix, for example npx extension install chromium. The warn deliberately omits the substitute binary path. Read that path from binary in ready.json. The output folder and the emitted manifest are exactly what they would be without the fallback.

What --browser=edge runs

edge never swaps to another browser, and it has no pinned managed copy. npx extension install edge installs Microsoft Edge on your system through Playwright. Extension.js resolves the Edge binary in this order:
  1. The path in --chromium-binary, when passed.
  2. The path in the EDGE_BINARY environment variable, when set.
  3. The Edge that is installed on your system.
The identity card then reads Edge <version> (system install). When nothing resolves, the command prints install guidance (npx extension install edge) and exits with code 1. There is no silent Chromium substitute for a missing Edge. If a Chromium window still surprises you, read binary in ready.json. It names the exact binary that launched, and binaryProvenance tells you why. A requested chrome or chromium can borrow a managed family binary (see above), but a requested edge cannot. The output contract holds either way: dist/edge exists only when edge was the requested target.

How the binary is chosen

From the release after 4.1.39, dev, start, and preview resolve the binary in one order for every browser:
  1. The flag, --chromium-binary or --gecko-binary.
  2. The environment variable of the browser.
  3. The managed binary from npx extension install <browser>.
  4. The system install.
A variable is a choice, as the flag is. A variable that names a missing path, or a file that cannot run, fails the launch with E_BROWSER_BINARY_INVALID. The run never skips it for the next step. Requested chrome still refuses a branded system Chrome. A variable that names branded Chrome is not taken, and the run continues with the managed binary. For Firefox, the managed step prefers a stable build. A nightly that sits in the same cache runs only when no stable build is there, and the card then says Firefox Nightly.

Binary provenance labels

ready.json records where the session’s binary came from in binaryProvenance: The identity card has no Binary row. Its Browser row carries a note for the two cases that you can act on:
(pinned with <flag or variable>) names what chose the binary. (system install) prints for chrome, chromium, edge, and firefox when the binary came from your system. A managed binary and a named fork get no note.

Chromium snapshots vs stable

The managed chromium install is a tip-of-tree snapshot, not a stable release. When a stable system Chromium exists, Extension.js swaps to it automatically and prints a warn with the opt-out. Set EXTENSION_PREFER_CHROMIUM_SNAPSHOT=true to keep the cached snapshot instead. Within the Chromium family this substitution is safe: dist/chrome and dist/chromium are byte-identical because manifest prefixes resolve per engine family, not per vendor. See Browser-specific manifest fields for the prefix rules. To pre-install managed binaries for reproducible runs, use npx extension install <browser> or npx extension install all (which covers chromium too).

Supported browsers

Named browser targets: Named forks (auto-located from your system, no binary path required): If a named fork is not installed, Extension.js exits with install guidance. See Running other browsers. Engine-based targets (custom binary required): Extension.js treats firefox-based as a Gecko engine target internally. Mobile browsers such as Samsung Internet, Chrome for Android, and Firefox for Android are not launch targets. Build the Firefox output and follow that browser’s own sideloading flow.

What engine targets are for

chromium-based and gecko-based target an engine family instead of one vendor. In dev, start, and preview, they run a binary that has no named target. Think a nightly fork, an in-house build, or a fork that the built-in locators do not know. In build, they exist for distribution, and no binary is involved at all:
  • extension build --browser=chromium-based writes a family-generic artifact to dist/chromium-based.
  • That artifact resolves chromium: manifest prefixes, reads .env.chromium-based, and sets EXTENSION_BROWSER=chromium-based.
  • Ship that one package to users on Chrome, Brave, Edge, or any other Chromium fork.
Choose a named target (chrome, edge) when a store build needs vendor-specific manifest fields. Choose an engine target when one artifact should serve the whole family. See What engine targets mean for build.

Safari and other WebKit targets

In addition to the Chromium family and Firefox (Gecko engine), Extension.js can build your extension into a Safari app on macOS. Safari is a dev and build target: build and dev are supported, but preview and start are not. dev converts your extension, signs an app, opens it, streams logs, and reloads on save, after you enable the extension once in Safari Settings. It requires macOS with the full Xcode app. See Building Safari extensions for the full workflow, requirements, and how to enable the extension in Safari.

Multi-browser selection

You can run multiple named browsers in one command:
Use comma-separated values to run multiple named targets in sequence (for example, --browser=chrome,edge,firefox).

Constraints and behavior

  • chromium-based requires --chromium-binary in commands that launch a browser (dev, start, preview); build needs no binary.
  • gecko-based / firefox-based require --gecko-binary under the same conditions.
  • Engine-based targets route to the same Chromium/Firefox runners with engine-aware behavior.
  • As build targets, engine targets get their own dist/<target> folder, .env.<target> resolution, EXTENSION_BROWSER value, and manifest prefix. See What engine targets mean for build.

Firefox builds warn about Chromium-only APIs

From 4.1.18, a production build for a Gecko target scans the emitted bundle for two calls that addons-linter reports as UNSUPPORTED_API: chrome.sidePanel.* on any manifest version, and chrome.action.* on Manifest V2. Each hit prints a warning that names the file. The build still succeeds.
The fix is a branch that the bundler resolves at build time, so the Firefox bundle drops the call entirely:
On 4.1.39 and earlier, a runtime guard such as chrome.sidePanel?.setPanelBehavior() still warns. From the release after 4.1.39, a call that Firefox never reaches does not warn. That covers a call behind if (chrome.sidePanel), a call written as chrome.sidePanel?.setPanelBehavior(), and a call behind a typeof chrome.sidePanel !== "undefined" check. The Safari warning reads the same guards, see Safari. A runtime guard quiets the build warning only. addons-linter matches the static read of the namespace, not what runs. Only a build-time branch on import.meta.env.EXTENSION_PUBLIC_BROWSER drops the call from the Firefox bundle, and that is what clears the UNSUPPORTED_API finding in the store check. See Environment variables for the EXTENSION_PUBLIC_BROWSER value on each target.

Best practices

  • Use named browsers for daily iteration: chrome, edge, and firefox are the fastest path for regular testing.
  • Use engine-based mode intentionally: Prefer chromium-based / gecko-based when validating custom binaries or shipping a family-generic build.
  • Keep profiles isolated per browser: Reduce cross-browser state leakage while debugging.
  • Pair with browser-specific fields: Use browser-prefixed manifest keys for true behavior differences.

Next steps