A lightweight polyfill library for Promise-based WebExtension APIs in Chrome
Go to file
Rob Wu 7ff6e8a1dc chore: Stop swallowing errors in .sh tests
The Bash shell scripts ignored errors in all but the last line.
This commit adds `set -eo pipefail` to fix the issue:

`set -e` causes the script to exit as soon as any command exits with a
non-zero exit code. When pipes are used, this flag only applies to the
last command in a pipe. The `set -o pipefail` option ensures that the
script is also exited with a non-zero exit code when any command in the
pipe fails.
2018-06-20 15:29:03 +02:00
src feat: Reject sendMessage returned promise when a onMessage listener returns a rejected promise. 2018-06-02 20:59:46 +02:00
test chore: Stop swallowing errors in .sh tests 2018-06-20 15:29:03 +02:00
.eslintrc test: Run a minimal set of tests from a test extension running on Chrome (#66) 2017-10-23 19:42:01 +02:00
.gitattributes chore: Added .gitattributes to force eol=lf to fix #101 (#103) 2018-04-09 15:44:30 +02:00
.gitignore test: introduced a test suite for unit testing. 2016-11-07 16:33:42 +01:00
.travis.yml chore: Cleaned up travis config (#129) 2018-06-19 23:29:35 +02:00
Gruntfile.js chore: Always use LF for dist files (#67) 2018-05-31 16:19:46 +02:00
LICENSE Initial commit 2016-10-06 12:02:51 -07:00
README.md chore(README): Drop reference to babel for async/await (#50) 2018-04-09 19:24:15 +02:00
api-metadata.json fix: add tabs.discard 2018-06-15 10:49:53 -07:00
package.json test(browsers-smoketests): Run a set of smoke tests on both Chrome and Firefox 2018-06-02 20:59:46 +02:00

README.md

WebExtension browser API Polyfill Build Status

This library allows extensions written for the Promise-based WebExtension/BrowserExt API being standardized by the W3 Browser Extensions group to be used without modification in Google Chrome.

Table of contents

Building

To build, assuming you're already installed node >= 6 and npm, simply run:

npm install
npm run build
npm run test

This will install all the npm dependencies and build both non-minified and minified versions of the final library, and output them to dist/browser-polyfill.js and dist/browser-polyfill.min.js, respectively, and finally executes the unit tests on the generated dist files.

Basic Setup

In order to use the polyfill, it must be loaded into any context where browser APIs are accessed. The most common cases are background and content scripts, which can be specified in manifest.json:

{
  // ...

  "background": {
    "scripts": [
      "browser-polyfill.js",
      "background.js"
    ]
  },

  "content_scripts": [{
    // ...
    "js": [
      "browser-polyfill.js",
      "content.js"
    ]
  }]
}

For HTML documents, such as browserAction popups, or tab pages, it must be included more explicitly:

<!DOCTYPE html>
<html>
  <head>
    <script type="application/javascript" src="browser-polyfill.js"></script>
    <script type="application/javascript" src="popup.js"></script>
  </head>
  <!-- ... -->
</html>

And for dynamically-injected content scripts loaded by tabs.executeScript, it must be injected by a separate executeScript call, unless it has already been loaded via a content_scripts declaration in manifest.json:

browser.tabs.executeScript({file: "browser-polyfill.js"});
browser.tabs.executeScript({file: "content.js"}).then(result => {
  // ...
});

Using the Promise-based APIs

The Promise-based APIs in the browser namespace work, for the most part, very similarly to the callback-based APIs in Chrome's chrome namespace. The major differences are:

  • Rather than receiving a callback argument, every async function returns a Promise object, which resolves or rejects when the operation completes.

  • Rather than checking the chrome.runtime.lastError property from every callback, code which needs to explicitly deal with errors registers a separate Promise rejection handler.

  • Rather than receiving a sendResponse callback to send a response, onMessage listeners simply return a Promise whose resolution value is used as a reply.

  • Rather than nesting callbacks when a sequence of operations depend on each other, Promise chaining is generally used instead.

  • The resulting Promises can be also used with async and await, rather than dealt with directly.

Examples

The following code will retrieve a list of URLs patterns from the storage API, retrieve a list of tabs which match any of them, reload each of those tabs, and notify the user that is has been done:

browser.storage.local.get("urls").then(({urls}) => {
  return browser.tabs.query({url: urls});
}).then(tabs => {
  return Promise.all(
    Array.from(tabs, tab => browser.tabs.reload(tab.id))
  );
}).then(() => {
  return browser.notifications.create({
    type: "basic",
    iconUrl: "icon.png",
    title: "Tabs reloaded",
    message: "Your tabs have been reloaded",
  });
}).catch(error => {
  console.error(`An error occurred while reloading tabs: ${error.message}`);
});

Or, using an async function:

async function reloadTabs() {
  try {
    let {urls} = await browser.storage.local.get("urls");

    let tabs = await browser.tabs.query({url: urls});

    await Promise.all(
      Array.from(tabs, tab => browser.tabs.reload(tab.id))
    );

    await browser.notifications.create({
      type: "basic",
      iconUrl: "icon.png",
      title: "Tabs reloaded",
      message: "Your tabs have been reloaded",
    });
  } catch (error) {
    console.error(`An error occurred while reloading tabs: ${error.message}`);
  }
}

It's also possible to use Promises effectively using two-way messaging. Communication between a background page and a tab content script, for example, looks something like this from the background page side:

browser.tabs.sendMessage("get-ids").then(results => {
  processResults(results);
});

And like this from the content script:

browser.runtime.onMessage.addListener(msg => {
  if (msg == "get-ids") {
    return browser.storage.local.get("idPattern").then(({idPattern}) => {
      return Array.from(document.querySelectorAll(idPattern),
                        elem => elem.textContent);
    });
  }
});

or:

browser.runtime.onMessage.addListener(async function(msg) {
  if (msg == "get-ids") {
    let {idPattern} = await browser.storage.local.get("idPattern");

    return Array.from(document.querySelectorAll(idPattern),
                      elem => elem.textContent);
  }
});

Or vice versa.