webextension-polyfill

A lightweight polyfill that gives Chrome extensions Firefox's Promise-based browser API namespace.

Library
npm
v0.12.0
3,069stars
Mozilla Public License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
74/100Good
Development Activity68
Maintenance64
Community64
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
76/100Good
Architecture82
Code Quality80
Innovation68
Learning Curve75

webextension-polyfill is Mozilla’s small runtime library that lets browser-extension developers write one codebase against the Promise-based browser.* namespace instead of juggling Chrome’s callback-based chrome.* APIs. It wraps every documented WebExtension API method in a Proxy that either forwards to the native browser object (a no-op on Firefox and Safari, which already expose it) or synthesizes Promise-returning wrappers around Chrome’s callback APIs using a metadata table of argument counts and callback shapes.

Because it ships as pure runtime code with no build step required from consumers, it drops into a manifest.json background/content script list, an ES module import, or a bundler require() call unchanged. The library has shipped since 2016 and was the de facto standard for cross-browser extension code until Chrome itself added native browser namespace support in 2026, at which point Mozilla marked the project feature-complete and moved it into maintenance mode.

For teams maintaining extensions that still need to support older Chrome releases, Edge, or Safari alongside Firefox, it remains the reference implementation for translating callback-shaped APIs into async/await-friendly code without hand-writing promisify wrappers for dozens of namespaces.

What You Get

  • A single browser-polyfill.js UMD bundle (plus minified build) that defines the global browser object when loaded in a background page, content script, or extension HTML page
  • Promise-based wrappers, generated from api-metadata.json, for dozens of WebExtension namespaces (tabs, storage, runtime, bookmarks, notifications, alarms, and more) so every async call can be awaited instead of passed a callback
  • Automatic no-op passthrough on browsers (Firefox, Safari) that already implement the native browser namespace, so the same script works everywhere without feature-detection code in the extension itself
  • Promise-aware wrapping of runtime.onMessage and webRequest.onRequestFinished listeners, so message handlers can simply return a value or a Promise instead of calling a sendResponse callback
  • Both CommonJS (require("webextension-polyfill")) and native ES module entry points, so it fits bundler-based extension builds as well as raw <script>/manifest.json loading

Common Use Cases

  • Cross-browser extension codebases - writing background and content scripts once against browser.* and shipping the same source to the Chrome Web Store and Firefox Add-ons without per-browser branches
  • Promise/async-await extension code - replacing nested chrome.* callbacks with await browser.storage.local.get()-style calls throughout an extension’s business logic
  • Two-way messaging between background and content scripts - implementing runtime.onMessage handlers that simply return a value or a Promise as the reply, instead of manually calling sendResponse
  • Migrating a legacy Chrome-only extension to also support Firefox/Edge - dropping the polyfill script in front of existing chrome.* call sites as an incremental compatibility layer
  • Bundler-based extension builds - importing the library via webpack/browserify so browser is available inside modules without a separate script tag, using the module bundler examples in the README

Under The Hood

Architecture The library is a single file, src/browser-polyfill.js, structured around one wrapping engine: wrapObject recursively walks the native chrome (or browser) object tree and returns a Proxy that intercepts property access, replacing any method with either a caller-supplied special-case wrapper or an auto-generated Promise wrapper built by wrapAsyncFunction. The wrapping decision for every method comes from api-metadata.json, a build-time-included JSON tree keyed by namespace and method name that records minArgs/maxArgs and callback-shape flags (singleCallbackArg, noCallback, fallbackToNoCallback) rather than hand-written per-API code, so adding support for a new WebExtension method is a metadata edit, not new wrapper logic. Two additional wrapping paths, wrapEvent and dedicated onMessage/onRequestFinished wrappers, handle the listener-based (rather than call-and-callback) parts of the API surface, using a DefaultWeakMap to keep a stable identity between an application’s original listener function and its generated wrapper so removeListener still works. The whole engine is a closure entered only if browser isn’t already defined, making the cost close to zero on browsers that need no polyfilling.

Tech Stack Runtime code is dependency-free vanilla JavaScript compiled to a UMD bundle via a Grunt pipeline (Gruntfile.js): grunt-replace inlines api-metadata.json into the source at the {/* include(...) */} marker and stamps package name/version/timestamp, a custom Babel plugin (scripts/babel-transform-to-umd-module) wraps the result as a UMD module exposing both a browser global and an AMD/CommonJS export, and grunt-terser produces the minified browser-polyfill.min.js with an appended MPL license banner via grunt-contrib-concat. Tests run on Mocha with Chai assertions and jsdom-based DOM/browser mocking for unit coverage, plus a separate Selenium WebDriver (chromedriver/geckodriver) integration suite that loads real extensions into live Chrome and Firefox instances via tape. Linting is ESLint with a flat config (eslint.config.mjs).

Code Quality The project has a substantial and deliberately layered test suite: test/test-async-functions.js, test/test-proxied-properties.js, and test/test-runtime-onMessage.js unit-test the wrapping engine’s Promise/callback translation and Proxy property-forwarding behavior in isolation, test/test-onRequestFinished.js and test/test-browser-global.js cover the event-wrapper and no-op-on-Firefox paths specifically, and test/integration/ runs the built bundle inside real browser instances rather than only mocks. Source is thoroughly JSDoc-commented (parameter types, return shapes, and behavior notes on nearly every exported function), uses strict-mode JS with const/arrow functions consistently, and is enforced by ESLint plus a CI pipeline (CircleCI) that runs lint, unit tests, and the module-bundler smoke tests on every change. There is no TypeScript in this repo itself; typing is delegated to the separately maintained @types/webextension-polyfill package referenced in the README.

What Makes It Unique Rather than hand-writing a Promise wrapper per WebExtension API method (the naive approach most ad-hoc “promisify chrome” snippets take), the library generates all of them from a single declarative metadata file plus one generic wrapAsyncFunction/wrapObject engine, which is what let it stay correct across dozens of namespaces and hundreds of methods for a decade with comparatively little maintenance. Its wrapObject Proxy also transparently falls through to the native object for any property it has no metadata for, so unknown or newly added Chrome APIs remain usable (just without Promise sugar) instead of breaking. That no-op detection is itself a deliberate design choice: on Firefox and Safari, which already expose a native browser object, the entire wrapping engine is skipped, so the same script has effectively zero runtime cost on the browsers that don’t need it — the project explicitly designed for its own eventual obsolescence as browsers converged on the API it polyfills.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search