UNPKG

16.6 kBMarkdownView Raw
1# WebExtension `browser` API Polyfill
2
3This library allows extensions that use the Promise-based WebExtension/BrowserExt API being standardized by the
4[W3 Browser Extensions][w3-browserext] group to run on Google Chrome with minimal or no changes.
5
6[![CircleCI](https://circleci.com/gh/mozilla/webextension-polyfill.svg?style=svg)](https://circleci.com/gh/mozilla/webextension-polyfill)
7[![codecov](https://codecov.io/gh/mozilla/webextension-polyfill/branch/master/graph/badge.svg)](https://codecov.io/gh/mozilla/webextension-polyfill)
8[![devDependency Status](https://david-dm.org/mozilla/webextension-polyfill/dev-status.svg)](https://david-dm.org/mozilla/webextension-polyfill#info=devDependencies)
9[![npm version](https://badge.fury.io/js/webextension-polyfill.svg)](https://badge.fury.io/js/webextension-polyfill)
10
11> This library doesn't (and it is not going to) polyfill API methods or options that are missing on Chrome but natively provided
12> on Firefox, and so the extension has to do its own "runtime feature detection" in those cases (and then eventually polyfill the
13> missing feature on its own or enable/disable some of the features accordingly).
14
15[w3-browserext]: https://www.w3.org/community/browserext/
16
17Table of contents
18=================
19
20* [Supported Browsers](#supported-browsers)
21* [Installation](#installation)
22* [Basic Setup](#basic-setup)
23 * [Basic Setup with ES6 module loader](#basic-setup-with-es6-module-loader)
24 * [Basic Setup with module bundlers](#basic-setup-with-module-bundlers)
25 * [Usage with webpack without bundling](#usage-with-webpack-without-bundling)
26* [Using the Promise-based APIs](#using-the-promise-based-apis)
27* [Examples](#examples)
28* [Usage with TypeScript](#usage-with-typescript)
29* [Known Limitations and Incompatibilities](#known-limitations-and-incompatibilities)
30* [Contributing to this project](#contributing-to-this-project)
31
32Supported Browsers
33==================
34
35| Browser | Support Level |
36| ------------------------- | -------------------------------------------------------------------------------------------------- |
37| Chrome | *Officially Supported* (with automated tests) |
38| Firefox | *Officially Supported as a NO-OP* (with automated tests for comparison with the behaviors on Chrome) |
39| Opera / Edge (>=79.0.309) | *Unofficially Supported* as a Chrome-compatible target (but not explicitly tested in automation) |
40
41The polyfill is being tested explicitly (with automated tests that run on every pull request) on **officially supported**
42browsers (that are currently the last stable versions of Chrome and Firefox).
43
44On Firefox, this library is actually acting as a NO-OP: it detects that the `browser` API object is already defined
45and it does not create any custom wrappers.
46Firefox is still included in the automated tests, to ensure that no wrappers are being created when running on Firefox,
47and for comparison with the behaviors implemented by the library on Chrome.
48
49## Installation
50
51A new version of the library is built from this repository and released as an npm package.
52
53The npm package is named after this repo: [webextension-polyfill](https://www.npmjs.com/package/webextension-polyfill).
54
55For the extension that already include a package.json file, the last released version of this library can be quickly installed using:
56
57```
58npm install --save-dev webextension-polyfill
59```
60
61Inside the `dist/` directory of the npm package, there are both the minified and non-minified builds (and their related source map files):
62
63- node_modules/webextension-polyfill/dist/browser-polyfill.js
64- node_modules/webextension-polyfill/dist/browser-polyfill.min.js
65
66For extensions that do not include a package.json file and/or prefer to download and add the library directly into their own code repository, all the versions released on npm are also available for direct download from unpkg.com:
67
68- https://unpkg.com/webextension-polyfill/dist/
69
70and linked to the Github releases:
71
72- https://github.com/mozilla/webextension-polyfill/releases
73
74## Basic Setup
75
76In order to use the polyfill, it must be loaded into any context where `browser` APIs are accessed. The most common cases
77are background and content scripts, which can be specified in `manifest.json` (make sure to include the `browser-polyfill.js` script before any other scripts that use it):
78
79```javascript
80{
81 // ...
82
83 "background": {
84 "scripts": [
85 "browser-polyfill.js",
86 "background.js"
87 ]
88 },
89
90 "content_scripts": [{
91 // ...
92 "js": [
93 "browser-polyfill.js",
94 "content.js"
95 ]
96 }]
97}
98```
99
100For HTML documents, such as `browserAction` popups, or tab pages, it must be
101included more explicitly:
102
103```html
104<!DOCTYPE html>
105<html>
106 <head>
107 <script type="application/javascript" src="browser-polyfill.js"></script>
108 <script type="application/javascript" src="popup.js"></script>
109 </head>
110 <!-- ... -->
111</html>
112```
113
114And for dynamically-injected content scripts loaded by `tabs.executeScript`,
115it must be injected by a separate `executeScript` call, unless it has
116already been loaded via a `content_scripts` declaration in
117`manifest.json`:
118
119```javascript
120browser.tabs.executeScript({file: "browser-polyfill.js"});
121browser.tabs.executeScript({file: "content.js"}).then(result => {
122 // ...
123});
124```
125
126### Basic Setup with ES6 module loader
127
128The polyfill can also be loaded using the native ES6 module loader available in
129the recent browsers versions.
130
131Be aware that the polyfill module does not export the `browser` API object,
132but defines the `browser` object in the global namespace (i.e. `window`).
133
134```html
135<!DOCTYPE html>
136<html>
137 <head>
138 <script type="module" src="browser-polyfill.js"></script>
139 <script type="module" src="background.js"></script>
140 </head>
141 <!-- ... -->
142</html>
143```
144
145```javascript
146// In background.js (loaded after browser-polyfill.js) the `browser`
147// API object is already defined and provides the promise-based APIs.
148browser.runtime.onMessage.addListener(...);
149```
150
151### Basic Setup with module bundlers
152
153This library is built as a **UMD module** (Universal Module Definition), and so it can also be used with module bundlers (and explicitly tested on both **webpack** and **browserify**) or AMD module loaders.
154
155**src/background.js**:
156```javascript
157var browser = require("webextension-polyfill");
158
159browser.runtime.onMessage.addListener(async (msg, sender) => {
160 console.log("BG page received message", msg, "from", sender);
161 console.log("Stored data", await browser.storage.local.get());
162});
163
164browser.browserAction.onClicked.addListener(() => {
165 browser.tabs.executeScript({file: "content.js"});
166});
167```
168
169**src/content.js**:
170```javascript
171var browser = require("webextension-polyfill");
172
173browser.storage.local.set({
174 [window.location.hostname]: document.title,
175}).then(() => {
176 browser.runtime.sendMessage(`Saved document title for ${window.location.hostname}`);
177});
178```
179
180By using `require("webextension-polyfill")`, the module bundler will use the non-minified version of this library, and the extension is supposed to minify the entire generated bundles as part of its own build steps.
181
182If the extension doesn't minify its own sources, it is still possible to explicitly ask the module bundler to use the minified version of this library, e.g.:
183
184```javascript
185var browser = require("webextension-polyfill/dist/browser-polyfill.min");
186
187...
188```
189
190### Usage with webpack without bundling
191
192The previous section explains how to bundle `webextension-polyfill` in each script. An alternative method is to include a single copy of the library in your extension, and load the library as shown in [Basic Setup](#basic-setup). You will need to install [copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin):
193
194```sh
195npm install --save-dev copy-webpack-plugin
196```
197
198**In `webpack.config.js`,** import the plugin and configure it this way. It will copy the minified file into your _output_ folder, wherever your other webpack files are generated.
199
200```js
201const CopyWebpackPlugin = require('copy-webpack-plugin');
202
203module.exports = {
204 /* Your regular webpack config, probably including something like this:
205 output: {
206 path: path.join(__dirname, 'distribution'),
207 filename: '[name].js'
208 },
209 */
210 plugins: [
211 new CopyWebpackPlugin({
212 patterns: [{
213 from: 'node_modules/webextension-polyfill/dist/browser-polyfill.js',
214 }],
215 })
216 ]
217}
218```
219
220And then include the file in each context, using the `manifest.json` just like in [Basic Setup](#basic-setup).
221
222## Using the Promise-based APIs
223
224The Promise-based APIs in the `browser` namespace work, for the most part,
225very similarly to the callback-based APIs in Chrome's `chrome` namespace.
226The major differences are:
227
228* Rather than receiving a callback argument, every async function returns a
229 `Promise` object, which resolves or rejects when the operation completes.
230
231* Rather than checking the `chrome.runtime.lastError` property from every
232 callback, code which needs to explicitly deal with errors registers a
233 separate Promise rejection handler.
234
235* Rather than receiving a `sendResponse` callback to send a response,
236 `onMessage` listeners simply return a Promise whose resolution value is
237 used as a reply.
238
239* Rather than nesting callbacks when a sequence of operations depend on each
240 other, Promise chaining is generally used instead.
241
242* The resulting Promises can be also used with `async` and `await`, rather
243 than dealt with directly.
244
245## Examples
246
247The following code will retrieve a list of URLs patterns from the `storage`
248API, retrieve a list of tabs which match any of them, reload each of those
249tabs, and notify the user that is has been done:
250
251```javascript
252browser.storage.local.get("urls").then(({urls}) => {
253 return browser.tabs.query({url: urls});
254}).then(tabs => {
255 return Promise.all(
256 Array.from(tabs, tab => browser.tabs.reload(tab.id))
257 );
258}).then(() => {
259 return browser.notifications.create({
260 type: "basic",
261 iconUrl: "icon.png",
262 title: "Tabs reloaded",
263 message: "Your tabs have been reloaded",
264 });
265}).catch(error => {
266 console.error(`An error occurred while reloading tabs: ${error.message}`);
267});
268```
269
270Or, using an async function:
271
272```javascript
273async function reloadTabs() {
274 try {
275 let {urls} = await browser.storage.local.get("urls");
276
277 let tabs = await browser.tabs.query({url: urls});
278
279 await Promise.all(
280 Array.from(tabs, tab => browser.tabs.reload(tab.id))
281 );
282
283 await browser.notifications.create({
284 type: "basic",
285 iconUrl: "icon.png",
286 title: "Tabs reloaded",
287 message: "Your tabs have been reloaded",
288 });
289 } catch (error) {
290 console.error(`An error occurred while reloading tabs: ${error.message}`);
291 }
292}
293```
294
295It's also possible to use Promises effectively using two-way messaging.
296Communication between a background page and a tab content script, for example,
297looks something like this from the background page side:
298
299```javascript
300browser.tabs.sendMessage(tabId, "get-ids").then(results => {
301 processResults(results);
302});
303```
304
305And like this from the content script:
306
307```javascript
308browser.runtime.onMessage.addListener(msg => {
309 if (msg == "get-ids") {
310 return browser.storage.local.get("idPattern").then(({idPattern}) => {
311 return Array.from(document.querySelectorAll(idPattern),
312 elem => elem.textContent);
313 });
314 }
315});
316```
317
318or:
319
320```javascript
321browser.runtime.onMessage.addListener(async function(msg) {
322 if (msg == "get-ids") {
323 let {idPattern} = await browser.storage.local.get("idPattern");
324
325 return Array.from(document.querySelectorAll(idPattern),
326 elem => elem.textContent);
327 }
328});
329```
330
331Or vice versa.
332
333## Usage with TypeScript
334
335There are multiple projects that add TypeScript support to your web-extension project:
336
337| Project | Description |
338| ------------- | ------------- |
339| [@types/webextension-polyfill](https://www.npmjs.com/package/@types/webextension-polyfill) | Types and JS-Doc are automatically generated from the mozilla schema files, so it is always up-to-date with the latest APIs. Formerly known as [webextension-polyfill-ts](https://github.com/Lusito/webextension-polyfill-ts). |
340| [web-ext-types](https://github.com/kelseasy/web-ext-types) | Manually maintained types based on MDN's documentation. No JS-Doc included. |
341| [@types/chrome](https://www.npmjs.com/package/@types/chrome) | Manually maintained types and JS-Doc. Only contains types for chrome extensions though! |
342
343## Known Limitations and Incompatibilities
344
345This library tries to minimize the amount of "special handling" that a cross-browser extension has to do to be able to run on the supported browsers from a single codebase, but there are still cases when polyfillling the missing or incompatible behaviors or features is not possible or out of the scope of this polyfill.
346
347This section aims to keep track of the most common issues that an extension may have.
348
349### No callback supported by the Promise-based APIs on Chrome
350
351While some of the asynchronous API methods in Firefox (the ones that return a promise) also support the callback parameter (mostly as a side effect of the backward compatibility with the callback-based APIs available on Chrome), the Promise-based APIs provided by this library do not support the callback parameter (See ["#102 Cannot call browser.storage.local.get with callback"][I-102]).
352
353### No promise returned on Chrome for some API methods
354
355This library takes its knowledge of the APIs to wrap and their signatures from a metadata JSON file:
356[api-metadata.json](api-metadata.json).
357
358If an API method is not yet included in this "API metadata" file, it will not be recognized.
359Promises are not supported for unrecognized APIs, and callbacks have to be used for them.
360
361Chrome-only APIs have no promise version, because extensions that use such APIs
362would not be compatible with Firefox.
363
364File an issue in this repository for API methods that support callbacks in Chrome *and*
365Firefox but are currently missing from the "API metadata" file.
366
367### Issues that happen only when running on Firefox
368
369When an extension that uses this library doesn't behave as expected on Firefox, it is almost never an issue in this polyfill, but an issue with the native implementation in Firefox.
370
371"Firefox only" issues should be reported upstream on Bugzilla:
372- https://bugzilla.mozilla.org/enter_bug.cgi?product=WebExtensions&component=Untriaged
373
374### API methods or options that are only available when running in Firefox
375
376This library does not provide any polyfill for API methods and options that are only available on Firefox, and they are actually considered out of the scope of this library.
377
378### tabs.executeScript
379
380On Firefox `browser.tabs.executeScript` returns a promise which resolves to the result of the content script code that has been executed, which can be an immediate value or a Promise.
381
382On Chrome, the `browser.tabs.executeScript` API method as polyfilled by this library also returns a promise which resolves to the result of the content script code, but only immediate values are supported.
383If the content script code result is a Promise, the promise returned by `browser.tabs.executeScript` will be resolved to `undefined`.
384
385### MSEdge support
386
387MSEdge versions >= 79.0.309 are unofficially supported as a Chrome-compatible target (as for Opera or other Chrome-based browsers that also support extensions).
388
389MSEdge versions older than 79.0.309 are **unsupported**, for extension developers that still have to work on extensions for older MSEdge versions, the MSEdge `--ms-preload` manifest key and the [Microsoft Edge Extension Toolkit](https://docs.microsoft.com/en-us/microsoft-edge/extensions/guides/porting-chrome-extensions)'s Chrome API bridge can be used to be able to load the webextension-polyfill without any MSEdge specific changes.
390
391The following Github repository provides some additional detail about this strategy and a minimal test extension that shows how to put it together:
392
393- https://github.com/rpl/example-msedge-extension-with-webextension-polyfill
394
395## Contributing to this project
396
397Read the [contributing section](CONTRIBUTING.md) for additional information about how to build the library from this repository and how to contribute and test changes.
398
399[PR-114]: https://github.com/mozilla/webextension-polyfill/pull/114
400[I-102]: https://github.com/mozilla/webextension-polyfill/issues/102#issuecomment-379365343