merge mozilla-inbound to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 03 Apr 2017 12:13:46 +0200
changeset 399028 aaa0cd3bd620daf6be29c72625f6e63fd0bc1d46
parent 399008 b2d106b73e6a4a1a8557981e597fea358b09d650 (current diff)
parent 399027 66146205625406fd7eabd6b03c8b4c7ec464c3a1 (diff)
child 399029 9c180e6aaea8d51a468dd0adb78c660e2545b341
child 399037 69713a85b57b688184d8175f53f91e7533114e75
child 399086 3345a25e462a52334de1f0169038defd97784fbe
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central a=merge
browser/components/extensions/ext-desktop-runtime.js
toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
tools/profiler/core/platform.cpp
--- a/browser/components/extensions/ext-bookmarks.js
+++ b/browser/components/extensions/ext-bookmarks.js
@@ -1,21 +1,16 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-const {
+var {
   SingletonEventManager,
 } = ExtensionUtils;
 
-XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
-                                  "resource://devtools/shared/event-emitter.js");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 let listenerCount = 0;
 
 function getTree(rootGuid, onlyChildren) {
@@ -178,199 +173,201 @@ function decrementListeners() {
 
 function incrementListeners() {
   listenerCount++;
   if (listenerCount == 1) {
     PlacesUtils.bookmarks.addObserver(observer, false);
   }
 }
 
-extensions.registerSchemaAPI("bookmarks", "addon_parent", context => {
-  return {
-    bookmarks: {
-      get: function(idOrIdList) {
-        let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];
+this.bookmarks = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      bookmarks: {
+        get: function(idOrIdList) {
+          let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];
 
-        return Task.spawn(function* () {
-          let bookmarks = [];
-          for (let id of list) {
-            let bookmark = yield PlacesUtils.bookmarks.fetch({guid: id});
-            if (!bookmark) {
-              throw new Error("Bookmark not found");
+          return Task.spawn(function* () {
+            let bookmarks = [];
+            for (let id of list) {
+              let bookmark = yield PlacesUtils.bookmarks.fetch({guid: id});
+              if (!bookmark) {
+                throw new Error("Bookmark not found");
+              }
+              bookmarks.push(convert(bookmark));
             }
-            bookmarks.push(convert(bookmark));
-          }
-          return bookmarks;
-        }).catch(error => Promise.reject({message: error.message}));
-      },
+            return bookmarks;
+          }).catch(error => Promise.reject({message: error.message}));
+        },
 
-      getChildren: function(id) {
-        // TODO: We should optimize this.
-        return getTree(id, true);
-      },
+        getChildren: function(id) {
+          // TODO: We should optimize this.
+          return getTree(id, true);
+        },
 
-      getTree: function() {
-        return getTree(PlacesUtils.bookmarks.rootGuid, false);
-      },
+        getTree: function() {
+          return getTree(PlacesUtils.bookmarks.rootGuid, false);
+        },
 
-      getSubTree: function(id) {
-        return getTree(id, false);
-      },
+        getSubTree: function(id) {
+          return getTree(id, false);
+        },
 
-      search: function(query) {
-        return PlacesUtils.bookmarks.search(query).then(result => result.map(convert));
-      },
+        search: function(query) {
+          return PlacesUtils.bookmarks.search(query).then(result => result.map(convert));
+        },
 
-      getRecent: function(numberOfItems) {
-        return PlacesUtils.bookmarks.getRecent(numberOfItems).then(result => result.map(convert));
-      },
+        getRecent: function(numberOfItems) {
+          return PlacesUtils.bookmarks.getRecent(numberOfItems).then(result => result.map(convert));
+        },
 
-      create: function(bookmark) {
-        let info = {
-          title: bookmark.title || "",
-        };
+        create: function(bookmark) {
+          let info = {
+            title: bookmark.title || "",
+          };
 
-        // If url is NULL or missing, it will be a folder.
-        if (bookmark.url !== null) {
-          info.type = PlacesUtils.bookmarks.TYPE_BOOKMARK;
-          info.url = bookmark.url || "";
-        } else {
-          info.type = PlacesUtils.bookmarks.TYPE_FOLDER;
-        }
+          // If url is NULL or missing, it will be a folder.
+          if (bookmark.url !== null) {
+            info.type = PlacesUtils.bookmarks.TYPE_BOOKMARK;
+            info.url = bookmark.url || "";
+          } else {
+            info.type = PlacesUtils.bookmarks.TYPE_FOLDER;
+          }
 
-        if (bookmark.index !== null) {
-          info.index = bookmark.index;
-        }
+          if (bookmark.index !== null) {
+            info.index = bookmark.index;
+          }
 
-        if (bookmark.parentId !== null) {
-          info.parentGuid = bookmark.parentId;
-        } else {
-          info.parentGuid = PlacesUtils.bookmarks.unfiledGuid;
-        }
+          if (bookmark.parentId !== null) {
+            info.parentGuid = bookmark.parentId;
+          } else {
+            info.parentGuid = PlacesUtils.bookmarks.unfiledGuid;
+          }
 
-        try {
-          return PlacesUtils.bookmarks.insert(info).then(convert)
-            .catch(error => Promise.reject({message: error.message}));
-        } catch (e) {
-          return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
-        }
-      },
+          try {
+            return PlacesUtils.bookmarks.insert(info).then(convert)
+              .catch(error => Promise.reject({message: error.message}));
+          } catch (e) {
+            return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
+          }
+        },
 
-      move: function(id, destination) {
-        let info = {
-          guid: id,
-        };
+        move: function(id, destination) {
+          let info = {
+            guid: id,
+          };
 
-        if (destination.parentId !== null) {
-          info.parentGuid = destination.parentId;
-        }
-        info.index = (destination.index === null) ?
-          PlacesUtils.bookmarks.DEFAULT_INDEX : destination.index;
+          if (destination.parentId !== null) {
+            info.parentGuid = destination.parentId;
+          }
+          info.index = (destination.index === null) ?
+            PlacesUtils.bookmarks.DEFAULT_INDEX : destination.index;
 
-        try {
-          return PlacesUtils.bookmarks.update(info).then(convert)
-            .catch(error => Promise.reject({message: error.message}));
-        } catch (e) {
-          return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
-        }
-      },
+          try {
+            return PlacesUtils.bookmarks.update(info).then(convert)
+              .catch(error => Promise.reject({message: error.message}));
+          } catch (e) {
+            return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
+          }
+        },
 
-      update: function(id, changes) {
-        let info = {
-          guid: id,
-        };
+        update: function(id, changes) {
+          let info = {
+            guid: id,
+          };
 
-        if (changes.title !== null) {
-          info.title = changes.title;
-        }
-        if (changes.url !== null) {
-          info.url = changes.url;
-        }
+          if (changes.title !== null) {
+            info.title = changes.title;
+          }
+          if (changes.url !== null) {
+            info.url = changes.url;
+          }
 
-        try {
-          return PlacesUtils.bookmarks.update(info).then(convert)
-            .catch(error => Promise.reject({message: error.message}));
-        } catch (e) {
-          return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
-        }
-      },
+          try {
+            return PlacesUtils.bookmarks.update(info).then(convert)
+              .catch(error => Promise.reject({message: error.message}));
+          } catch (e) {
+            return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
+          }
+        },
 
-      remove: function(id) {
-        let info = {
-          guid: id,
-        };
+        remove: function(id) {
+          let info = {
+            guid: id,
+          };
 
-        // The API doesn't give you the old bookmark at the moment
-        try {
-          return PlacesUtils.bookmarks.remove(info, {preventRemovalOfNonEmptyFolders: true}).then(result => {})
-            .catch(error => Promise.reject({message: error.message}));
-        } catch (e) {
-          return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
-        }
-      },
+          // The API doesn't give you the old bookmark at the moment
+          try {
+            return PlacesUtils.bookmarks.remove(info, {preventRemovalOfNonEmptyFolders: true}).then(result => {})
+              .catch(error => Promise.reject({message: error.message}));
+          } catch (e) {
+            return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
+          }
+        },
 
-      removeTree: function(id) {
-        let info = {
-          guid: id,
-        };
+        removeTree: function(id) {
+          let info = {
+            guid: id,
+          };
 
-        try {
-          return PlacesUtils.bookmarks.remove(info).then(result => {})
-            .catch(error => Promise.reject({message: error.message}));
-        } catch (e) {
-          return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
-        }
-      },
+          try {
+            return PlacesUtils.bookmarks.remove(info).then(result => {})
+              .catch(error => Promise.reject({message: error.message}));
+          } catch (e) {
+            return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
+          }
+        },
 
-      onCreated: new SingletonEventManager(context, "bookmarks.onCreated", fire => {
-        let listener = (event, bookmark) => {
-          fire.sync(bookmark.id, bookmark);
-        };
+        onCreated: new SingletonEventManager(context, "bookmarks.onCreated", fire => {
+          let listener = (event, bookmark) => {
+            fire.sync(bookmark.id, bookmark);
+          };
 
-        observer.on("created", listener);
-        incrementListeners();
-        return () => {
-          observer.off("created", listener);
-          decrementListeners();
-        };
-      }).api(),
+          observer.on("created", listener);
+          incrementListeners();
+          return () => {
+            observer.off("created", listener);
+            decrementListeners();
+          };
+        }).api(),
 
-      onRemoved: new SingletonEventManager(context, "bookmarks.onRemoved", fire => {
-        let listener = (event, data) => {
-          fire.sync(data.guid, data.info);
-        };
+        onRemoved: new SingletonEventManager(context, "bookmarks.onRemoved", fire => {
+          let listener = (event, data) => {
+            fire.sync(data.guid, data.info);
+          };
 
-        observer.on("removed", listener);
-        incrementListeners();
-        return () => {
-          observer.off("removed", listener);
-          decrementListeners();
-        };
-      }).api(),
+          observer.on("removed", listener);
+          incrementListeners();
+          return () => {
+            observer.off("removed", listener);
+            decrementListeners();
+          };
+        }).api(),
 
-      onChanged: new SingletonEventManager(context, "bookmarks.onChanged", fire => {
-        let listener = (event, data) => {
-          fire.sync(data.guid, data.info);
-        };
+        onChanged: new SingletonEventManager(context, "bookmarks.onChanged", fire => {
+          let listener = (event, data) => {
+            fire.sync(data.guid, data.info);
+          };
 
-        observer.on("changed", listener);
-        incrementListeners();
-        return () => {
-          observer.off("changed", listener);
-          decrementListeners();
-        };
-      }).api(),
+          observer.on("changed", listener);
+          incrementListeners();
+          return () => {
+            observer.off("changed", listener);
+            decrementListeners();
+          };
+        }).api(),
 
-      onMoved: new SingletonEventManager(context, "bookmarks.onMoved", fire => {
-        let listener = (event, data) => {
-          fire.sync(data.guid, data.info);
-        };
+        onMoved: new SingletonEventManager(context, "bookmarks.onMoved", fire => {
+          let listener = (event, data) => {
+            fire.sync(data.guid, data.info);
+          };
 
-        observer.on("moved", listener);
-        incrementListeners();
-        return () => {
-          observer.off("moved", listener);
-          decrementListeners();
-        };
-      }).api(),
-    },
-  };
-});
+          observer.on("moved", listener);
+          incrementListeners();
+          return () => {
+            observer.off("moved", listener);
+            decrementListeners();
+          };
+        }).api(),
+      },
+    };
+  }
+};
rename from browser/components/extensions/ext-desktop-runtime.js
rename to browser/components/extensions/ext-browser.js
--- a/browser/components/extensions/ext-desktop-runtime.js
+++ b/browser/components/extensions/ext-browser.js
@@ -1,26 +1,225 @@
 "use strict";
 
+XPCOMUtils.defineLazyModuleGetter(global, "EventEmitter",
+                                  "resource://devtools/shared/event-emitter.js");
+
+// This function is pretty tightly tied to Extension.jsm.
+// Its job is to fill in the |tab| property of the sender.
+function getSender(extension, target, sender) {
+  let tabId;
+  if ("tabId" in sender) {
+    // The message came from a privileged extension page running in a tab. In
+    // that case, it should include a tabId property (which is filled in by the
+    // page-open listener below).
+    tabId = sender.tabId;
+    delete sender.tabId;
+  } else if (target instanceof Ci.nsIDOMXULElement) {
+    tabId = tabTracker.getBrowserData(target).tabId;
+  }
+
+  if (tabId) {
+    let tab = extension.tabManager.get(tabId, null);
+    if (tab) {
+      sender.tab = tab.convert();
+    }
+  }
+}
+
+// Used by Extension.jsm
+global.tabGetSender = getSender;
+
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("uninstall", (msg, extension) => {
   if (extension.uninstallURL) {
     let browser = windowTracker.topWindow.gBrowser;
     browser.addTab(extension.uninstallURL, {relatedToCurrent: true});
   }
 });
 
+extensions.on("page-shutdown", (type, context) => {
+  if (context.viewType == "tab") {
+    if (context.extension.id !== context.xulBrowser.contentPrincipal.addonId) {
+      // Only close extension tabs.
+      // This check prevents about:addons from closing when it contains a
+      // WebExtension as an embedded inline options page.
+      return;
+    }
+    let {gBrowser} = context.xulBrowser.ownerGlobal;
+    if (gBrowser) {
+      let nativeTab = gBrowser.getTabForBrowser(context.xulBrowser);
+      if (nativeTab) {
+        gBrowser.removeTab(nativeTab);
+      }
+    }
+  }
+});
+/* eslint-enable mozilla/balanced-listeners */
+/* eslint-enable mozilla/balanced-listeners */
+
 global.openOptionsPage = (extension) => {
   let window = windowTracker.topWindow;
   if (!window) {
     return Promise.reject({message: "No browser window available"});
   }
 
   if (extension.manifest.options_ui.open_in_tab) {
     window.switchToTabHavingURI(extension.manifest.options_ui.page, true);
     return Promise.resolve();
   }
 
   let viewId = `addons://detail/${encodeURIComponent(extension.id)}/preferences`;
 
   return window.BrowserOpenAddonsMgr(viewId);
 };
 
+extensions.registerModules({
+  bookmarks: {
+    url: "chrome://browser/content/ext-bookmarks.js",
+    schema: "chrome://browser/content/schemas/bookmarks.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["bookmarks"],
+    ],
+  },
+  browserAction: {
+    url: "chrome://browser/content/ext-browserAction.js",
+    schema: "chrome://browser/content/schemas/browser_action.json",
+    scopes: ["addon_parent"],
+    manifest: ["browser_action"],
+    paths: [
+      ["browserAction"],
+    ],
+  },
+  browsingData: {
+    url: "chrome://browser/content/ext-browsingData.js",
+    schema: "chrome://browser/content/schemas/browsing_data.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["browsingData"],
+    ],
+  },
+  chrome_settings_overrides: {
+    url: "chrome://browser/content/ext-chrome-settings-overrides.js",
+    scopes: [],
+    schema: "chrome://browser/content/schemas/chrome_settings_overrides.json",
+    manifest: ["chrome_settings_overrides"],
+  },
+  commands: {
+    url: "chrome://browser/content/ext-commands.js",
+    schema: "chrome://browser/content/schemas/commands.json",
+    scopes: ["addon_parent"],
+    manifest: ["commands"],
+    paths: [
+      ["commands"],
+    ],
+  },
+  contextMenus: {
+    url: "chrome://browser/content/ext-contextMenus.js",
+    schema: "chrome://browser/content/schemas/context_menus.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["contextMenus"],
+    ],
+  },
+  devtools: {
+    url: "chrome://browser/content/ext-devtools.js",
+    schema: "chrome://browser/content/schemas/devtools.json",
+    scopes: ["devtools_parent"],
+    manifest: ["devtools_page"],
+    paths: [
+      ["devtools"],
+    ],
+  },
+  devtools_inspectedWindow: {
+    url: "chrome://browser/content/ext-devtools-inspectedWindow.js",
+    schema: "chrome://browser/content/schemas/devtools_inspected_window.json",
+    scopes: ["devtools_parent"],
+    paths: [
+      ["devtools", "inspectedWindow"],
+    ],
+  },
+  devtools_network: {
+    url: "chrome://browser/content/ext-devtools-network.js",
+    schema: "chrome://browser/content/schemas/devtools_network.json",
+    scopes: ["devtools_parent"],
+    paths: [
+      ["devtools", "network"],
+    ],
+  },
+  devtools_panels: {
+    url: "chrome://browser/content/ext-devtools-panels.js",
+    schema: "chrome://browser/content/schemas/devtools_panels.json",
+    scopes: ["devtools_parent"],
+    paths: [
+      ["devtools", "panels"],
+    ],
+  },
+  history: {
+    url: "chrome://browser/content/ext-history.js",
+    schema: "chrome://browser/content/schemas/history.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["history"],
+    ],
+  },
+  omnibox: {
+    url: "chrome://browser/content/ext-omnibox.js",
+    schema: "chrome://browser/content/schemas/omnibox.json",
+    scopes: ["addon_parent"],
+    manifest: ["omnibox"],
+    paths: [
+      ["omnibox"],
+    ],
+  },
+  pageAction: {
+    url: "chrome://browser/content/ext-pageAction.js",
+    schema: "chrome://browser/content/schemas/page_action.json",
+    scopes: ["addon_parent"],
+    manifest: ["page_action"],
+    paths: [
+      ["pageAction"],
+    ],
+  },
+  sessions: {
+    url: "chrome://browser/content/ext-sessions.js",
+    schema: "chrome://browser/content/schemas/sessions.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["sessions"],
+    ],
+  },
+  sidebarAction: {
+    url: "chrome://browser/content/ext-sidebarAction.js",
+    schema: "chrome://browser/content/schemas/sidebar_action.json",
+    scopes: ["addon_parent"],
+    manifest: ["sidebar_action"],
+    paths: [
+      ["sidebarAction"],
+    ],
+  },
+  tabs: {
+    url: "chrome://browser/content/ext-tabs.js",
+    schema: "chrome://browser/content/schemas/tabs.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["tabs"],
+    ],
+  },
+  urlOverrides: {
+    url: "chrome://browser/content/ext-url-overrides.js",
+    schema: "chrome://browser/content/schemas/url_overrides.json",
+    scopes: ["addon_parent"],
+    manifest: ["chrome_url_overrides"],
+    paths: [
+      ["urlOverrides"],
+    ],
+  },
+  windows: {
+    url: "chrome://browser/content/ext-windows.js",
+    schema: "chrome://browser/content/schemas/windows.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["windows"],
+    ],
+  },
+});
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -11,27 +11,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "ViewPopup",
                                   "resource:///modules/ExtensionPopups.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
                                    "@mozilla.org/inspector/dom-utils;1",
                                    "inIDOMUtils");
 
 Cu.import("resource://devtools/shared/event-emitter.js");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 var {
   IconDetails,
   SingletonEventManager,
 } = ExtensionUtils;
 
 const POPUP_PRELOAD_TIMEOUT_MS = 200;
 
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 function isAncestorOrSelf(target, node) {
   for (; node; node = node.parentNode) {
     if (node === target) {
       return true;
     }
   }
   return false;
@@ -44,54 +43,70 @@ XPCOMUtils.defineLazyGetter(this, "brows
   return {
     "navbar": CustomizableUI.AREA_NAVBAR,
     "menupanel": CustomizableUI.AREA_PANEL,
     "tabstrip": CustomizableUI.AREA_TABSTRIP,
     "personaltoolbar": CustomizableUI.AREA_BOOKMARKS,
   };
 });
 
-// Responsible for the browser_action section of the manifest as well
-// as the associated popup.
-function BrowserAction(options, extension) {
-  this.extension = extension;
-
-  let widgetId = makeWidgetId(extension.id);
-  this.id = `${widgetId}-browser-action`;
-  this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
-  this.widget = null;
-
-  this.pendingPopup = null;
-  this.pendingPopupTimeout = null;
-
-  this.tabManager = extension.tabManager;
-
-  this.defaults = {
-    enabled: true,
-    title: options.default_title || extension.name,
-    badgeText: "",
-    badgeBackgroundColor: null,
-    icon: IconDetails.normalize({path: options.default_icon}, extension),
-    popup: options.default_popup || "",
-    area: browserAreas[options.default_area || "navbar"],
-  };
-
-  this.browserStyle = options.browser_style || false;
-  if (options.browser_style === null) {
-    this.extension.logger.warn("Please specify whether you want browser_style " +
-                               "or not in your browser_action options.");
+this.browserAction = class extends ExtensionAPI {
+  static for(extension) {
+    return browserActionMap.get(extension);
   }
 
-  this.tabContext = new TabContext(tab => Object.create(this.defaults),
-                                   extension);
+  onManifestEntry(entryName) {
+    let {extension} = this;
+
+    let options = extension.manifest.browser_action;
+
+    let widgetId = makeWidgetId(extension.id);
+    this.id = `${widgetId}-browser-action`;
+    this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
+    this.widget = null;
+
+    this.pendingPopup = null;
+    this.pendingPopupTimeout = null;
+
+    this.tabManager = extension.tabManager;
+
+    this.defaults = {
+      enabled: true,
+      title: options.default_title || extension.name,
+      badgeText: "",
+      badgeBackgroundColor: null,
+      icon: IconDetails.normalize({path: options.default_icon}, extension),
+      popup: options.default_popup || "",
+      area: browserAreas[options.default_area || "navbar"],
+    };
 
-  EventEmitter.decorate(this);
-}
+    this.browserStyle = options.browser_style || false;
+    if (options.browser_style === null) {
+      this.extension.logger.warn("Please specify whether you want browser_style " +
+                                 "or not in your browser_action options.");
+    }
+
+    this.tabContext = new TabContext(tab => Object.create(this.defaults),
+                                     extension);
+
+    EventEmitter.decorate(this);
 
-BrowserAction.prototype = {
+    this.build();
+    browserActionMap.set(extension, this);
+  }
+
+  onShutdown(reason) {
+    browserActionMap.delete(this.extension);
+
+    this.tabContext.shutdown();
+    CustomizableUI.destroyWidget(this.id);
+
+    this.clearPopup();
+  }
+
   build() {
     let widget = CustomizableUI.createWidget({
       id: this.id,
       viewId: this.viewId,
       type: "view",
       removable: true,
       label: this.defaults.title || this.extension.name,
       tooltiptext: this.defaults.title || "",
@@ -102,23 +117,24 @@ BrowserAction.prototype = {
         view.id = this.viewId;
         view.setAttribute("flex", "1");
 
         document.getElementById("PanelUI-multiView").appendChild(view);
         document.addEventListener("popupshowing", this);
       },
 
       onDestroyed: document => {
+        document.removeEventListener("popupshowing", this);
+
         let view = document.getElementById(this.viewId);
         if (view) {
           this.clearPopup();
           CustomizableUI.hidePanelForNode(view);
           view.remove();
         }
-        document.removeEventListener("popupshowing", this);
       },
 
       onCreated: node => {
         node.classList.add("badged-button");
         node.classList.add("webextension-browser-action");
         node.setAttribute("constrain-size", "true");
 
         node.onmousedown = event => this.handleEvent(event);
@@ -155,26 +171,28 @@ BrowserAction.prototype = {
         }
       },
     });
 
     this.tabContext.on("tab-select", // eslint-disable-line mozilla/balanced-listeners
                        (evt, tab) => { this.updateWindow(tab.ownerGlobal); });
 
     this.widget = widget;
-  },
+  }
 
   /**
    * Triggers this browser action for the given window, with the same effects as
    * if it were clicked by a user.
    *
    * This has no effect if the browser action is disabled for, or not
    * present in, the given window.
+   *
+   * @param {Window} window
    */
-  triggerAction: Task.async(function* (window) {
+  async triggerAction(window) {
     let popup = ViewPopup.for(this.extension, window);
     if (popup) {
       popup.closePopup();
       return;
     }
 
     let widget = this.widget.forWindow(window);
     let tab = window.gBrowser.selectedTab;
@@ -183,25 +201,25 @@ BrowserAction.prototype = {
       return;
     }
 
     // Popups are shown only if a popup URL is defined; otherwise
     // a "click" event is dispatched. This is done for compatibility with the
     // Google Chrome onClicked extension API.
     if (this.getProperty(tab, "popup")) {
       if (this.widget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
-        yield window.PanelUI.show();
+        await window.PanelUI.show();
       }
 
       let event = new window.CustomEvent("command", {bubbles: true, cancelable: true});
       widget.node.dispatchEvent(event);
     } else {
       this.emit("click");
     }
-  }),
+  }
 
   handleEvent(event) {
     let button = event.target;
     let window = button.ownerGlobal;
 
     switch (event.type) {
       case "mousedown":
         if (event.button == 0) {
@@ -274,17 +292,17 @@ BrowserAction.prototype = {
           global.actionContextMenu({
             extension: this.extension,
             onBrowserAction: true,
             menu: menu,
           });
         }
         break;
     }
-  },
+  }
 
   /**
    * Returns a potentially pre-loaded popup for the given URL in the given
    * window. If a matching pre-load popup already exists, returns that.
    * Otherwise, initializes a new one.
    *
    * If a pre-load popup exists which does not match, it is destroyed before a
    * new one is created.
@@ -310,46 +328,46 @@ BrowserAction.prototype = {
 
         return pendingPopup;
       }
       pendingPopup.destroy();
     }
 
     let fixedWidth = this.widget.areaType == CustomizableUI.TYPE_MENU_PANEL;
     return new ViewPopup(this.extension, window, popupURL, this.browserStyle, fixedWidth, blockParser);
-  },
+  }
 
   /**
    * Clears any pending pre-loaded popup and related timeouts.
    */
   clearPopup() {
     this.clearPopupTimeout();
     if (this.pendingPopup) {
       if (this.tabToRevokeDuringClearPopup) {
         this.tabManager.revokeActiveTabPermission(this.tabToRevokeDuringClearPopup);
-        this.tabToRevokeDuringClearPopup = null;
       }
       this.pendingPopup.destroy();
       this.pendingPopup = null;
     }
-  },
+    this.tabToRevokeDuringClearPopup = null;
+  }
 
   /**
    * Clears any pending timeouts to clear stale, pre-loaded popups.
    */
   clearPopupTimeout() {
     if (this.pendingPopup) {
       this.pendingPopup.window.removeEventListener("mouseup", this, true);
     }
 
     if (this.pendingPopupTimeout) {
       clearTimeout(this.pendingPopupTimeout);
       this.pendingPopupTimeout = null;
     }
-  },
+  }
 
   // Update the toolbar button |node| with the tab context data
   // in |tabData|.
   updateButton(node, tabData) {
     let title = tabData.title || this.extension.name;
     node.setAttribute("tooltiptext", title);
     node.setAttribute("label", title);
 
@@ -397,194 +415,172 @@ BrowserAction.prototype = {
       IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
 
     node.setAttribute("style", `
       --webextension-menupanel-image: url("${getIcon(32)}");
       --webextension-menupanel-image-2x: url("${getIcon(64)}");
       --webextension-toolbar-image: url("${IconDetails.escapeUrl(icon)}");
       --webextension-toolbar-image-2x: url("${getIcon(baseSize * 2)}");
     `);
-  },
+  }
 
   // Update the toolbar button for a given window.
   updateWindow(window) {
     let widget = this.widget.forWindow(window);
     if (widget) {
       let tab = window.gBrowser.selectedTab;
       this.updateButton(widget.node, this.tabContext.get(tab));
     }
-  },
+  }
 
   // Update the toolbar button when the extension changes the icon,
   // title, badge, etc. If it only changes a parameter for a single
   // tab, |tab| will be that tab. Otherwise it will be null.
   updateOnChange(tab) {
     if (tab) {
       if (tab.selected) {
         this.updateWindow(tab.ownerGlobal);
       }
     } else {
       for (let window of windowTracker.browserWindows()) {
         this.updateWindow(window);
       }
     }
-  },
+  }
 
   // tab is allowed to be null.
   // prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
   setProperty(tab, prop, value) {
     if (tab == null) {
       this.defaults[prop] = value;
     } else if (value != null) {
       this.tabContext.get(tab)[prop] = value;
     } else {
       delete this.tabContext.get(tab)[prop];
     }
 
     this.updateOnChange(tab);
-  },
+  }
 
   // tab is allowed to be null.
   // prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
   getProperty(tab, prop) {
     if (tab == null) {
       return this.defaults[prop];
     }
     return this.tabContext.get(tab)[prop];
-  },
-
-  shutdown() {
-    this.tabContext.shutdown();
-    CustomizableUI.destroyWidget(this.id);
-  },
-};
-
-BrowserAction.for = (extension) => {
-  return browserActionMap.get(extension);
-};
-
-global.browserActionFor = BrowserAction.for;
-
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
-  let browserAction = new BrowserAction(manifest.browser_action, extension);
-  browserAction.build();
-  browserActionMap.set(extension, browserAction);
-});
-
-extensions.on("shutdown", (type, extension) => {
-  if (browserActionMap.has(extension)) {
-    browserActionMap.get(extension).shutdown();
-    browserActionMap.delete(extension);
-  }
-});
-/* eslint-enable mozilla/balanced-listeners */
-
-extensions.registerSchemaAPI("browserAction", "addon_parent", context => {
-  let {extension} = context;
-
-  let {tabManager} = extension;
-
-  function getTab(tabId) {
-    if (tabId !== null) {
-      return tabTracker.getTab(tabId);
-    }
-    return null;
   }
 
-  return {
-    browserAction: {
-      onClicked: new SingletonEventManager(context, "browserAction.onClicked", fire => {
-        let listener = () => {
-          fire.async(tabManager.convert(tabTracker.activeTab));
-        };
-        BrowserAction.for(extension).on("click", listener);
-        return () => {
-          BrowserAction.for(extension).off("click", listener);
-        };
-      }).api(),
+  getAPI(context) {
+    let {extension} = context;
+    let {tabManager} = extension;
+
+    let browserAction = this;
 
-      enable: function(tabId) {
-        let tab = getTab(tabId);
-        BrowserAction.for(extension).setProperty(tab, "enabled", true);
-      },
+    function getTab(tabId) {
+      if (tabId !== null) {
+        return tabTracker.getTab(tabId);
+      }
+      return null;
+    }
 
-      disable: function(tabId) {
-        let tab = getTab(tabId);
-        BrowserAction.for(extension).setProperty(tab, "enabled", false);
-      },
+    return {
+      browserAction: {
+        onClicked: new SingletonEventManager(context, "browserAction.onClicked", fire => {
+          let listener = () => {
+            fire.async(tabManager.convert(tabTracker.activeTab));
+          };
+          browserAction.on("click", listener);
+          return () => {
+            browserAction.off("click", listener);
+          };
+        }).api(),
 
-      setTitle: function(details) {
-        let tab = getTab(details.tabId);
+        enable: function(tabId) {
+          let tab = getTab(tabId);
+          browserAction.setProperty(tab, "enabled", true);
+        },
 
-        let title = details.title;
-        // Clear the tab-specific title when given a null string.
-        if (tab && title == "") {
-          title = null;
-        }
-        BrowserAction.for(extension).setProperty(tab, "title", title);
-      },
+        disable: function(tabId) {
+          let tab = getTab(tabId);
+          browserAction.setProperty(tab, "enabled", false);
+        },
+
+        setTitle: function(details) {
+          let tab = getTab(details.tabId);
 
-      getTitle: function(details) {
-        let tab = getTab(details.tabId);
-
-        let title = BrowserAction.for(extension).getProperty(tab, "title");
-        return Promise.resolve(title);
-      },
+          let title = details.title;
+          // Clear the tab-specific title when given a null string.
+          if (tab && title == "") {
+            title = null;
+          }
+          browserAction.setProperty(tab, "title", title);
+        },
 
-      setIcon: function(details) {
-        let tab = getTab(details.tabId);
+        getTitle: function(details) {
+          let tab = getTab(details.tabId);
 
-        let icon = IconDetails.normalize(details, extension, context);
-        BrowserAction.for(extension).setProperty(tab, "icon", icon);
-      },
+          let title = browserAction.getProperty(tab, "title");
+          return Promise.resolve(title);
+        },
+
+        setIcon: function(details) {
+          let tab = getTab(details.tabId);
 
-      setBadgeText: function(details) {
-        let tab = getTab(details.tabId);
+          let icon = IconDetails.normalize(details, extension, context);
+          browserAction.setProperty(tab, "icon", icon);
+        },
 
-        BrowserAction.for(extension).setProperty(tab, "badgeText", details.text);
-      },
+        setBadgeText: function(details) {
+          let tab = getTab(details.tabId);
 
-      getBadgeText: function(details) {
-        let tab = getTab(details.tabId);
+          browserAction.setProperty(tab, "badgeText", details.text);
+        },
+
+        getBadgeText: function(details) {
+          let tab = getTab(details.tabId);
 
-        let text = BrowserAction.for(extension).getProperty(tab, "badgeText");
-        return Promise.resolve(text);
-      },
+          let text = browserAction.getProperty(tab, "badgeText");
+          return Promise.resolve(text);
+        },
 
-      setPopup: function(details) {
-        let tab = getTab(details.tabId);
+        setPopup: function(details) {
+          let tab = getTab(details.tabId);
 
-        // Note: Chrome resolves arguments to setIcon relative to the calling
-        // context, but resolves arguments to setPopup relative to the extension
-        // root.
-        // For internal consistency, we currently resolve both relative to the
-        // calling context.
-        let url = details.popup && context.uri.resolve(details.popup);
-        BrowserAction.for(extension).setProperty(tab, "popup", url);
-      },
+          // Note: Chrome resolves arguments to setIcon relative to the calling
+          // context, but resolves arguments to setPopup relative to the extension
+          // root.
+          // For internal consistency, we currently resolve both relative to the
+          // calling context.
+          let url = details.popup && context.uri.resolve(details.popup);
+          browserAction.setProperty(tab, "popup", url);
+        },
 
-      getPopup: function(details) {
-        let tab = getTab(details.tabId);
+        getPopup: function(details) {
+          let tab = getTab(details.tabId);
 
-        let popup = BrowserAction.for(extension).getProperty(tab, "popup");
-        return Promise.resolve(popup);
-      },
+          let popup = browserAction.getProperty(tab, "popup");
+          return Promise.resolve(popup);
+        },
 
-      setBadgeBackgroundColor: function(details) {
-        let tab = getTab(details.tabId);
-        let color = details.color;
-        if (!Array.isArray(color)) {
-          let col = DOMUtils.colorToRGBA(color);
-          color = col && [col.r, col.g, col.b, Math.round(col.a * 255)];
-        }
-        BrowserAction.for(extension).setProperty(tab, "badgeBackgroundColor", color);
-      },
+        setBadgeBackgroundColor: function(details) {
+          let tab = getTab(details.tabId);
+          let color = details.color;
+          if (!Array.isArray(color)) {
+            let col = DOMUtils.colorToRGBA(color);
+            color = col && [col.r, col.g, col.b, Math.round(col.a * 255)];
+          }
+          browserAction.setProperty(tab, "badgeBackgroundColor", color);
+        },
 
-      getBadgeBackgroundColor: function(details, callback) {
-        let tab = getTab(details.tabId);
+        getBadgeBackgroundColor: function(details, callback) {
+          let tab = getTab(details.tabId);
 
-        let color = BrowserAction.for(extension).getProperty(tab, "badgeBackgroundColor");
-        return Promise.resolve(color || [0xd9, 0, 0, 255]);
+          let color = browserAction.getProperty(tab, "badgeBackgroundColor");
+          return Promise.resolve(color || [0xd9, 0, 0, 255]);
+        },
       },
-    },
-  };
-});
+    };
+  }
+};
+
+global.browserActionFor = this.browserAction.for;
+
--- a/browser/components/extensions/ext-browsingData.js
+++ b/browser/components/extensions/ext-browsingData.js
@@ -1,14 +1,12 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
                                   "resource:///modules/Sanitizer.jsm");
@@ -170,68 +168,70 @@ function doRemoval(options, dataToRemove
   }
   if (extension && invalidDataTypes.length) {
     extension.logger.warn(
       `Firefox does not support dataTypes: ${invalidDataTypes.toString()}.`);
   }
   return Promise.all(removalPromises);
 }
 
-extensions.registerSchemaAPI("browsingData", "addon_parent", context => {
-  let {extension} = context;
-  return {
-    browsingData: {
-      settings() {
-        const PREF_DOMAIN = "privacy.cpd.";
-        // The following prefs are the only ones in Firefox that match corresponding
-        // values used by Chrome when rerturning settings.
-        const PREF_LIST = ["cache", "cookies", "history", "formdata", "downloads"];
+this.browsingData = class extends ExtensionAPI {
+  getAPI(context) {
+    let {extension} = context;
+    return {
+      browsingData: {
+        settings() {
+          const PREF_DOMAIN = "privacy.cpd.";
+          // The following prefs are the only ones in Firefox that match corresponding
+          // values used by Chrome when rerturning settings.
+          const PREF_LIST = ["cache", "cookies", "history", "formdata", "downloads"];
 
-        // since will be the start of what is returned by Sanitizer.getClearRange
-        // divided by 1000 to convert to ms.
-        // If Sanitizer.getClearRange returns undefined that means the range is
-        // currently "Everything", so we should set since to 0.
-        let clearRange = Sanitizer.getClearRange();
-        let since = clearRange ? clearRange[0] / 1000 : 0;
-        let options = {since};
+          // since will be the start of what is returned by Sanitizer.getClearRange
+          // divided by 1000 to convert to ms.
+          // If Sanitizer.getClearRange returns undefined that means the range is
+          // currently "Everything", so we should set since to 0.
+          let clearRange = Sanitizer.getClearRange();
+          let since = clearRange ? clearRange[0] / 1000 : 0;
+          let options = {since};
 
-        let dataToRemove = {};
-        let dataRemovalPermitted = {};
+          let dataToRemove = {};
+          let dataRemovalPermitted = {};
 
-        for (let item of PREF_LIST) {
-          // The property formData needs a different case than the
-          // formdata preference.
-          const name = item === "formdata" ? "formData" : item;
-          dataToRemove[name] = Preferences.get(`${PREF_DOMAIN}${item}`);
-          // Firefox doesn't have the same concept of dataRemovalPermitted
-          // as Chrome, so it will always be true.
-          dataRemovalPermitted[name] = true;
-        }
+          for (let item of PREF_LIST) {
+            // The property formData needs a different case than the
+            // formdata preference.
+            const name = item === "formdata" ? "formData" : item;
+            dataToRemove[name] = Preferences.get(`${PREF_DOMAIN}${item}`);
+            // Firefox doesn't have the same concept of dataRemovalPermitted
+            // as Chrome, so it will always be true.
+            dataRemovalPermitted[name] = true;
+          }
 
-        return Promise.resolve({options, dataToRemove, dataRemovalPermitted});
-      },
-      remove(options, dataToRemove) {
-        return doRemoval(options, dataToRemove, extension);
-      },
-      removeCache(options) {
-        return doRemoval(options, {cache: true});
+          return Promise.resolve({options, dataToRemove, dataRemovalPermitted});
+        },
+        remove(options, dataToRemove) {
+          return doRemoval(options, dataToRemove, extension);
+        },
+        removeCache(options) {
+          return doRemoval(options, {cache: true});
+        },
+        removeCookies(options) {
+          return doRemoval(options, {cookies: true});
+        },
+        removeDownloads(options) {
+          return doRemoval(options, {downloads: true});
+        },
+        removeFormData(options) {
+          return doRemoval(options, {formData: true});
+        },
+        removeHistory(options) {
+          return doRemoval(options, {history: true});
+        },
+        removePasswords(options) {
+          return doRemoval(options, {passwords: true});
+        },
+        removePluginData(options) {
+          return doRemoval(options, {pluginData: true});
+        },
       },
-      removeCookies(options) {
-        return doRemoval(options, {cookies: true});
-      },
-      removeDownloads(options) {
-        return doRemoval(options, {downloads: true});
-      },
-      removeFormData(options) {
-        return doRemoval(options, {formData: true});
-      },
-      removeHistory(options) {
-        return doRemoval(options, {history: true});
-      },
-      removePasswords(options) {
-        return doRemoval(options, {passwords: true});
-      },
-      removePluginData(options) {
-        return doRemoval(options, {pluginData: true});
-      },
-    },
-  };
-});
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-c-browser.js
@@ -0,0 +1,46 @@
+"use strict";
+
+extensions.registerModules({
+  devtools: {
+    url: "chrome://browser/content/ext-c-devtools.js",
+    scopes: ["devtools_child"],
+    paths: [
+      ["devtools"],
+    ],
+  },
+  devtools_inspectedWindow: {
+    url: "chrome://browser/content/ext-c-devtools-inspectedWindow.js",
+    scopes: ["devtools_child"],
+    paths: [
+      ["devtools", "inspectedWindow"],
+    ],
+  },
+  devtools_panels: {
+    url: "chrome://browser/content/ext-c-devtools-panels.js",
+    scopes: ["devtools_child"],
+    paths: [
+      ["devtools", "panels"],
+    ],
+  },
+  contextMenus: {
+    url: "chrome://browser/content/ext-c-contextMenus.js",
+    scopes: ["addon_child"],
+    paths: [
+      ["contextMenus"],
+    ],
+  },
+  omnibox: {
+    url: "chrome://browser/content/ext-c-omnibox.js",
+    scopes: ["addon_child"],
+    paths: [
+      ["omnibox"],
+    ],
+  },
+  tabs: {
+    url: "chrome://browser/content/ext-c-tabs.js",
+    scopes: ["addon_child"],
+    paths: [
+      ["tabs"],
+    ],
+  },
+});
--- a/browser/components/extensions/ext-c-contextMenus.js
+++ b/browser/components/extensions/ext-c-contextMenus.js
@@ -96,63 +96,65 @@ class ContextMenusClickPropHandler {
   // Removes all `onclick` handlers from this context.
   close() {
     for (let id of this.onclickMap.keys()) {
       this.unsetListener(id);
     }
   }
 }
 
-extensions.registerSchemaAPI("contextMenus", "addon_child", context => {
-  let onClickedProp = new ContextMenusClickPropHandler(context);
+this.contextMenus = class extends ExtensionAPI {
+  getAPI(context) {
+    let onClickedProp = new ContextMenusClickPropHandler(context);
 
-  return {
-    contextMenus: {
-      create(createProperties, callback) {
-        if (createProperties.id === null) {
-          createProperties.id = ++gNextMenuItemID;
-        }
-        let {onclick} = createProperties;
-        delete createProperties.onclick;
-        context.childManager.callParentAsyncFunction("contextMenus.createInternal", [
-          createProperties,
-        ]).then(() => {
-          if (onclick) {
-            onClickedProp.setListener(createProperties.id, onclick);
+    return {
+      contextMenus: {
+        create(createProperties, callback) {
+          if (createProperties.id === null) {
+            createProperties.id = ++gNextMenuItemID;
           }
-          if (callback) {
-            callback();
-          }
-        });
-        return createProperties.id;
-      },
+          let {onclick} = createProperties;
+          delete createProperties.onclick;
+          context.childManager.callParentAsyncFunction("contextMenus.createInternal", [
+            createProperties,
+          ]).then(() => {
+            if (onclick) {
+              onClickedProp.setListener(createProperties.id, onclick);
+            }
+            if (callback) {
+              callback();
+            }
+          });
+          return createProperties.id;
+        },
 
-      update(id, updateProperties) {
-        let {onclick} = updateProperties;
-        delete updateProperties.onclick;
-        return context.childManager.callParentAsyncFunction("contextMenus.update", [
-          id,
-          updateProperties,
-        ]).then(() => {
-          if (onclick) {
-            onClickedProp.setListener(id, onclick);
-          } else if (onclick === null) {
-            onClickedProp.unsetListenerFromAnyContext(id);
-          }
-          // else onclick is not set so it should not be changed.
-        });
-      },
+        update(id, updateProperties) {
+          let {onclick} = updateProperties;
+          delete updateProperties.onclick;
+          return context.childManager.callParentAsyncFunction("contextMenus.update", [
+            id,
+            updateProperties,
+          ]).then(() => {
+            if (onclick) {
+              onClickedProp.setListener(id, onclick);
+            } else if (onclick === null) {
+              onClickedProp.unsetListenerFromAnyContext(id);
+            }
+            // else onclick is not set so it should not be changed.
+          });
+        },
 
-      remove(id) {
-        onClickedProp.unsetListenerFromAnyContext(id);
-        return context.childManager.callParentAsyncFunction("contextMenus.remove", [
-          id,
-        ]);
-      },
+        remove(id) {
+          onClickedProp.unsetListenerFromAnyContext(id);
+          return context.childManager.callParentAsyncFunction("contextMenus.remove", [
+            id,
+          ]);
+        },
 
-      removeAll() {
-        onClickedProp.deleteAllListenersFromExtension();
+        removeAll() {
+          onClickedProp.deleteAllListenersFromExtension();
 
-        return context.childManager.callParentAsyncFunction("contextMenus.removeAll", []);
+          return context.childManager.callParentAsyncFunction("contextMenus.removeAll", []);
+        },
       },
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/ext-c-devtools-inspectedWindow.js
+++ b/browser/components/extensions/ext-c-devtools-inspectedWindow.js
@@ -1,22 +1,24 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-extensions.registerSchemaAPI("devtools.inspectedWindow", "devtools_child", context => {
-  // `devtoolsToolboxInfo` is received from the child process when the root devtools view
-  // has been created, and every sub-frame of that top level devtools frame will
-  // receive the same information when the context has been created from the
-  // `ExtensionChild.createExtensionContext` method.
-  let tabId = (context.devtoolsToolboxInfo &&
-               context.devtoolsToolboxInfo.inspectedWindowTabId);
+this.devtools_inspectedWindow = class extends ExtensionAPI {
+  getAPI(context) {
+    // `devtoolsToolboxInfo` is received from the child process when the root devtools view
+    // has been created, and every sub-frame of that top level devtools frame will
+    // receive the same information when the context has been created from the
+    // `ExtensionChild.createExtensionContext` method.
+    let tabId = (context.devtoolsToolboxInfo &&
+                 context.devtoolsToolboxInfo.inspectedWindowTabId);
 
-  return {
-    devtools: {
-      inspectedWindow: {
-        get tabId() {
-          return tabId;
+    return {
+      devtools: {
+        inspectedWindow: {
+          get tabId() {
+            return tabId;
+          },
         },
       },
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/ext-c-devtools-panels.js
+++ b/browser/components/extensions/ext-c-devtools-panels.js
@@ -1,21 +1,20 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+                                  "resource://devtools/shared/event-emitter.js");
 
-const {
+var {
   promiseDocumentLoaded,
   SingletonEventManager,
 } = ExtensionUtils;
 
-const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
-
 /**
  * Represents an addon devtools panel in the child process.
  *
  * @param {DevtoolsExtensionContext}
  *   A devtools extension context running in a child process.
  * @param {object} panelOptions
  * @param {string} panelOptions.id
  *   The id of the addon devtools panel registered in the main process.
@@ -127,29 +126,31 @@ class ChildDevToolsPanel extends EventEm
     this.mm.removeMessageListener("Extension:DevToolsPanelShown", this);
     this.mm.removeMessageListener("Extension:DevToolsPanelHidden", this);
 
     this._panelContext = null;
     this.context = null;
   }
 }
 
-extensions.registerSchemaAPI("devtools.panels", "devtools_child", context => {
-  return {
-    devtools: {
-      panels: {
-        create(title, icon, url) {
-          return context.cloneScope.Promise.resolve().then(async () => {
-            const panelId = await context.childManager.callParentAsyncFunction(
-              "devtools.panels.create", [title, icon, url]);
+this.devtools_panels = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      devtools: {
+        panels: {
+          create(title, icon, url) {
+            return context.cloneScope.Promise.resolve().then(async () => {
+              const panelId = await context.childManager.callParentAsyncFunction(
+                "devtools.panels.create", [title, icon, url]);
 
-            const devtoolsPanel = new ChildDevToolsPanel(context, {id: panelId});
+              const devtoolsPanel = new ChildDevToolsPanel(context, {id: panelId});
 
-            const devtoolsPanelAPI = Cu.cloneInto(devtoolsPanel.api(),
-                                                  context.cloneScope,
-                                                  {cloneFunctions: true});
-            return devtoolsPanelAPI;
-          });
+              const devtoolsPanelAPI = Cu.cloneInto(devtoolsPanel.api(),
+                                                    context.cloneScope,
+                                                    {cloneFunctions: true});
+              return devtoolsPanelAPI;
+            });
+          },
         },
       },
-    },
-  };
-});
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-c-devtools.js
@@ -0,0 +1,13 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+this.devtools = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      devtools: {},
+    };
+  }
+};
+
--- a/browser/components/extensions/ext-c-omnibox.js
+++ b/browser/components/extensions/ext-c-omnibox.js
@@ -1,30 +1,30 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
 var {
   SingletonEventManager,
 } = ExtensionUtils;
 
-extensions.registerSchemaAPI("omnibox", "addon_child", context => {
-  return {
-    omnibox: {
-      onInputChanged: new SingletonEventManager(context, "omnibox.onInputChanged", fire => {
-        let listener = (text, id) => {
-          fire.asyncWithoutClone(text, suggestions => {
-            context.childManager.callParentFunctionNoReturn("omnibox_internal.addSuggestions", [
-              id,
-              suggestions,
-            ]);
-          });
-        };
-        context.childManager.getParentEvent("omnibox_internal.onInputChanged").addListener(listener);
-        return () => {
-          context.childManager.getParentEvent("omnibox_internal.onInputChanged").removeListener(listener);
-        };
-      }).api(),
-    },
-  };
-});
+this.omnibox = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      omnibox: {
+        onInputChanged: new SingletonEventManager(context, "omnibox.onInputChanged", fire => {
+          let listener = (text, id) => {
+            fire.asyncWithoutClone(text, suggestions => {
+              context.childManager.callParentFunctionNoReturn("omnibox_internal.addSuggestions", [
+                id,
+                suggestions,
+              ]);
+            });
+          };
+          context.childManager.getParentEvent("omnibox_internal.onInputChanged").addListener(listener);
+          return () => {
+            context.childManager.getParentEvent("omnibox_internal.onInputChanged").removeListener(listener);
+          };
+        }).api(),
+      },
+    };
+  }
+};
--- a/browser/components/extensions/ext-c-tabs.js
+++ b/browser/components/extensions/ext-c-tabs.js
@@ -1,35 +1,37 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-extensions.registerSchemaAPI("tabs", "addon_child", context => {
-  return {
-    tabs: {
-      connect: function(tabId, connectInfo) {
-        let name = "";
-        if (connectInfo && connectInfo.name !== null) {
-          name = connectInfo.name;
-        }
-        let recipient = {
-          extensionId: context.extension.id,
-          tabId,
-        };
-        if (connectInfo && connectInfo.frameId !== null) {
-          recipient.frameId = connectInfo.frameId;
-        }
-        return context.messenger.connect(context.messageManager, name, recipient);
+this.tabs = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      tabs: {
+        connect: function(tabId, connectInfo) {
+          let name = "";
+          if (connectInfo && connectInfo.name !== null) {
+            name = connectInfo.name;
+          }
+          let recipient = {
+            extensionId: context.extension.id,
+            tabId,
+          };
+          if (connectInfo && connectInfo.frameId !== null) {
+            recipient.frameId = connectInfo.frameId;
+          }
+          return context.messenger.connect(context.messageManager, name, recipient);
+        },
+
+        sendMessage: function(tabId, message, options, responseCallback) {
+          let recipient = {
+            extensionId: context.extension.id,
+            tabId: tabId,
+          };
+          if (options && options.frameId !== null) {
+            recipient.frameId = options.frameId;
+          }
+          return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
+        },
       },
-
-      sendMessage: function(tabId, message, options, responseCallback) {
-        let recipient = {
-          extensionId: context.extension.id,
-          tabId: tabId,
-        };
-        if (options && options.frameId !== null) {
-          recipient.frameId = options.frameId;
-        }
-        return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
-      },
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/ext-chrome-settings-overrides.js
+++ b/browser/components/extensions/ext-chrome-settings-overrides.js
@@ -1,28 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPreferencesManager",
                                   "resource://gre/modules/ExtensionPreferencesManager.jsm");
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_chrome_settings_overrides", (type, directive, extension, manifest) => {
-  if (manifest.chrome_settings_overrides.homepage) {
-    ExtensionPreferencesManager.setSetting(extension, "homepage_override",
-                                           manifest.chrome_settings_overrides.homepage);
+this.chrome_settings_overrides = class extends ExtensionAPI {
+  onManifestEntry(entryName) {
+    let {extension} = this;
+    let {manifest} = extension;
+
+    if (manifest.chrome_settings_overrides.homepage) {
+      ExtensionPreferencesManager.setSetting(extension, "homepage_override",
+                                             manifest.chrome_settings_overrides.homepage);
+    }
   }
-});
-/* eslint-enable mozilla/balanced-listeners */
+};
 
 ExtensionPreferencesManager.addSetting("homepage_override", {
   prefNames: [
     "browser.startup.homepage",
   ],
   setCallback(value) {
     return {
       "browser.startup.homepage": value,
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -1,72 +1,71 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://devtools/shared/event-emitter.js");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
 var {
   SingletonEventManager,
   PlatformInfo,
 } = ExtensionUtils;
 
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-// WeakMap[Extension -> CommandList]
-var commandsMap = new WeakMap();
+this.commands = class extends ExtensionAPI {
+  onManifestEntry(entryName) {
+    let {extension} = this;
 
-function CommandList(manifest, extension) {
-  this.extension = extension;
-  this.id = makeWidgetId(extension.id);
-  this.windowOpenListener = null;
+    this.id = makeWidgetId(extension.id);
+    this.windowOpenListener = null;
+
+    // Map[{String} commandName -> {Object} commandProperties]
+    this.commands = this.loadCommandsFromManifest(this.extension.manifest);
 
-  // Map[{String} commandName -> {Object} commandProperties]
-  this.commands = this.loadCommandsFromManifest(manifest);
-
-  // WeakMap[Window -> <xul:keyset>]
-  this.keysetsMap = new WeakMap();
+    // WeakMap[Window -> <xul:keyset>]
+    this.keysetsMap = new WeakMap();
 
-  this.register();
-  EventEmitter.decorate(this);
-}
+    this.register();
+    EventEmitter.decorate(this);
+  }
 
-CommandList.prototype = {
+  onShutdown(reason) {
+    this.unregister();
+  }
+
   /**
    * Registers the commands to all open windows and to any which
    * are later created.
    */
   register() {
     for (let window of windowTracker.browserWindows()) {
       this.registerKeysToDocument(window);
     }
 
     this.windowOpenListener = (window) => {
       if (!this.keysetsMap.has(window)) {
         this.registerKeysToDocument(window);
       }
     };
 
     windowTracker.addOpenListener(this.windowOpenListener);
-  },
+  }
 
   /**
    * Unregisters the commands from all open windows and stops commands
    * from being registered to windows which are later created.
    */
   unregister() {
     for (let window of windowTracker.browserWindows()) {
       if (this.keysetsMap.has(window)) {
         this.keysetsMap.get(window).remove();
       }
     }
 
     windowTracker.removeOpenListener(this.windowOpenListener);
-  },
+  }
 
   /**
    * Creates a Map from commands for each command in the manifest.commands object.
    *
    * @param {Object} manifest The manifest JSON object.
    * @returns {Map<string, object>}
    */
   loadCommandsFromManifest(manifest) {
@@ -79,17 +78,17 @@ CommandList.prototype = {
       let shortcut = suggested_key[os] || suggested_key.default;
       shortcut = shortcut ? shortcut.replace(/\s+/g, "") : null;
       commands.set(name, {
         description: command.description,
         shortcut,
       });
     }
     return commands;
-  },
+  }
 
   /**
    * Registers the commands to a document.
    * @param {ChromeWindow} window The XUL window to insert the Keyset.
    */
   registerKeysToDocument(window) {
     let doc = window.document;
     let keyset = doc.createElementNS(XUL_NS, "keyset");
@@ -97,17 +96,17 @@ CommandList.prototype = {
     this.commands.forEach((command, name) => {
       if (command.shortcut) {
         let keyElement = this.buildKey(doc, name, command.shortcut);
         keyset.appendChild(keyElement);
       }
     });
     doc.documentElement.appendChild(keyset);
     this.keysetsMap.set(window, keyset);
-  },
+  }
 
   /**
    * Builds a XUL Key element and attaches an onCommand listener which
    * emits a command event with the provided name when fired.
    *
    * @param {Document} doc The XUL document.
    * @param {string} name The name of the command.
    * @param {string} shortcut The shortcut provided in the manifest.
@@ -142,17 +141,17 @@ CommandList.prototype = {
       if (action) {
         let win = event.target.ownerGlobal;
         action.triggerAction(win);
       }
     });
     /* eslint-enable mozilla/balanced-listeners */
 
     return keyElement;
-  },
+  }
 
   /**
    * Builds a XUL Key element from the provided shortcut.
    *
    * @param {Document} doc The XUL document.
    * @param {string} shortcut The shortcut provided in the manifest.
    *
    * @see https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/key
@@ -172,34 +171,34 @@ CommandList.prototype = {
     if (/^[A-Z0-9]$/.test(chromeKey)) {
       // We use the key attribute for all single digits and characters.
       keyElement.setAttribute("key", chromeKey);
     } else {
       keyElement.setAttribute("keycode", this.getKeycodeAttribute(chromeKey));
     }
 
     return keyElement;
-  },
+  }
 
   /**
    * Determines the corresponding XUL keycode from the given chrome key.
    *
    * For example:
    *
    *    input     |  output
    *    ---------------------------------------
    *    "PageUP"  |  "VK_PAGE_UP"
    *    "Delete"  |  "VK_DELETE"
    *
    * @param {string} chromeKey The chrome key (e.g. "PageUp", "Space", ...)
    * @returns {string} The constructed value for the Key's 'keycode' attribute.
    */
   getKeycodeAttribute(chromeKey) {
     return `VK${chromeKey.replace(/([A-Z])/g, "_$&").toUpperCase()}`;
-  },
+  }
 
   /**
    * Determines the corresponding XUL modifiers from the chrome modifiers.
    *
    * For example:
    *
    *    input             |   output
    *    ---------------------------------------
@@ -215,52 +214,36 @@ CommandList.prototype = {
       "Command": "accel",
       "Ctrl": "accel",
       "MacCtrl": "control",
       "Shift": "shift",
     };
     return Array.from(chromeModifiers, modifier => {
       return modifiersMap[modifier];
     }).join(" ");
-  },
-};
-
-
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_commands", (type, directive, extension, manifest) => {
-  commandsMap.set(extension, new CommandList(manifest, extension));
-});
-
-extensions.on("shutdown", (type, extension) => {
-  let commandsList = commandsMap.get(extension);
-  if (commandsList) {
-    commandsList.unregister();
-    commandsMap.delete(extension);
   }
-});
-/* eslint-enable mozilla/balanced-listeners */
 
-extensions.registerSchemaAPI("commands", "addon_parent", context => {
-  let {extension} = context;
-  return {
-    commands: {
-      getAll() {
-        let commands = commandsMap.get(extension).commands;
-        return Promise.resolve(Array.from(commands, ([name, command]) => {
-          return ({
-            name,
-            description: command.description,
-            shortcut: command.shortcut,
-          });
-        }));
+  getAPI(context) {
+    return {
+      commands: {
+        getAll: () => {
+          let commands = this.commands;
+          return Promise.resolve(Array.from(commands, ([name, command]) => {
+            return ({
+              name,
+              description: command.description,
+              shortcut: command.shortcut,
+            });
+          }));
+        },
+        onCommand: new SingletonEventManager(context, "commands.onCommand", fire => {
+          let listener = (eventName, commandName) => {
+            fire.async(commandName);
+          };
+          this.on("command", listener);
+          return () => {
+            this.off("command", listener);
+          };
+        }).api(),
       },
-      onCommand: new SingletonEventManager(context, "commands.onCommand", fire => {
-        let listener = (eventName, commandName) => {
-          fire.async(commandName);
-        };
-        commandsMap.get(extension).on("command", listener);
-        return () => {
-          commandsMap.get(extension).off("command", listener);
-        };
-      }).api(),
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -1,17 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/MatchPattern.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
 
 var {
   ExtensionError,
   IconDetails,
   SingletonEventManager,
 } = ExtensionUtils;
 
 const ACTION_MENU_TOP_LEVEL_LIMIT = 6;
@@ -238,19 +238,19 @@ var gMenuBuilder = {
       info.modifiers = Object.keys(map).filter(key => event[key]).map(key => map[key]);
       if (event.ctrlKey && AppConstants.platform === "macosx") {
         info.modifiers.push("MacCtrl");
       }
 
       // Allow context menu's to open various actions supported in webext prior
       // to notifying onclicked.
       let actionFor = {
-        _execute_page_action: pageActionFor,
-        _execute_browser_action: browserActionFor,
-        _execute_sidebar_action: sidebarActionFor,
+        _execute_page_action: global.pageActionFor,
+        _execute_browser_action: global.browserActionFor,
+        _execute_sidebar_action: global.sidebarActionFor,
       }[item.command];
       if (actionFor) {
         let win = event.target.ownerGlobal;
         actionFor(item.extension).triggerAction(win);
       }
 
       item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
     });
@@ -590,71 +590,75 @@ const contextMenuTracker = {
       const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
       const pageUrl = tab.linkedBrowser.currentURI.spec;
       gMenuBuilder.build({menu, tab, pageUrl, onTab: true});
     }
   },
 };
 
 var gExtensionCount = 0;
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("startup", (type, extension) => {
-  gContextMenuMap.set(extension, new Map());
-  if (++gExtensionCount == 1) {
-    contextMenuTracker.register();
-  }
-});
+
+this.contextMenus = class extends ExtensionAPI {
+  onShutdown(reason) {
+    let {extension} = this;
 
-extensions.on("shutdown", (type, extension) => {
-  gContextMenuMap.delete(extension);
-  gRootItems.delete(extension);
-  if (--gExtensionCount == 0) {
-    contextMenuTracker.unregister();
+    if (gContextMenuMap.has(extension)) {
+      gContextMenuMap.delete(extension);
+      gRootItems.delete(extension);
+      if (--gExtensionCount == 0) {
+        contextMenuTracker.unregister();
+      }
+    }
   }
-});
-/* eslint-enable mozilla/balanced-listeners */
+
+  getAPI(context) {
+    let {extension} = context;
+
+    gContextMenuMap.set(extension, new Map());
+    if (++gExtensionCount == 1) {
+      contextMenuTracker.register();
+    }
 
-extensions.registerSchemaAPI("contextMenus", "addon_parent", context => {
-  let {extension} = context;
-  return {
-    contextMenus: {
-      createInternal: function(createProperties) {
-        // Note that the id is required by the schema. If the addon did not set
-        // it, the implementation of contextMenus.create in the child should
-        // have added it.
-        let menuItem = new MenuItem(extension, createProperties);
-        gContextMenuMap.get(extension).set(menuItem.id, menuItem);
-      },
+    return {
+      contextMenus: {
+        createInternal: function(createProperties) {
+          // Note that the id is required by the schema. If the addon did not set
+          // it, the implementation of contextMenus.create in the child should
+          // have added it.
+          let menuItem = new MenuItem(extension, createProperties);
+          gContextMenuMap.get(extension).set(menuItem.id, menuItem);
+        },
 
-      update: function(id, updateProperties) {
-        let menuItem = gContextMenuMap.get(extension).get(id);
-        if (menuItem) {
-          menuItem.setProps(updateProperties);
-        }
-      },
+        update: function(id, updateProperties) {
+          let menuItem = gContextMenuMap.get(extension).get(id);
+          if (menuItem) {
+            menuItem.setProps(updateProperties);
+          }
+        },
 
-      remove: function(id) {
-        let menuItem = gContextMenuMap.get(extension).get(id);
-        if (menuItem) {
-          menuItem.remove();
-        }
-      },
+        remove: function(id) {
+          let menuItem = gContextMenuMap.get(extension).get(id);
+          if (menuItem) {
+            menuItem.remove();
+          }
+        },
 
-      removeAll: function() {
-        let root = gRootItems.get(extension);
-        if (root) {
-          root.remove();
-        }
-      },
+        removeAll: function() {
+          let root = gRootItems.get(extension);
+          if (root) {
+            root.remove();
+          }
+        },
 
-      onClicked: new SingletonEventManager(context, "contextMenus.onClicked", fire => {
-        let listener = (event, info, tab) => {
-          fire.async(info, tab);
-        };
+        onClicked: new SingletonEventManager(context, "contextMenus.onClicked", fire => {
+          let listener = (event, info, tab) => {
+            fire.async(info, tab);
+          };
 
-        extension.on("webext-contextmenu-menuitem-click", listener);
-        return () => {
-          extension.off("webext-contextmenu-menuitem-click", listener);
-        };
-      }).api(),
-    },
-  };
-});
+          extension.on("webext-contextmenu-menuitem-click", listener);
+          return () => {
+            extension.off("webext-contextmenu-menuitem-click", listener);
+          };
+        }).api(),
+      },
+    };
+  }
+};
--- a/browser/components/extensions/ext-devtools-inspectedWindow.js
+++ b/browser/components/extensions/ext-devtools-inspectedWindow.js
@@ -1,64 +1,64 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 /* global getDevToolsTargetForContext */
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
-const {
+var {
   SpreadArgs,
 } = ExtensionUtils;
 
-extensions.registerSchemaAPI("devtools.inspectedWindow", "devtools_parent", context => {
-  const {
-    WebExtensionInspectedWindowFront,
-  } = require("devtools/shared/fronts/webextension-inspected-window");
+this.devtools_inspectedWindow = class extends ExtensionAPI {
+  getAPI(context) {
+    const {
+      WebExtensionInspectedWindowFront,
+    } = require("devtools/shared/fronts/webextension-inspected-window");
 
-  // Lazily retrieve and store an inspectedWindow actor front per child context.
-  let waitForInspectedWindowFront;
-  async function getInspectedWindowFront() {
-    // If there is not yet a front instance, then a lazily cloned target for the context is
-    // retrieved using the DevtoolsParentContextsManager helper (which is an asynchronous operation,
-    // because the first time that the target has been cloned, it is not ready to be used to create
-    // the front instance until it is connected to the remote debugger successfully).
-    const clonedTarget = await getDevToolsTargetForContext(context);
-    return new WebExtensionInspectedWindowFront(clonedTarget.client, clonedTarget.form);
-  }
+    // Lazily retrieve and store an inspectedWindow actor front per child context.
+    let waitForInspectedWindowFront;
+    async function getInspectedWindowFront() {
+      // If there is not yet a front instance, then a lazily cloned target for the context is
+      // retrieved using the DevtoolsParentContextsManager helper (which is an asynchronous operation,
+      // because the first time that the target has been cloned, it is not ready to be used to create
+      // the front instance until it is connected to the remote debugger successfully).
+      const clonedTarget = await getDevToolsTargetForContext(context);
+      return new WebExtensionInspectedWindowFront(clonedTarget.client, clonedTarget.form);
+    }
 
-  // TODO(rpl): retrive a more detailed callerInfo object, like the filename and
-  // lineNumber of the actual extension called, in the child process.
-  const callerInfo = {
-    addonId: context.extension.id,
-    url: context.extension.baseURI.spec,
-  };
+    // TODO(rpl): retrive a more detailed callerInfo object, like the filename and
+    // lineNumber of the actual extension called, in the child process.
+    const callerInfo = {
+      addonId: context.extension.id,
+      url: context.extension.baseURI.spec,
+    };
 
-  return {
-    devtools: {
-      inspectedWindow: {
-        async eval(expression, options) {
-          if (!waitForInspectedWindowFront) {
-            waitForInspectedWindowFront = getInspectedWindowFront();
-          }
+    return {
+      devtools: {
+        inspectedWindow: {
+          async eval(expression, options) {
+            if (!waitForInspectedWindowFront) {
+              waitForInspectedWindowFront = getInspectedWindowFront();
+            }
 
-          const front = await waitForInspectedWindowFront;
-          return front.eval(callerInfo, expression, options || {}).then(evalResult => {
-            // TODO(rpl): check for additional undocumented behaviors on chrome
-            // (e.g. if we should also print error to the console or set lastError?).
-            return new SpreadArgs([evalResult.value, evalResult.exceptionInfo]);
-          });
-        },
-        async reload(options) {
-          const {ignoreCache, userAgent, injectedScript} = options || {};
+            const front = await waitForInspectedWindowFront;
+            return front.eval(callerInfo, expression, options || {}).then(evalResult => {
+              // TODO(rpl): check for additional undocumented behaviors on chrome
+              // (e.g. if we should also print error to the console or set lastError?).
+              return new SpreadArgs([evalResult.value, evalResult.exceptionInfo]);
+            });
+          },
+          async reload(options) {
+            const {ignoreCache, userAgent, injectedScript} = options || {};
 
-          if (!waitForInspectedWindowFront) {
-            waitForInspectedWindowFront = getInspectedWindowFront();
-          }
+            if (!waitForInspectedWindowFront) {
+              waitForInspectedWindowFront = getInspectedWindowFront();
+            }
 
-          const front = await waitForInspectedWindowFront;
-          front.reload(callerInfo, {ignoreCache, userAgent, injectedScript});
+            const front = await waitForInspectedWindowFront;
+            front.reload(callerInfo, {ignoreCache, userAgent, injectedScript});
+          },
         },
       },
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/ext-devtools-network.js
+++ b/browser/components/extensions/ext-devtools-network.js
@@ -1,35 +1,33 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
-const {
+var {
   SingletonEventManager,
 } = ExtensionUtils;
 
-extensions.registerSchemaAPI("devtools.network", "devtools_parent", (context) => {
-  return {
-    devtools: {
-      network: {
-        onNavigated: new SingletonEventManager(context, "devtools.onNavigated", fire => {
-          let listener = (event, data) => {
-            fire.async(data.url);
-          };
+this.devtools_network = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      devtools: {
+        network: {
+          onNavigated: new SingletonEventManager(context, "devtools.onNavigated", fire => {
+            let listener = (event, data) => {
+              fire.async(data.url);
+            };
 
-          let targetPromise = getDevToolsTargetForContext(context);
-          targetPromise.then(target => {
-            target.on("navigate", listener);
-          });
-          return () => {
+            let targetPromise = getDevToolsTargetForContext(context);
             targetPromise.then(target => {
-              target.off("navigate", listener);
+              target.on("navigate", listener);
             });
-          };
-        }).api(),
+            return () => {
+              targetPromise.then(target => {
+                target.off("navigate", listener);
+              });
+            };
+          }).api(),
+        },
       },
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -1,30 +1,27 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
 
-const {
+var {
   watchExtensionProxyContextLoad,
 } = ExtensionParent;
 
-const {
+var {
   IconDetails,
   promiseEvent,
 } = ExtensionUtils;
 
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
  * Represents an addon devtools panel in the main process.
  *
  * @param {ExtensionChildProxyContext} context
  *        A devtools extension proxy context running in a main process.
  * @param {object} options
  * @param {string} options.id
@@ -223,40 +220,42 @@ class ParentDevToolsPanel {
 
     this.context = null;
     this.toolbox = null;
     this.waitTopLevelContext = null;
     this._resolveTopLevelContext = null;
   }
 }
 
-extensions.registerSchemaAPI("devtools.panels", "devtools_parent", context => {
-  // An incremental "per context" id used in the generated devtools panel id.
-  let nextPanelId = 0;
+this.devtools_panels = class extends ExtensionAPI {
+  getAPI(context) {
+    // An incremental "per context" id used in the generated devtools panel id.
+    let nextPanelId = 0;
 
-  return {
-    devtools: {
-      panels: {
-        create(title, icon, url) {
-          // Get a fallback icon from the manifest data.
-          if (icon === "" && context.extension.manifest.icons) {
-            const iconInfo = IconDetails.getPreferredIcon(context.extension.manifest.icons,
-                                                          context.extension, 128);
-            icon = iconInfo ? iconInfo.icon : "";
-          }
+    return {
+      devtools: {
+        panels: {
+          create(title, icon, url) {
+            // Get a fallback icon from the manifest data.
+            if (icon === "" && context.extension.manifest.icons) {
+              const iconInfo = IconDetails.getPreferredIcon(context.extension.manifest.icons,
+                                                            context.extension, 128);
+              icon = iconInfo ? iconInfo.icon : "";
+            }
 
-          icon = context.extension.baseURI.resolve(icon);
-          url = context.extension.baseURI.resolve(url);
+            icon = context.extension.baseURI.resolve(icon);
+            url = context.extension.baseURI.resolve(url);
 
-          const baseId = `${context.extension.id}-${context.contextId}-${nextPanelId++}`;
-          const id = `${makeWidgetId(baseId)}-devtools-panel`;
+            const baseId = `${context.extension.id}-${context.contextId}-${nextPanelId++}`;
+            const id = `${makeWidgetId(baseId)}-devtools-panel`;
 
-          new ParentDevToolsPanel(context, {title, icon, url, id});
+            new ParentDevToolsPanel(context, {title, icon, url, id});
 
-          // Resolved to the devtools panel id into the child addon process,
-          // where it will be used to identify the messages related
-          // to the panel API onShown/onHidden events.
-          return Promise.resolve(id);
+            // Resolved to the devtools panel id into the child addon process,
+            // where it will be used to identify the messages related
+            // to the panel API onShown/onHidden events.
+            return Promise.resolve(id);
+          },
         },
       },
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/ext-devtools.js
+++ b/browser/components/extensions/ext-devtools.js
@@ -11,17 +11,17 @@
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource://devtools/client/framework/gDevTools.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 
-const {
+var {
   HiddenExtensionPage,
   watchExtensionProxyContextLoad,
 } = ExtensionParent;
 
 // Map[extension -> DevToolsPageDefinition]
 let devtoolsPageDefinitionMap = new Map();
 
 let initDevTools;
@@ -244,24 +244,24 @@ class DevToolsPageDefinition {
     if (this.devtoolsPageForTarget.size > 0) {
       throw new Error(
         `Leaked ${this.devtoolsPageForTarget.size} DevToolsPage instances in devtoolsPageForTarget Map`
       );
     }
   }
 }
 
-/* eslint-disable mozilla/balanced-listeners */
 
 let devToolsInitialized = false;
 initDevTools = function() {
   if (devToolsInitialized) {
     return;
   }
 
+  /* eslint-disable mozilla/balanced-listeners */
   // Create a devtools page context for a new opened toolbox,
   // based on the registered devtools_page definitions.
   gDevTools.on("toolbox-created", (evt, toolbox) => {
     if (!toolbox.target.isLocalTab) {
       // Only local tabs are currently supported (See Bug 1304378 for additional details
       // related to remote targets support).
       let msg = `Ignoring DevTools Toolbox for target "${toolbox.target.toString()}": ` +
                 `"${toolbox.target.name}" ("${toolbox.target.url}"). ` +
@@ -287,27 +287,40 @@ initDevTools = function() {
       // related to remote targets support).
       return;
     }
 
     for (let devtoolsPageDefinition of devtoolsPageDefinitionMap.values()) {
       devtoolsPageDefinition.shutdownForTarget(target);
     }
   });
+  /* eslint-enable mozilla/balanced-listeners */
 
   devToolsInitialized = true;
 };
 
-// Create and register a new devtools_page definition as specified in the
-// "devtools_page" property in the extension manifest.
-extensions.on("manifest_devtools_page", (type, directive, extension, manifest) => {
-  let devtoolsPageDefinition = new DevToolsPageDefinition(extension, manifest[directive]);
-  devtoolsPageDefinitionMap.set(extension, devtoolsPageDefinition);
-});
+this.devtools = class extends ExtensionAPI {
+  onManifestEntry(entryName) {
+    let {extension} = this;
+    let {manifest} = extension;
+
+    // Create and register a new devtools_page definition as specified in the
+    // "devtools_page" property in the extension manifest.
+    let devtoolsPageDefinition = new DevToolsPageDefinition(extension, manifest.devtools_page);
+    devtoolsPageDefinitionMap.set(extension, devtoolsPageDefinition);
+  }
+
+  onShutdown(reason) {
+    let {extension} = this;
 
-// Destroy the registered devtools_page definition on extension shutdown.
-extensions.on("shutdown", (type, extension) => {
-  if (devtoolsPageDefinitionMap.has(extension)) {
-    devtoolsPageDefinitionMap.get(extension).shutdown();
-    devtoolsPageDefinitionMap.delete(extension);
+    // Destroy the registered devtools_page definition on extension shutdown.
+    if (devtoolsPageDefinitionMap.has(extension)) {
+      devtoolsPageDefinitionMap.get(extension).shutdown();
+      devtoolsPageDefinitionMap.delete(extension);
+    }
   }
-});
-/* eslint-enable mozilla/balanced-listeners */
+
+  getAPI(context) {
+    return {
+      devtools: {},
+    };
+  }
+};
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -1,24 +1,18 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
-                                  "resource://devtools/shared/event-emitter.js");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 
-const {
+var {
   normalizeTime,
   SingletonEventManager,
 } = ExtensionUtils;
 
 let nsINavHistoryService = Ci.nsINavHistoryService;
 const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
   ["link", nsINavHistoryService.TRANSITION_LINK],
   ["typed", nsINavHistoryService.TRANSITION_TYPED],
@@ -127,133 +121,135 @@ function getObserver() {
       },
     };
     EventEmitter.decorate(_observer);
     PlacesUtils.history.addObserver(_observer, false);
   }
   return _observer;
 }
 
-extensions.registerSchemaAPI("history", "addon_parent", context => {
-  return {
-    history: {
-      addUrl: function(details) {
-        let transition, date;
-        try {
-          transition = getTransitionType(details.transition);
-        } catch (error) {
-          return Promise.reject({message: error.message});
-        }
-        if (details.visitTime) {
-          date = normalizeTime(details.visitTime);
-        }
-        let pageInfo = {
-          title: details.title,
-          url: details.url,
-          visits: [
-            {
-              transition,
-              date,
-            },
-          ],
-        };
-        try {
-          return PlacesUtils.history.insert(pageInfo).then(() => undefined);
-        } catch (error) {
-          return Promise.reject({message: error.message});
-        }
-      },
+this.history = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      history: {
+        addUrl: function(details) {
+          let transition, date;
+          try {
+            transition = getTransitionType(details.transition);
+          } catch (error) {
+            return Promise.reject({message: error.message});
+          }
+          if (details.visitTime) {
+            date = normalizeTime(details.visitTime);
+          }
+          let pageInfo = {
+            title: details.title,
+            url: details.url,
+            visits: [
+              {
+                transition,
+                date,
+              },
+            ],
+          };
+          try {
+            return PlacesUtils.history.insert(pageInfo).then(() => undefined);
+          } catch (error) {
+            return Promise.reject({message: error.message});
+          }
+        },
 
-      deleteAll: function() {
-        return PlacesUtils.history.clear();
-      },
+        deleteAll: function() {
+          return PlacesUtils.history.clear();
+        },
 
-      deleteRange: function(filter) {
-        let newFilter = {
-          beginDate: normalizeTime(filter.startTime),
-          endDate: normalizeTime(filter.endTime),
-        };
-        // History.removeVisitsByFilter returns a boolean, but our API should return nothing
-        return PlacesUtils.history.removeVisitsByFilter(newFilter).then(() => undefined);
-      },
+        deleteRange: function(filter) {
+          let newFilter = {
+            beginDate: normalizeTime(filter.startTime),
+            endDate: normalizeTime(filter.endTime),
+          };
+          // History.removeVisitsByFilter returns a boolean, but our API should return nothing
+          return PlacesUtils.history.removeVisitsByFilter(newFilter).then(() => undefined);
+        },
 
-      deleteUrl: function(details) {
-        let url = details.url;
-        // History.remove returns a boolean, but our API should return nothing
-        return PlacesUtils.history.remove(url).then(() => undefined);
-      },
+        deleteUrl: function(details) {
+          let url = details.url;
+          // History.remove returns a boolean, but our API should return nothing
+          return PlacesUtils.history.remove(url).then(() => undefined);
+        },
 
-      search: function(query) {
-        let beginTime = (query.startTime == null) ?
-                          PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000) :
-                          PlacesUtils.toPRTime(normalizeTime(query.startTime));
-        let endTime = (query.endTime == null) ?
-                        Number.MAX_VALUE :
-                        PlacesUtils.toPRTime(normalizeTime(query.endTime));
-        if (beginTime > endTime) {
-          return Promise.reject({message: "The startTime cannot be after the endTime"});
-        }
+        search: function(query) {
+          let beginTime = (query.startTime == null) ?
+                            PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000) :
+                            PlacesUtils.toPRTime(normalizeTime(query.startTime));
+          let endTime = (query.endTime == null) ?
+                          Number.MAX_VALUE :
+                          PlacesUtils.toPRTime(normalizeTime(query.endTime));
+          if (beginTime > endTime) {
+            return Promise.reject({message: "The startTime cannot be after the endTime"});
+          }
 
-        let options = PlacesUtils.history.getNewQueryOptions();
-        options.sortingMode = options.SORT_BY_DATE_DESCENDING;
-        options.maxResults = query.maxResults || 100;
+          let options = PlacesUtils.history.getNewQueryOptions();
+          options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+          options.maxResults = query.maxResults || 100;
 
-        let historyQuery = PlacesUtils.history.getNewQuery();
-        historyQuery.searchTerms = query.text;
-        historyQuery.beginTime = beginTime;
-        historyQuery.endTime = endTime;
-        let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
-        let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToHistoryItem);
-        return Promise.resolve(results);
-      },
+          let historyQuery = PlacesUtils.history.getNewQuery();
+          historyQuery.searchTerms = query.text;
+          historyQuery.beginTime = beginTime;
+          historyQuery.endTime = endTime;
+          let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
+          let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToHistoryItem);
+          return Promise.resolve(results);
+        },
 
-      getVisits: function(details) {
-        let url = details.url;
-        if (!url) {
-          return Promise.reject({message: "A URL must be provided for getVisits"});
-        }
+        getVisits: function(details) {
+          let url = details.url;
+          if (!url) {
+            return Promise.reject({message: "A URL must be provided for getVisits"});
+          }
 
-        let options = PlacesUtils.history.getNewQueryOptions();
-        options.sortingMode = options.SORT_BY_DATE_DESCENDING;
-        options.resultType = options.RESULTS_AS_VISIT;
+          let options = PlacesUtils.history.getNewQueryOptions();
+          options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+          options.resultType = options.RESULTS_AS_VISIT;
 
-        let historyQuery = PlacesUtils.history.getNewQuery();
-        historyQuery.uri = NetUtil.newURI(url);
-        let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
-        let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
-        return Promise.resolve(results);
-      },
+          let historyQuery = PlacesUtils.history.getNewQuery();
+          historyQuery.uri = NetUtil.newURI(url);
+          let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
+          let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
+          return Promise.resolve(results);
+        },
 
-      onVisited: new SingletonEventManager(context, "history.onVisited", fire => {
-        let listener = (event, data) => {
-          fire.sync(data);
-        };
+        onVisited: new SingletonEventManager(context, "history.onVisited", fire => {
+          let listener = (event, data) => {
+            fire.sync(data);
+          };
 
-        getObserver().on("visited", listener);
-        return () => {
-          getObserver().off("visited", listener);
-        };
-      }).api(),
+          getObserver().on("visited", listener);
+          return () => {
+            getObserver().off("visited", listener);
+          };
+        }).api(),
 
-      onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => {
-        let listener = (event, data) => {
-          fire.sync(data);
-        };
+        onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => {
+          let listener = (event, data) => {
+            fire.sync(data);
+          };
 
-        getObserver().on("visitRemoved", listener);
-        return () => {
-          getObserver().off("visitRemoved", listener);
-        };
-      }).api(),
+          getObserver().on("visitRemoved", listener);
+          return () => {
+            getObserver().off("visitRemoved", listener);
+          };
+        }).api(),
 
-      onTitleChanged: new SingletonEventManager(context, "history.onTitleChanged", fire => {
-        let listener = (event, data) => {
-          fire.sync(data);
-        };
+        onTitleChanged: new SingletonEventManager(context, "history.onTitleChanged", fire => {
+          let listener = (event, data) => {
+            fire.sync(data);
+          };
 
-        getObserver().on("titleChanged", listener);
-        return () => {
-          getObserver().off("titleChanged", listener);
-        };
-      }).api(),
-    },
-  };
-});
+          getObserver().on("titleChanged", listener);
+          return () => {
+            getObserver().off("titleChanged", listener);
+          };
+        }).api(),
+      },
+    };
+  }
+};
--- a/browser/components/extensions/ext-omnibox.js
+++ b/browser/components/extensions/ext-omnibox.js
@@ -1,104 +1,96 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
                                   "resource://gre/modules/ExtensionSearchHandler.jsm");
 var {
   SingletonEventManager,
 } = ExtensionUtils;
 
-// WeakMap[extension -> keyword]
-let gKeywordMap = new WeakMap();
+this.omnibox = class extends ExtensionAPI {
+  onManifestEntry(entryName) {
+    let {extension} = this;
+    let {manifest} = extension;
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_omnibox", (type, directive, extension, manifest) => {
-  let keyword = manifest.omnibox.keyword;
-  try {
-    // This will throw if the keyword is already registered.
-    ExtensionSearchHandler.registerKeyword(keyword, extension);
-    gKeywordMap.set(extension, keyword);
-  } catch (e) {
-    extension.manifestError(e.message);
+    let keyword = manifest.omnibox.keyword;
+    try {
+      // This will throw if the keyword is already registered.
+      ExtensionSearchHandler.registerKeyword(keyword, extension);
+      this.keyword = keyword;
+    } catch (e) {
+      extension.manifestError(e.message);
+    }
   }
-});
+
+  onShutdown(reason) {
+    ExtensionSearchHandler.unregisterKeyword(this.keyword);
+  }
 
-extensions.on("shutdown", (type, extension) => {
-  let keyword = gKeywordMap.get(extension);
-  if (keyword) {
-    ExtensionSearchHandler.unregisterKeyword(keyword);
-    gKeywordMap.delete(extension);
-  }
-});
-/* eslint-enable mozilla/balanced-listeners */
+  getAPI(context) {
+    let {extension} = context;
+    return {
+      omnibox: {
+        setDefaultSuggestion: (suggestion) => {
+          try {
+            // This will throw if the keyword failed to register.
+            ExtensionSearchHandler.setDefaultSuggestion(this.keyword, suggestion);
+          } catch (e) {
+            return Promise.reject(e.message);
+          }
+        },
 
-extensions.registerSchemaAPI("omnibox", "addon_parent", context => {
-  let {extension} = context;
-  return {
-    omnibox: {
-      setDefaultSuggestion(suggestion) {
-        let keyword = gKeywordMap.get(extension);
-        try {
-          // This will throw if the keyword failed to register.
-          ExtensionSearchHandler.setDefaultSuggestion(keyword, suggestion);
-        } catch (e) {
-          return Promise.reject(e.message);
-        }
+        onInputStarted: new SingletonEventManager(context, "omnibox.onInputStarted", fire => {
+          let listener = (eventName) => {
+            fire.sync();
+          };
+          extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
+          return () => {
+            extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
+          };
+        }).api(),
+
+        onInputCancelled: new SingletonEventManager(context, "omnibox.onInputCancelled", fire => {
+          let listener = (eventName) => {
+            fire.sync();
+          };
+          extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
+          return () => {
+            extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
+          };
+        }).api(),
+
+        onInputEntered: new SingletonEventManager(context, "omnibox.onInputEntered", fire => {
+          let listener = (eventName, text, disposition) => {
+            fire.sync(text, disposition);
+          };
+          extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
+          return () => {
+            extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
+          };
+        }).api(),
       },
 
-      onInputStarted: new SingletonEventManager(context, "omnibox.onInputStarted", fire => {
-        let listener = (eventName) => {
-          fire.sync();
-        };
-        extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
-        return () => {
-          extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
-        };
-      }).api(),
-
-      onInputCancelled: new SingletonEventManager(context, "omnibox.onInputCancelled", fire => {
-        let listener = (eventName) => {
-          fire.sync();
-        };
-        extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
-        return () => {
-          extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
-        };
-      }).api(),
+      omnibox_internal: {
+        addSuggestions: (id, suggestions) => {
+          try {
+            ExtensionSearchHandler.addSuggestions(this.keyword, id, suggestions);
+          } catch (e) {
+            // Silently fail because the extension developer can not know for sure if the user
+            // has already invalidated the callback when asynchronously providing suggestions.
+          }
+        },
 
-      onInputEntered: new SingletonEventManager(context, "omnibox.onInputEntered", fire => {
-        let listener = (eventName, text, disposition) => {
-          fire.sync(text, disposition);
-        };
-        extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
-        return () => {
-          extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
-        };
-      }).api(),
-    },
-
-    omnibox_internal: {
-      addSuggestions(id, suggestions) {
-        let keyword = gKeywordMap.get(extension);
-        try {
-          ExtensionSearchHandler.addSuggestions(keyword, id, suggestions);
-        } catch (e) {
-          // Silently fail because the extension developer can not know for sure if the user
-          // has already invalidated the callback when asynchronously providing suggestions.
-        }
+        onInputChanged: new SingletonEventManager(context, "omnibox_internal.onInputChanged", fire => {
+          let listener = (eventName, text, id) => {
+            fire.sync(text, id);
+          };
+          extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
+          return () => {
+            extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
+          };
+        }).api(),
       },
-
-      onInputChanged: new SingletonEventManager(context, "omnibox_internal.onInputChanged", fire => {
-        let listener = (eventName, text, id) => {
-          fire.sync(text, id);
-        };
-        extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
-        return () => {
-          extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
-        };
-      }).api(),
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -1,80 +1,99 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "PanelPopup",
                                   "resource:///modules/ExtensionPopups.jsm");
 
 Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
 var {
   SingletonEventManager,
   IconDetails,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> PageAction]
-var pageActionMap = new WeakMap();
-
-// Handles URL bar icons, including the |page_action| manifest entry
-// and associated API.
-function PageAction(options, extension) {
-  this.extension = extension;
-  this.id = makeWidgetId(extension.id) + "-page-action";
-
-  this.tabManager = extension.tabManager;
+let pageActionMap = new WeakMap();
 
-  this.defaults = {
-    show: false,
-    title: options.default_title || extension.name,
-    icon: IconDetails.normalize({path: options.default_icon}, extension),
-    popup: options.default_popup || "",
-  };
-
-  this.browserStyle = options.browser_style || false;
-  if (options.browser_style === null) {
-    this.extension.logger.warn("Please specify whether you want browser_style " +
-                               "or not in your page_action options.");
+this.pageAction = class extends ExtensionAPI {
+  static for(extension) {
+    return pageActionMap.get(extension);
   }
 
-  this.tabContext = new TabContext(tab => Object.create(this.defaults),
-                                   extension);
+  onManifestEntry(entryName) {
+    let {extension} = this;
+    let options = extension.manifest.page_action;
+
+    this.id = makeWidgetId(extension.id) + "-page-action";
+
+    this.tabManager = extension.tabManager;
 
-  this.tabContext.on("location-change", this.handleLocationChange.bind(this)); // eslint-disable-line mozilla/balanced-listeners
+    this.defaults = {
+      show: false,
+      title: options.default_title || extension.name,
+      icon: IconDetails.normalize({path: options.default_icon}, extension),
+      popup: options.default_popup || "",
+    };
+
+    this.browserStyle = options.browser_style || false;
+    if (options.browser_style === null) {
+      this.extension.logger.warn("Please specify whether you want browser_style " +
+                                 "or not in your page_action options.");
+    }
+
+    this.tabContext = new TabContext(tab => Object.create(this.defaults),
+                                     extension);
 
-  // WeakMap[ChromeWindow -> <xul:image>]
-  this.buttons = new WeakMap();
+    this.tabContext.on("location-change", this.handleLocationChange.bind(this)); // eslint-disable-line mozilla/balanced-listeners
+
+    // WeakMap[ChromeWindow -> <xul:image>]
+    this.buttons = new WeakMap();
+
+    EventEmitter.decorate(this);
+
+    pageActionMap.set(extension, this);
+  }
 
-  EventEmitter.decorate(this);
-}
+  onShutdown(reason) {
+    pageActionMap.delete(this.extension);
+
+    this.tabContext.shutdown();
 
-PageAction.prototype = {
+    for (let window of windowTracker.browserWindows()) {
+      if (this.buttons.has(window)) {
+        this.buttons.get(window).remove();
+        window.document.removeEventListener("popupshowing", this);
+      }
+    }
+  }
+
   // Returns the value of the property |prop| for the given tab, where
   // |prop| is one of "show", "title", "icon", "popup".
   getProperty(tab, prop) {
     return this.tabContext.get(tab)[prop];
-  },
+  }
 
   // Sets the value of the property |prop| for the given tab to the
   // given value, symmetrically to |getProperty|.
   //
   // If |tab| is currently selected, updates the page action button to
   // reflect the new value.
   setProperty(tab, prop, value) {
     if (value != null) {
       this.tabContext.get(tab)[prop] = value;
     } else {
       delete this.tabContext.get(tab)[prop];
     }
 
     if (tab.selected) {
       this.updateButton(tab.ownerGlobal);
     }
-  },
+  }
 
   // Updates the page action button in the given window to reflect the
   // properties of the currently selected tab:
   //
   // Updates "tooltiptext" and "aria-label" to match "title" property.
   // Updates "image" to match the "icon" property.
   // Shows or hides the icon, based on the "show" property.
   updateButton(window) {
@@ -106,60 +125,60 @@ PageAction.prototype = {
         --webextension-urlbar-image: url("${getIcon(16)}");
         --webextension-urlbar-image-2x: url("${getIcon(32)}");
       `);
 
       button.classList.add("webextension-page-action");
     }
 
     button.hidden = !tabData.show;
-  },
+  }
 
   // Create an |image| node and add it to the |urlbar-icons|
   // container in the given window.
   addButton(window) {
     let document = window.document;
 
     let button = document.createElement("image");
     button.id = this.id;
     button.setAttribute("class", "urlbar-icon");
 
     button.addEventListener("click", this); // eslint-disable-line mozilla/balanced-listeners
     document.addEventListener("popupshowing", this);
 
     document.getElementById("urlbar-icons").appendChild(button);
 
     return button;
-  },
+  }
 
   // Returns the page action button for the given window, creating it if
   // it doesn't already exist.
   getButton(window) {
     if (!this.buttons.has(window)) {
       let button = this.addButton(window);
       this.buttons.set(window, button);
     }
 
     return this.buttons.get(window);
-  },
+  }
 
   /**
    * Triggers this page action for the given window, with the same effects as
    * if it were clicked by a user.
    *
    * This has no effect if the page action is hidden for the selected tab.
    *
    * @param {Window} window
    */
   triggerAction(window) {
     let pageAction = pageActionMap.get(this.extension);
     if (pageAction.getProperty(window.gBrowser.selectedTab, "show")) {
       pageAction.handleClick(window);
     }
-  },
+  }
 
   handleEvent(event) {
     const window = event.target.ownerGlobal;
 
     switch (event.type) {
       case "click":
         if (event.button === 0) {
           this.handleClick(window);
@@ -174,17 +193,17 @@ PageAction.prototype = {
           global.actionContextMenu({
             extension: this.extension,
             onPageAction: true,
             menu: menu,
           });
         }
         break;
     }
-  },
+  }
 
   // Handles a click event on the page action button for the given
   // window.
   // If the page action has a |popup| property, a panel is opened to
   // that URL. Otherwise, a "click" event is emitted, and dispatched to
   // the any click listeners in the add-on.
   handleClick(window) {
     let tab = window.gBrowser.selectedTab;
@@ -197,120 +216,91 @@ PageAction.prototype = {
     // If it has no popup URL defined, we dispatch a click event, but do not
     // open a popup.
     if (popupURL) {
       new PanelPopup(this.extension, this.getButton(window), popupURL,
                      this.browserStyle);
     } else {
       this.emit("click", tab);
     }
-  },
+  }
 
   handleLocationChange(eventType, tab, fromBrowse) {
     if (fromBrowse) {
       this.tabContext.clear(tab);
     }
     this.updateButton(tab.ownerGlobal);
-  },
+  }
+
+  getAPI(context) {
+    let {extension} = context;
 
-  shutdown() {
-    this.tabContext.shutdown();
+    const {tabManager} = extension;
+    const pageAction = this;
+
+    return {
+      pageAction: {
+        onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
+          let listener = (evt, tab) => {
+            fire.async(tabManager.convert(tab));
+          };
 
-    for (let window of windowTracker.browserWindows()) {
-      if (this.buttons.has(window)) {
-        this.buttons.get(window).remove();
-        window.removeEventListener("popupshowing", this);
-      }
-    }
-  },
-};
+          pageAction.on("click", listener);
+          return () => {
+            pageAction.off("click", listener);
+          };
+        }).api(),
+
+        show(tabId) {
+          let tab = tabTracker.getTab(tabId);
+          pageAction.setProperty(tab, "show", true);
+        },
+
+        hide(tabId) {
+          let tab = tabTracker.getTab(tabId);
+          pageAction.setProperty(tab, "show", false);
+        },
+
+        setTitle(details) {
+          let tab = tabTracker.getTab(details.tabId);
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
-  let pageAction = new PageAction(manifest.page_action, extension);
-  pageActionMap.set(extension, pageAction);
-});
+          // Clear the tab-specific title when given a null string.
+          pageAction.setProperty(tab, "title", details.title || null);
+        },
+
+        getTitle(details) {
+          let tab = tabTracker.getTab(details.tabId);
+
+          let title = pageAction.getProperty(tab, "title");
+          return Promise.resolve(title);
+        },
+
+        setIcon(details) {
+          let tab = tabTracker.getTab(details.tabId);
+
+          let icon = IconDetails.normalize(details, extension, context);
+          pageAction.setProperty(tab, "icon", icon);
+        },
 
-extensions.on("shutdown", (type, extension) => {
-  if (pageActionMap.has(extension)) {
-    pageActionMap.get(extension).shutdown();
-    pageActionMap.delete(extension);
+        setPopup(details) {
+          let tab = tabTracker.getTab(details.tabId);
+
+          // Note: Chrome resolves arguments to setIcon relative to the calling
+          // context, but resolves arguments to setPopup relative to the extension
+          // root.
+          // For internal consistency, we currently resolve both relative to the
+          // calling context.
+          let url = details.popup && context.uri.resolve(details.popup);
+          pageAction.setProperty(tab, "popup", url);
+        },
+
+        getPopup(details) {
+          let tab = tabTracker.getTab(details.tabId);
+
+          let popup = pageAction.getProperty(tab, "popup");
+          return Promise.resolve(popup);
+        },
+      },
+    };
   }
-});
-/* eslint-enable mozilla/balanced-listeners */
-
-PageAction.for = extension => {
-  return pageActionMap.get(extension);
 };
 
-global.pageActionFor = PageAction.for;
-
-extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
-  let {extension} = context;
-
-  const {tabManager} = extension;
-
-  return {
-    pageAction: {
-      onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
-        let listener = (evt, tab) => {
-          fire.async(tabManager.convert(tab));
-        };
-        let pageAction = PageAction.for(extension);
-
-        pageAction.on("click", listener);
-        return () => {
-          pageAction.off("click", listener);
-        };
-      }).api(),
-
-      show(tabId) {
-        let tab = tabTracker.getTab(tabId);
-        PageAction.for(extension).setProperty(tab, "show", true);
-      },
-
-      hide(tabId) {
-        let tab = tabTracker.getTab(tabId);
-        PageAction.for(extension).setProperty(tab, "show", false);
-      },
-
-      setTitle(details) {
-        let tab = tabTracker.getTab(details.tabId);
-
-        // Clear the tab-specific title when given a null string.
-        PageAction.for(extension).setProperty(tab, "title", details.title || null);
-      },
-
-      getTitle(details) {
-        let tab = tabTracker.getTab(details.tabId);
-
-        let title = PageAction.for(extension).getProperty(tab, "title");
-        return Promise.resolve(title);
-      },
-
-      setIcon(details) {
-        let tab = tabTracker.getTab(details.tabId);
-
-        let icon = IconDetails.normalize(details, extension, context);
-        PageAction.for(extension).setProperty(tab, "icon", icon);
-      },
-
-      setPopup(details) {
-        let tab = tabTracker.getTab(details.tabId);
-
-        // Note: Chrome resolves arguments to setIcon relative to the calling
-        // context, but resolves arguments to setPopup relative to the extension
-        // root.
-        // For internal consistency, we currently resolve both relative to the
-        // calling context.
-        let url = details.popup && context.uri.resolve(details.popup);
-        PageAction.for(extension).setProperty(tab, "popup", url);
-      },
-
-      getPopup(details) {
-        let tab = tabTracker.getTab(details.tabId);
-
-        let popup = PageAction.for(extension).getProperty(tab, "popup");
-        return Promise.resolve(popup);
-      },
-    },
-  };
-});
+global.pageActionFor = this.pageAction.for;
--- a/browser/components/extensions/ext-sessions.js
+++ b/browser/components/extensions/ext-sessions.js
@@ -1,13 +1,12 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   promiseObserved,
   SingletonEventManager,
 } = ExtensionUtils;
 
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
                                   "resource:///modules/sessionstore/SessionStore.jsm");
 
@@ -49,59 +48,61 @@ function createSession(restored, extensi
       sessionObj.window = extension.windowManager.convert(restored, {populate: true});
       return Promise.resolve([sessionObj]);
     });
   }
   sessionObj.tab = extension.tabManager.convert(restored);
   return Promise.resolve([sessionObj]);
 }
 
-extensions.registerSchemaAPI("sessions", "addon_parent", context => {
-  let {extension} = context;
-  return {
-    sessions: {
-      getRecentlyClosed: function(filter) {
-        let maxResults = filter.maxResults == undefined ? this.MAX_SESSION_RESULTS : filter.maxResults;
-        return Promise.resolve(getRecentlyClosed(maxResults, extension));
-      },
+this.sessions = class extends ExtensionAPI {
+  getAPI(context) {
+    let {extension} = context;
+    return {
+      sessions: {
+        getRecentlyClosed: function(filter) {
+          let maxResults = filter.maxResults == undefined ? this.MAX_SESSION_RESULTS : filter.maxResults;
+          return Promise.resolve(getRecentlyClosed(maxResults, extension));
+        },
 
-      restore: function(sessionId) {
-        let session, closedId;
-        if (sessionId) {
-          closedId = sessionId;
-          session = SessionStore.undoCloseById(closedId);
-        } else if (SessionStore.lastClosedObjectType == "window") {
-          // If the most recently closed object is a window, just undo closing the most recent window.
-          session = SessionStore.undoCloseWindow(0);
-        } else {
-          // It is a tab, and we cannot call SessionStore.undoCloseTab without a window,
-          // so we must find the tab in which case we can just use its closedId.
-          let recentlyClosedTabs = [];
-          for (let window of windowTracker.browserWindows()) {
-            let closedTabData = SessionStore.getClosedTabData(window, false);
-            for (let tab of closedTabData) {
-              recentlyClosedTabs.push(tab);
+        restore: function(sessionId) {
+          let session, closedId;
+          if (sessionId) {
+            closedId = sessionId;
+            session = SessionStore.undoCloseById(closedId);
+          } else if (SessionStore.lastClosedObjectType == "window") {
+            // If the most recently closed object is a window, just undo closing the most recent window.
+            session = SessionStore.undoCloseWindow(0);
+          } else {
+            // It is a tab, and we cannot call SessionStore.undoCloseTab without a window,
+            // so we must find the tab in which case we can just use its closedId.
+            let recentlyClosedTabs = [];
+            for (let window of windowTracker.browserWindows()) {
+              let closedTabData = SessionStore.getClosedTabData(window, false);
+              for (let tab of closedTabData) {
+                recentlyClosedTabs.push(tab);
+              }
             }
-          }
 
-          // Sort the tabs.
-          recentlyClosedTabs.sort((a, b) => b.closedAt - a.closedAt);
+            // Sort the tabs.
+            recentlyClosedTabs.sort((a, b) => b.closedAt - a.closedAt);
 
-          // Use the closedId of the most recently closed tab to restore it.
-          closedId = recentlyClosedTabs[0].closedId;
-          session = SessionStore.undoCloseById(closedId);
-        }
-        return createSession(session, extension, closedId);
-      },
+            // Use the closedId of the most recently closed tab to restore it.
+            closedId = recentlyClosedTabs[0].closedId;
+            session = SessionStore.undoCloseById(closedId);
+          }
+          return createSession(session, extension, closedId);
+        },
 
-      onChanged: new SingletonEventManager(context, "sessions.onChanged", fire => {
-        let observer = () => {
-          fire.async();
-        };
+        onChanged: new SingletonEventManager(context, "sessions.onChanged", fire => {
+          let observer = () => {
+            fire.async();
+          };
 
-        Services.obs.addObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED, false);
-        return () => {
-          Services.obs.removeObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED);
-        };
-      }).api(),
-    },
-  };
-});
+          Services.obs.addObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED, false);
+          return () => {
+            Services.obs.removeObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED);
+          };
+        }).api(),
+      },
+    };
+  }
+};
--- a/browser/components/extensions/ext-sidebarAction.js
+++ b/browser/components/extensions/ext-sidebarAction.js
@@ -1,42 +1,48 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
-let {
+var {
   ExtensionError,
   IconDetails,
 } = ExtensionUtils;
 
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> SidebarAction]
 let sidebarActionMap = new WeakMap();
 
 const sidebarURL = "chrome://browser/content/webext-panels.xul";
 
 /**
  * Responsible for the sidebar_action section of the manifest as well
  * as the associated sidebar browser.
  */
-class SidebarAction {
-  constructor(options, extension) {
-    this.extension = extension;
+this.sidebarAction = class extends ExtensionAPI {
+  static for(extension) {
+    return sidebarActionMap.get(extension);
+  }
+
+  onManifestEntry(entryName) {
+    let {extension} = this;
+
+    extension.once("ready", this.onReady.bind(this));
+
+    let options = extension.manifest.sidebar_action;
 
     // Add the extension to the sidebar menu.  The sidebar widget will copy
     // from that when it is viewed, so we shouldn't need to update that.
     let widgetId = makeWidgetId(extension.id);
     this.id = `${widgetId}-sidebar-action`;
     this.menuId = `menu_${this.id}`;
 
     this.defaults = {
@@ -49,16 +55,50 @@ class SidebarAction {
     this.tabContext = new TabContext(tab => Object.create(this.defaults),
                                      extension);
 
     // We need to ensure our elements are available before session restore.
     this.windowOpenListener = (window) => {
       this.createMenuItem(window, this.defaults);
     };
     windowTracker.addOpenListener(this.windowOpenListener);
+
+    sidebarActionMap.set(extension, this);
+  }
+
+  onReady() {
+    this.build();
+  }
+
+  onShutdown(reason) {
+    sidebarActionMap.delete(this.this);
+
+    this.tabContext.shutdown();
+
+    // Don't remove everything on app shutdown so session restore can handle
+    // restoring open sidebars.
+    if (reason === "APP_SHUTDOWN") {
+      return;
+    }
+
+    for (let window of windowTracker.browserWindows()) {
+      let {document, SidebarUI} = window;
+      if (SidebarUI.currentID === this.id) {
+        SidebarUI.hide();
+      }
+      let menu = document.getElementById(this.menuId);
+      if (menu) {
+        menu.remove();
+      }
+      let broadcaster = document.getElementById(this.id);
+      if (broadcaster) {
+        broadcaster.remove();
+      }
+    }
+    windowTracker.removeOpenListener(this.windowOpenListener);
   }
 
   build() {
     this.tabContext.on("tab-select", // eslint-disable-line mozilla/balanced-listeners
                        (evt, tab) => { this.updateWindow(tab.ownerGlobal); });
 
     let install = this.extension.startupReason === "ADDON_INSTALL";
     for (let window of windowTracker.browserWindows()) {
@@ -235,134 +275,87 @@ class SidebarAction {
    */
   getProperty(nativeTab, prop) {
     if (nativeTab === null) {
       return this.defaults[prop];
     }
     return this.tabContext.get(nativeTab)[prop];
   }
 
-  shutdown() {
-    this.tabContext.shutdown();
-    for (let window of windowTracker.browserWindows()) {
-      let {document, SidebarUI} = window;
-      if (SidebarUI.currentID === this.id) {
-        SidebarUI.hide();
-      }
-      let menu = document.getElementById(this.menuId);
-      if (menu) {
-        menu.remove();
-      }
-      let broadcaster = document.getElementById(this.id);
-      if (broadcaster) {
-        broadcaster.remove();
-      }
-    }
-    windowTracker.removeOpenListener(this.windowOpenListener);
-  }
-
   /**
    * Triggers this sidebar action for the given window, with the same effects as
    * if it were toggled via menu or toolbarbutton by a user.
    *
    * @param {ChromeWindow} window
    */
   triggerAction(window) {
     let {SidebarUI} = window;
     if (SidebarUI) {
       SidebarUI.toggle(this.id);
     }
   }
-}
+
+  getAPI(context) {
+    let {extension} = context;
+    const sidebarAction = this;
+
+    function getTab(tabId) {
+      if (tabId !== null) {
+        return tabTracker.getTab(tabId);
+      }
+      return null;
+    }
+
+    return {
+      sidebarAction: {
+        async setTitle(details) {
+          let nativeTab = getTab(details.tabId);
+
+          let title = details.title;
+          // Clear the tab-specific title when given a null string.
+          if (nativeTab && title === "") {
+            title = null;
+          }
+          sidebarAction.setProperty(nativeTab, "title", title);
+        },
+
+        getTitle(details) {
+          let nativeTab = getTab(details.tabId);
+
+          let title = sidebarAction.getProperty(nativeTab, "title");
+          return Promise.resolve(title);
+        },
 
-SidebarAction.for = (extension) => {
-  return sidebarActionMap.get(extension);
+        async setIcon(details) {
+          let nativeTab = getTab(details.tabId);
+
+          let icon = IconDetails.normalize(details, extension, context);
+          sidebarAction.setProperty(nativeTab, "icon", icon);
+        },
+
+        async setPanel(details) {
+          let nativeTab = getTab(details.tabId);
+
+          let url;
+          // Clear the tab-specific url when given a null string.
+          if (nativeTab && details.panel === "") {
+            url = null;
+          } else if (details.panel !== "") {
+            url = context.uri.resolve(details.panel);
+          } else {
+            throw new ExtensionError("Invalid url for sidebar panel.");
+          }
+
+          sidebarAction.setProperty(nativeTab, "panel", url);
+        },
+
+        getPanel(details) {
+          let nativeTab = getTab(details.tabId);
+
+          let panel = sidebarAction.getProperty(nativeTab, "panel");
+          return Promise.resolve(panel);
+        },
+      },
+    };
+  }
 };
 
-global.sidebarActionFor = SidebarAction.for;
-
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_sidebar_action", (type, directive, extension, manifest) => {
-  let sidebarAction = new SidebarAction(manifest.sidebar_action, extension);
-  sidebarActionMap.set(extension, sidebarAction);
-});
-
-extensions.on("ready", (type, extension) => {
-  // We build sidebars during ready to ensure the background scripts are ready.
-  if (sidebarActionMap.has(extension)) {
-    sidebarActionMap.get(extension).build();
-  }
-});
-
-extensions.on("shutdown", (type, extension) => {
-  if (sidebarActionMap.has(extension)) {
-    // Don't remove everything on app shutdown so session restore can handle
-    // restoring open sidebars.
-    if (extension.shutdownReason !== "APP_SHUTDOWN") {
-      sidebarActionMap.get(extension).shutdown();
-    }
-    sidebarActionMap.delete(extension);
-  }
-});
-/* eslint-enable mozilla/balanced-listeners */
-
-extensions.registerSchemaAPI("sidebarAction", "addon_parent", context => {
-  let {extension} = context;
-
-  function getTab(tabId) {
-    if (tabId !== null) {
-      return tabTracker.getTab(tabId);
-    }
-    return null;
-  }
-
-  return {
-    sidebarAction: {
-      async setTitle(details) {
-        let nativeTab = getTab(details.tabId);
-
-        let title = details.title;
-        // Clear the tab-specific title when given a null string.
-        if (nativeTab && title === "") {
-          title = null;
-        }
-        SidebarAction.for(extension).setProperty(nativeTab, "title", title);
-      },
-
-      getTitle(details) {
-        let nativeTab = getTab(details.tabId);
-
-        let title = SidebarAction.for(extension).getProperty(nativeTab, "title");
-        return Promise.resolve(title);
-      },
-
-      async setIcon(details) {
-        let nativeTab = getTab(details.tabId);
-
-        let icon = IconDetails.normalize(details, extension, context);
-        SidebarAction.for(extension).setProperty(nativeTab, "icon", icon);
-      },
-
-      async setPanel(details) {
-        let nativeTab = getTab(details.tabId);
-
-        let url;
-        // Clear the tab-specific url when given a null string.
-        if (nativeTab && details.panel === "") {
-          url = null;
-        } else if (details.panel !== "") {
-          url = context.uri.resolve(details.panel);
-        } else {
-          throw new ExtensionError("Invalid url for sidebar panel.");
-        }
-
-        SidebarAction.for(extension).setProperty(nativeTab, "panel", url);
-      },
-
-      getPanel(details) {
-        let nativeTab = getTab(details.tabId);
-
-        let panel = SidebarAction.for(extension).getProperty(nativeTab, "panel");
-        return Promise.resolve(panel);
-      },
-    },
-  };
-});
+global.sidebarActionFor = this.sidebarAction.for;
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -10,67 +10,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
 var {
   SingletonEventManager,
 } = ExtensionUtils;
 
-// This function is pretty tightly tied to Extension.jsm.
-// Its job is to fill in the |tab| property of the sender.
-function getSender(extension, target, sender) {
-  let tabId;
-  if ("tabId" in sender) {
-    // The message came from a privileged extension page running in a tab. In
-    // that case, it should include a tabId property (which is filled in by the
-    // page-open listener below).
-    tabId = sender.tabId;
-    delete sender.tabId;
-  } else if (target instanceof Ci.nsIDOMXULElement) {
-    tabId = tabTracker.getBrowserData(target).tabId;
-  }
-
-  if (tabId) {
-    let tab = extension.tabManager.get(tabId, null);
-    if (tab) {
-      sender.tab = tab.convert();
-    }
-  }
-}
-
-// Used by Extension.jsm
-global.tabGetSender = getSender;
-
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("page-shutdown", (type, context) => {
-  if (context.viewType == "tab") {
-    if (context.extension.id !== context.xulBrowser.contentPrincipal.addonId) {
-      // Only close extension tabs.
-      // This check prevents about:addons from closing when it contains a
-      // WebExtension as an embedded inline options page.
-      return;
-    }
-    let {gBrowser} = context.xulBrowser.ownerGlobal;
-    if (gBrowser) {
-      let nativeTab = gBrowser.getTabForBrowser(context.xulBrowser);
-      if (nativeTab) {
-        gBrowser.removeTab(nativeTab);
-      }
-    }
-  }
-});
-/* eslint-enable mozilla/balanced-listeners */
-
 let tabListener = {
   tabReadyInitialized: false,
   tabReadyPromises: new WeakMap(),
   initializingTabs: new WeakSet(),
 
   initTabReady() {
     if (!this.tabReadyInitialized) {
       windowTracker.addListener("progress", this);
@@ -117,671 +70,673 @@ let tabListener = {
         this.initTabReady();
         this.tabReadyPromises.set(nativeTab, deferred);
       }
     }
     return deferred.promise;
   },
 };
 
-extensions.registerSchemaAPI("tabs", "addon_parent", context => {
-  let {extension} = context;
+this.tabs = class extends ExtensionAPI {
+  getAPI(context) {
+    let {extension} = context;
+
+    let {tabManager} = extension;
 
-  let {tabManager} = extension;
-
-  function getTabOrActive(tabId) {
-    if (tabId !== null) {
-      return tabTracker.getTab(tabId);
+    function getTabOrActive(tabId) {
+      if (tabId !== null) {
+        return tabTracker.getTab(tabId);
+      }
+      return tabTracker.activeTab;
     }
-    return tabTracker.activeTab;
-  }
 
-  async function promiseTabWhenReady(tabId) {
-    let tab;
-    if (tabId !== null) {
-      tab = tabManager.get(tabId);
-    } else {
-      tab = tabManager.getWrapper(tabTracker.activeTab);
+    async function promiseTabWhenReady(tabId) {
+      let tab;
+      if (tabId !== null) {
+        tab = tabManager.get(tabId);
+      } else {
+        tab = tabManager.getWrapper(tabTracker.activeTab);
+      }
+
+      await tabListener.awaitTabReady(tab.nativeTab);
+
+      return tab;
     }
 
-    await tabListener.awaitTabReady(tab.nativeTab);
-
-    return tab;
-  }
+    let self = {
+      tabs: {
+        onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
+          let nativeTab = event.originalTarget;
+          let tabId = tabTracker.getId(nativeTab);
+          let windowId = windowTracker.getId(nativeTab.ownerGlobal);
+          fire.async({tabId, windowId});
+        }).api(),
 
-  let self = {
-    tabs: {
-      onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
-        let nativeTab = event.originalTarget;
-        let tabId = tabTracker.getId(nativeTab);
-        let windowId = windowTracker.getId(nativeTab.ownerGlobal);
-        fire.async({tabId, windowId});
-      }).api(),
+        onCreated: new SingletonEventManager(context, "tabs.onCreated", fire => {
+          let listener = (eventName, event) => {
+            fire.async(tabManager.convert(event.nativeTab));
+          };
 
-      onCreated: new SingletonEventManager(context, "tabs.onCreated", fire => {
-        let listener = (eventName, event) => {
-          fire.async(tabManager.convert(event.nativeTab));
-        };
+          tabTracker.on("tab-created", listener);
+          return () => {
+            tabTracker.off("tab-created", listener);
+          };
+        }).api(),
 
-        tabTracker.on("tab-created", listener);
-        return () => {
-          tabTracker.off("tab-created", listener);
-        };
-      }).api(),
+        /**
+         * Since multiple tabs currently can't be highlighted, onHighlighted
+         * essentially acts an alias for self.tabs.onActivated but returns
+         * the tabId in an array to match the API.
+         * @see  https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
+        */
+        onHighlighted: new WindowEventManager(context, "tabs.onHighlighted", "TabSelect", (fire, event) => {
+          let nativeTab = event.originalTarget;
+          let tabIds = [tabTracker.getId(nativeTab)];
+          let windowId = windowTracker.getId(nativeTab.ownerGlobal);
+          fire.async({tabIds, windowId});
+        }).api(),
+
+        onAttached: new SingletonEventManager(context, "tabs.onAttached", fire => {
+          let listener = (eventName, event) => {
+            fire.async(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
+          };
 
-      /**
-       * Since multiple tabs currently can't be highlighted, onHighlighted
-       * essentially acts an alias for self.tabs.onActivated but returns
-       * the tabId in an array to match the API.
-       * @see  https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
-      */
-      onHighlighted: new WindowEventManager(context, "tabs.onHighlighted", "TabSelect", (fire, event) => {
-        let nativeTab = event.originalTarget;
-        let tabIds = [tabTracker.getId(nativeTab)];
-        let windowId = windowTracker.getId(nativeTab.ownerGlobal);
-        fire.async({tabIds, windowId});
-      }).api(),
+          tabTracker.on("tab-attached", listener);
+          return () => {
+            tabTracker.off("tab-attached", listener);
+          };
+        }).api(),
+
+        onDetached: new SingletonEventManager(context, "tabs.onDetached", fire => {
+          let listener = (eventName, event) => {
+            fire.async(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
+          };
 
-      onAttached: new SingletonEventManager(context, "tabs.onAttached", fire => {
-        let listener = (eventName, event) => {
-          fire.async(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
-        };
+          tabTracker.on("tab-detached", listener);
+          return () => {
+            tabTracker.off("tab-detached", listener);
+          };
+        }).api(),
 
-        tabTracker.on("tab-attached", listener);
-        return () => {
-          tabTracker.off("tab-attached", listener);
-        };
-      }).api(),
+        onRemoved: new SingletonEventManager(context, "tabs.onRemoved", fire => {
+          let listener = (eventName, event) => {
+            fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
+          };
 
-      onDetached: new SingletonEventManager(context, "tabs.onDetached", fire => {
-        let listener = (eventName, event) => {
-          fire.async(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
-        };
+          tabTracker.on("tab-removed", listener);
+          return () => {
+            tabTracker.off("tab-removed", listener);
+          };
+        }).api(),
 
-        tabTracker.on("tab-detached", listener);
-        return () => {
-          tabTracker.off("tab-detached", listener);
-        };
-      }).api(),
+        onReplaced: new SingletonEventManager(context, "tabs.onReplaced", fire => {
+          return () => {};
+        }).api(),
 
-      onRemoved: new SingletonEventManager(context, "tabs.onRemoved", fire => {
-        let listener = (eventName, event) => {
-          fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
-        };
+        onMoved: new SingletonEventManager(context, "tabs.onMoved", fire => {
+          // There are certain circumstances where we need to ignore a move event.
+          //
+          // Namely, the first time the tab is moved after it's created, we need
+          // to report the final position as the initial position in the tab's
+          // onAttached or onCreated event. This is because most tabs are inserted
+          // in a temporary location and then moved after the TabOpen event fires,
+          // which generates a TabOpen event followed by a TabMove event, which
+          // does not match the contract of our API.
+          let ignoreNextMove = new WeakSet();
 
-        tabTracker.on("tab-removed", listener);
-        return () => {
-          tabTracker.off("tab-removed", listener);
-        };
-      }).api(),
+          let openListener = event => {
+            ignoreNextMove.add(event.target);
+            // Remove the tab from the set on the next tick, since it will already
+            // have been moved by then.
+            Promise.resolve().then(() => {
+              ignoreNextMove.delete(event.target);
+            });
+          };
 
-      onReplaced: new SingletonEventManager(context, "tabs.onReplaced", fire => {
-        return () => {};
-      }).api(),
+          let moveListener = event => {
+            let nativeTab = event.originalTarget;
 
-      onMoved: new SingletonEventManager(context, "tabs.onMoved", fire => {
-        // There are certain circumstances where we need to ignore a move event.
-        //
-        // Namely, the first time the tab is moved after it's created, we need
-        // to report the final position as the initial position in the tab's
-        // onAttached or onCreated event. This is because most tabs are inserted
-        // in a temporary location and then moved after the TabOpen event fires,
-        // which generates a TabOpen event followed by a TabMove event, which
-        // does not match the contract of our API.
-        let ignoreNextMove = new WeakSet();
+            if (ignoreNextMove.has(nativeTab)) {
+              ignoreNextMove.delete(nativeTab);
+              return;
+            }
+
+            fire.async(tabTracker.getId(nativeTab), {
+              windowId: windowTracker.getId(nativeTab.ownerGlobal),
+              fromIndex: event.detail,
+              toIndex: nativeTab._tPos,
+            });
+          };
 
-        let openListener = event => {
-          ignoreNextMove.add(event.target);
-          // Remove the tab from the set on the next tick, since it will already
-          // have been moved by then.
-          Promise.resolve().then(() => {
-            ignoreNextMove.delete(event.target);
-          });
-        };
+          windowTracker.addListener("TabMove", moveListener);
+          windowTracker.addListener("TabOpen", openListener);
+          return () => {
+            windowTracker.removeListener("TabMove", moveListener);
+            windowTracker.removeListener("TabOpen", openListener);
+          };
+        }).api(),
+
+        onUpdated: new SingletonEventManager(context, "tabs.onUpdated", fire => {
+          const restricted = ["url", "favIconUrl", "title"];
 
-        let moveListener = event => {
-          let nativeTab = event.originalTarget;
-
-          if (ignoreNextMove.has(nativeTab)) {
-            ignoreNextMove.delete(nativeTab);
-            return;
+          function sanitize(extension, changeInfo) {
+            let result = {};
+            let nonempty = false;
+            for (let prop in changeInfo) {
+              if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
+                nonempty = true;
+                result[prop] = changeInfo[prop];
+              }
+            }
+            return [nonempty, result];
           }
 
-          fire.async(tabTracker.getId(nativeTab), {
-            windowId: windowTracker.getId(nativeTab.ownerGlobal),
-            fromIndex: event.detail,
-            toIndex: nativeTab._tPos,
-          });
-        };
+          let fireForTab = (tab, changed) => {
+            let [needed, changeInfo] = sanitize(extension, changed);
+            if (needed) {
+              fire.async(tab.id, changeInfo, tab.convert());
+            }
+          };
+
+          let listener = event => {
+            let needed = [];
+            if (event.type == "TabAttrModified") {
+              let changed = event.detail.changed;
+              if (changed.includes("image")) {
+                needed.push("favIconUrl");
+              }
+              if (changed.includes("muted")) {
+                needed.push("mutedInfo");
+              }
+              if (changed.includes("soundplaying")) {
+                needed.push("audible");
+              }
+              if (changed.includes("label")) {
+                needed.push("title");
+              }
+            } else if (event.type == "TabPinned") {
+              needed.push("pinned");
+            } else if (event.type == "TabUnpinned") {
+              needed.push("pinned");
+            }
+
+            let tab = tabManager.getWrapper(event.originalTarget);
+            let changeInfo = {};
+            for (let prop of needed) {
+              changeInfo[prop] = tab[prop];
+            }
+
+            fireForTab(tab, changeInfo);
+          };
 
-        windowTracker.addListener("TabMove", moveListener);
-        windowTracker.addListener("TabOpen", openListener);
-        return () => {
-          windowTracker.removeListener("TabMove", moveListener);
-          windowTracker.removeListener("TabOpen", openListener);
-        };
-      }).api(),
+          let statusListener = ({browser, status, url}) => {
+            let {gBrowser} = browser.ownerGlobal;
+            let tabElem = gBrowser.getTabForBrowser(browser);
+            if (tabElem) {
+              let changed = {status};
+              if (url) {
+                changed.url = url;
+              }
+
+              fireForTab(tabManager.wrapTab(tabElem), changed);
+            }
+          };
+
+          windowTracker.addListener("status", statusListener);
+          windowTracker.addListener("TabAttrModified", listener);
+          windowTracker.addListener("TabPinned", listener);
+          windowTracker.addListener("TabUnpinned", listener);
 
-      onUpdated: new SingletonEventManager(context, "tabs.onUpdated", fire => {
-        const restricted = ["url", "favIconUrl", "title"];
+          return () => {
+            windowTracker.removeListener("status", statusListener);
+            windowTracker.removeListener("TabAttrModified", listener);
+            windowTracker.removeListener("TabPinned", listener);
+            windowTracker.removeListener("TabUnpinned", listener);
+          };
+        }).api(),
 
-        function sanitize(extension, changeInfo) {
-          let result = {};
-          let nonempty = false;
-          for (let prop in changeInfo) {
-            if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
-              nonempty = true;
-              result[prop] = changeInfo[prop];
+        create(createProperties) {
+          return new Promise((resolve, reject) => {
+            let window = createProperties.windowId !== null ?
+              windowTracker.getWindow(createProperties.windowId, context) :
+              windowTracker.topWindow;
+
+            if (!window.gBrowser) {
+              let obs = (finishedWindow, topic, data) => {
+                if (finishedWindow != window) {
+                  return;
+                }
+                Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
+                resolve(window);
+              };
+              Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
+            } else {
+              resolve(window);
             }
-          }
-          return [nonempty, result];
-        }
+          }).then(window => {
+            let url;
+
+            if (createProperties.url !== null) {
+              url = context.uri.resolve(createProperties.url);
+
+              if (!context.checkLoadURL(url, {dontReportErrors: true})) {
+                return Promise.reject({message: `Illegal URL: ${url}`});
+              }
+            }
+
+            if (createProperties.cookieStoreId && !extension.hasPermission("cookies")) {
+              return Promise.reject({message: `No permission for cookieStoreId: ${createProperties.cookieStoreId}`});
+            }
 
-        let fireForTab = (tab, changed) => {
-          let [needed, changeInfo] = sanitize(extension, changed);
-          if (needed) {
-            fire.async(tab.id, changeInfo, tab.convert());
-          }
-        };
+            let options = {};
+            if (createProperties.cookieStoreId) {
+              if (!global.isValidCookieStoreId(createProperties.cookieStoreId)) {
+                return Promise.reject({message: `Illegal cookieStoreId: ${createProperties.cookieStoreId}`});
+              }
+
+              let privateWindow = PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser);
+              if (privateWindow && !global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
+                return Promise.reject({message: `Illegal to set non-private cookieStoreId in a private window`});
+              }
+
+              if (!privateWindow && global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
+                return Promise.reject({message: `Illegal to set private cookieStoreId in a non-private window`});
+              }
+
+              if (global.isContainerCookieStoreId(createProperties.cookieStoreId)) {
+                let containerId = global.getContainerForCookieStoreId(createProperties.cookieStoreId);
+                if (!containerId) {
+                  return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
+                }
+
+                options.userContextId = containerId;
+              }
+            }
 
-        let listener = event => {
-          let needed = [];
-          if (event.type == "TabAttrModified") {
-            let changed = event.detail.changed;
-            if (changed.includes("image")) {
-              needed.push("favIconUrl");
+            // Make sure things like about:blank and data: URIs never inherit,
+            // and instead always get a NullPrincipal.
+            options.disallowInheritPrincipal = true;
+
+            tabListener.initTabReady();
+            let nativeTab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
+
+            let active = true;
+            if (createProperties.active !== null) {
+              active = createProperties.active;
             }
-            if (changed.includes("muted")) {
-              needed.push("mutedInfo");
+            if (active) {
+              window.gBrowser.selectedTab = nativeTab;
+            }
+
+            if (createProperties.index !== null) {
+              window.gBrowser.moveTabTo(nativeTab, createProperties.index);
+            }
+
+            if (createProperties.pinned) {
+              window.gBrowser.pinTab(nativeTab);
             }
-            if (changed.includes("soundplaying")) {
-              needed.push("audible");
+
+            if (active && !url) {
+              window.focusAndSelectUrlBar();
             }
-            if (changed.includes("label")) {
-              needed.push("title");
+
+            if (createProperties.url && createProperties.url !== window.BROWSER_NEW_TAB_URL) {
+              // We can't wait for a location change event for about:newtab,
+              // since it may be pre-rendered, in which case its initial
+              // location change event has already fired.
+
+              // Mark the tab as initializing, so that operations like
+              // `executeScript` wait until the requested URL is loaded in
+              // the tab before dispatching messages to the inner window
+              // that contains the URL we're attempting to load.
+              tabListener.initializingTabs.add(nativeTab);
             }
-          } else if (event.type == "TabPinned") {
-            needed.push("pinned");
-          } else if (event.type == "TabUnpinned") {
-            needed.push("pinned");
+
+            return tabManager.convert(nativeTab);
+          });
+        },
+
+        async remove(tabs) {
+          if (!Array.isArray(tabs)) {
+            tabs = [tabs];
           }
 
-          let tab = tabManager.getWrapper(event.originalTarget);
-          let changeInfo = {};
-          for (let prop of needed) {
-            changeInfo[prop] = tab[prop];
+          for (let tabId of tabs) {
+            let nativeTab = tabTracker.getTab(tabId);
+            nativeTab.ownerGlobal.gBrowser.removeTab(nativeTab);
           }
-
-          fireForTab(tab, changeInfo);
-        };
-
-        let statusListener = ({browser, status, url}) => {
-          let {gBrowser} = browser.ownerGlobal;
-          let tabElem = gBrowser.getTabForBrowser(browser);
-          if (tabElem) {
-            let changed = {status};
-            if (url) {
-              changed.url = url;
-            }
-
-            fireForTab(tabManager.wrapTab(tabElem), changed);
-          }
-        };
-
-        windowTracker.addListener("status", statusListener);
-        windowTracker.addListener("TabAttrModified", listener);
-        windowTracker.addListener("TabPinned", listener);
-        windowTracker.addListener("TabUnpinned", listener);
+        },
 
-        return () => {
-          windowTracker.removeListener("status", statusListener);
-          windowTracker.removeListener("TabAttrModified", listener);
-          windowTracker.removeListener("TabPinned", listener);
-          windowTracker.removeListener("TabUnpinned", listener);
-        };
-      }).api(),
-
-      create(createProperties) {
-        return new Promise((resolve, reject) => {
-          let window = createProperties.windowId !== null ?
-            windowTracker.getWindow(createProperties.windowId, context) :
-            windowTracker.topWindow;
+        async update(tabId, updateProperties) {
+          let nativeTab = getTabOrActive(tabId);
 
-          if (!window.gBrowser) {
-            let obs = (finishedWindow, topic, data) => {
-              if (finishedWindow != window) {
-                return;
-              }
-              Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
-              resolve(window);
-            };
-            Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
-          } else {
-            resolve(window);
-          }
-        }).then(window => {
-          let url;
+          let tabbrowser = nativeTab.ownerGlobal.gBrowser;
 
-          if (createProperties.url !== null) {
-            url = context.uri.resolve(createProperties.url);
+          if (updateProperties.url !== null) {
+            let url = context.uri.resolve(updateProperties.url);
 
             if (!context.checkLoadURL(url, {dontReportErrors: true})) {
               return Promise.reject({message: `Illegal URL: ${url}`});
             }
+
+            nativeTab.linkedBrowser.loadURI(url);
           }
 
-          if (createProperties.cookieStoreId && !extension.hasPermission("cookies")) {
-            return Promise.reject({message: `No permission for cookieStoreId: ${createProperties.cookieStoreId}`});
+          if (updateProperties.active !== null) {
+            if (updateProperties.active) {
+              tabbrowser.selectedTab = nativeTab;
+            } else {
+              // Not sure what to do here? Which tab should we select?
+            }
+          }
+          if (updateProperties.muted !== null) {
+            if (nativeTab.muted != updateProperties.muted) {
+              nativeTab.toggleMuteAudio(extension.uuid);
+            }
           }
+          if (updateProperties.pinned !== null) {
+            if (updateProperties.pinned) {
+              tabbrowser.pinTab(nativeTab);
+            } else {
+              tabbrowser.unpinTab(nativeTab);
+            }
+          }
+          // FIXME: highlighted/selected, openerTabId
+
+          return tabManager.convert(nativeTab);
+        },
 
-          let options = {};
-          if (createProperties.cookieStoreId) {
-            if (!global.isValidCookieStoreId(createProperties.cookieStoreId)) {
-              return Promise.reject({message: `Illegal cookieStoreId: ${createProperties.cookieStoreId}`});
+        async reload(tabId, reloadProperties) {
+          let nativeTab = getTabOrActive(tabId);
+
+          let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+          if (reloadProperties && reloadProperties.bypassCache) {
+            flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+          }
+          nativeTab.linkedBrowser.reloadWithFlags(flags);
+        },
+
+        async get(tabId) {
+          return tabManager.get(tabId).convert();
+        },
+
+        getCurrent() {
+          let tabData;
+          if (context.tabId) {
+            tabData = tabManager.get(context.tabId).convert();
+          }
+          return Promise.resolve(tabData);
+        },
+
+        async query(queryInfo) {
+          if (queryInfo.url !== null) {
+            if (!extension.hasPermission("tabs")) {
+              return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
             }
 
-            let privateWindow = PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser);
-            if (privateWindow && !global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
-              return Promise.reject({message: `Illegal to set non-private cookieStoreId in a private window`});
-            }
+            queryInfo = Object.assign({}, queryInfo);
+            queryInfo.url = new MatchPattern(queryInfo.url);
+          }
+
+          return Array.from(tabManager.query(queryInfo, context),
+                            tab => tab.convert());
+        },
+
+        async captureVisibleTab(windowId, options) {
+          let window = windowId == null ?
+            windowTracker.topWindow :
+            windowTracker.getWindow(windowId, context);
+
+          let tab = tabManager.wrapTab(window.gBrowser.selectedTab);
+          await tabListener.awaitTabReady(tab.nativeTab);
+
+          return tab.capture(context, options);
+        },
+
+        async detectLanguage(tabId) {
+          let tab = await promiseTabWhenReady(tabId);
+
+          return tab.sendMessage(context, "Extension:DetectLanguage");
+        },
+
+        async executeScript(tabId, details) {
+          let tab = await promiseTabWhenReady(tabId);
 
-            if (!privateWindow && global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
-              return Promise.reject({message: `Illegal to set private cookieStoreId in a non-private window`});
-            }
+          return tab.executeScript(context, details);
+        },
+
+        async insertCSS(tabId, details) {
+          let tab = await promiseTabWhenReady(tabId);
+
+          return tab.insertCSS(context, details);
+        },
+
+        async removeCSS(tabId, details) {
+          let tab = await promiseTabWhenReady(tabId);
 
-            if (global.isContainerCookieStoreId(createProperties.cookieStoreId)) {
-              let containerId = global.getContainerForCookieStoreId(createProperties.cookieStoreId);
-              if (!containerId) {
-                return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
-              }
+          return tab.removeCSS(context, details);
+        },
 
-              options.userContextId = containerId;
+        async move(tabIds, moveProperties) {
+          let index = moveProperties.index;
+          let tabsMoved = [];
+          if (!Array.isArray(tabIds)) {
+            tabIds = [tabIds];
+          }
+
+          let destinationWindow = null;
+          if (moveProperties.windowId !== null) {
+            destinationWindow = windowTracker.getWindow(moveProperties.windowId);
+            // Fail on an invalid window.
+            if (!destinationWindow) {
+              return Promise.reject({message: `Invalid window ID: ${moveProperties.windowId}`});
             }
           }
 
-          // Make sure things like about:blank and data: URIs never inherit,
-          // and instead always get a NullPrincipal.
-          options.disallowInheritPrincipal = true;
-
-          tabListener.initTabReady();
-          let nativeTab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
+          /*
+            Indexes are maintained on a per window basis so that a call to
+              move([tabA, tabB], {index: 0})
+                -> tabA to 0, tabB to 1 if tabA and tabB are in the same window
+              move([tabA, tabB], {index: 0})
+                -> tabA to 0, tabB to 0 if tabA and tabB are in different windows
+          */
+          let indexMap = new Map();
 
-          let active = true;
-          if (createProperties.active !== null) {
-            active = createProperties.active;
-          }
-          if (active) {
-            window.gBrowser.selectedTab = nativeTab;
-          }
+          let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
+          for (let nativeTab of tabs) {
+            // If the window is not specified, use the window from the tab.
+            let window = destinationWindow || nativeTab.ownerGlobal;
+            let gBrowser = window.gBrowser;
 
-          if (createProperties.index !== null) {
-            window.gBrowser.moveTabTo(nativeTab, createProperties.index);
-          }
+            let insertionPoint = indexMap.get(window) || index;
+            // If the index is -1 it should go to the end of the tabs.
+            if (insertionPoint == -1) {
+              insertionPoint = gBrowser.tabs.length;
+            }
 
-          if (createProperties.pinned) {
-            window.gBrowser.pinTab(nativeTab);
-          }
-
-          if (active && !url) {
-            window.focusAndSelectUrlBar();
-          }
+            // We can only move pinned tabs to a point within, or just after,
+            // the current set of pinned tabs. Unpinned tabs, likewise, can only
+            // be moved to a position after the current set of pinned tabs.
+            // Attempts to move a tab to an illegal position are ignored.
+            let numPinned = gBrowser._numPinnedTabs;
+            let ok = nativeTab.pinned ? insertionPoint <= numPinned : insertionPoint >= numPinned;
+            if (!ok) {
+              continue;
+            }
 
-          if (createProperties.url && createProperties.url !== window.BROWSER_NEW_TAB_URL) {
-            // We can't wait for a location change event for about:newtab,
-            // since it may be pre-rendered, in which case its initial
-            // location change event has already fired.
+            indexMap.set(window, insertionPoint + 1);
 
-            // Mark the tab as initializing, so that operations like
-            // `executeScript` wait until the requested URL is loaded in
-            // the tab before dispatching messages to the inner window
-            // that contains the URL we're attempting to load.
-            tabListener.initializingTabs.add(nativeTab);
+            if (nativeTab.ownerGlobal != window) {
+              // If the window we are moving the tab in is different, then move the tab
+              // to the new window.
+              nativeTab = gBrowser.adoptTab(nativeTab, insertionPoint, false);
+            } else {
+              // If the window we are moving is the same, just move the tab.
+              gBrowser.moveTabTo(nativeTab, insertionPoint);
+            }
+            tabsMoved.push(nativeTab);
           }
 
-          return tabManager.convert(nativeTab);
-        });
-      },
+          return tabsMoved.map(nativeTab => tabManager.convert(nativeTab));
+        },
+
+        duplicate(tabId) {
+          let nativeTab = tabTracker.getTab(tabId);
+
+          let gBrowser = nativeTab.ownerGlobal.gBrowser;
+          let newTab = gBrowser.duplicateTab(nativeTab);
 
-      async remove(tabs) {
-        if (!Array.isArray(tabs)) {
-          tabs = [tabs];
-        }
+          return new Promise(resolve => {
+            // We need to use SSTabRestoring because any attributes set before
+            // are ignored. SSTabRestored is too late and results in a jump in
+            // the UI. See http://bit.ly/session-store-api for more information.
+            newTab.addEventListener("SSTabRestoring", function() {
+              // As the tab is restoring, move it to the correct position.
 
-        for (let tabId of tabs) {
-          let nativeTab = tabTracker.getTab(tabId);
-          nativeTab.ownerGlobal.gBrowser.removeTab(nativeTab);
-        }
-      },
+              // Pinned tabs that are duplicated are inserted
+              // after the existing pinned tab and pinned.
+              if (nativeTab.pinned) {
+                gBrowser.pinTab(newTab);
+              }
+              gBrowser.moveTabTo(newTab, nativeTab._tPos + 1);
+            }, {once: true});
 
-      async update(tabId, updateProperties) {
-        let nativeTab = getTabOrActive(tabId);
+            newTab.addEventListener("SSTabRestored", function() {
+              // Once it has been restored, select it and return the promise.
+              gBrowser.selectedTab = newTab;
 
-        let tabbrowser = nativeTab.ownerGlobal.gBrowser;
+              resolve(tabManager.convert(newTab));
+            }, {once: true});
+          });
+        },
 
-        if (updateProperties.url !== null) {
-          let url = context.uri.resolve(updateProperties.url);
+        getZoom(tabId) {
+          let nativeTab = getTabOrActive(tabId);
 
-          if (!context.checkLoadURL(url, {dontReportErrors: true})) {
-            return Promise.reject({message: `Illegal URL: ${url}`});
-          }
+          let {ZoomManager} = nativeTab.ownerGlobal;
+          let zoom = ZoomManager.getZoomForBrowser(nativeTab.linkedBrowser);
+
+          return Promise.resolve(zoom);
+        },
 
-          nativeTab.linkedBrowser.loadURI(url);
-        }
+        setZoom(tabId, zoom) {
+          let nativeTab = getTabOrActive(tabId);
+
+          let {FullZoom, ZoomManager} = nativeTab.ownerGlobal;
 
-        if (updateProperties.active !== null) {
-          if (updateProperties.active) {
-            tabbrowser.selectedTab = nativeTab;
+          if (zoom === 0) {
+            // A value of zero means use the default zoom factor.
+            return FullZoom.reset(nativeTab.linkedBrowser);
+          } else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
+            FullZoom.setZoom(zoom, nativeTab.linkedBrowser);
           } else {
-            // Not sure what to do here? Which tab should we select?
-          }
-        }
-        if (updateProperties.muted !== null) {
-          if (nativeTab.muted != updateProperties.muted) {
-            nativeTab.toggleMuteAudio(extension.uuid);
-          }
-        }
-        if (updateProperties.pinned !== null) {
-          if (updateProperties.pinned) {
-            tabbrowser.pinTab(nativeTab);
-          } else {
-            tabbrowser.unpinTab(nativeTab);
-          }
-        }
-        // FIXME: highlighted/selected, openerTabId
-
-        return tabManager.convert(nativeTab);
-      },
-
-      async reload(tabId, reloadProperties) {
-        let nativeTab = getTabOrActive(tabId);
-
-        let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
-        if (reloadProperties && reloadProperties.bypassCache) {
-          flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
-        }
-        nativeTab.linkedBrowser.reloadWithFlags(flags);
-      },
-
-      async get(tabId) {
-        return tabManager.get(tabId).convert();
-      },
-
-      getCurrent() {
-        let tabData;
-        if (context.tabId) {
-          tabData = tabManager.get(context.tabId).convert();
-        }
-        return Promise.resolve(tabData);
-      },
-
-      async query(queryInfo) {
-        if (queryInfo.url !== null) {
-          if (!extension.hasPermission("tabs")) {
-            return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
+            return Promise.reject({
+              message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
+            });
           }
 
-          queryInfo = Object.assign({}, queryInfo);
-          queryInfo.url = new MatchPattern(queryInfo.url);
-        }
-
-        return Array.from(tabManager.query(queryInfo, context),
-                          tab => tab.convert());
-      },
+          return Promise.resolve();
+        },
 
-      async captureVisibleTab(windowId, options) {
-        let window = windowId == null ?
-          windowTracker.topWindow :
-          windowTracker.getWindow(windowId, context);
+        _getZoomSettings(tabId) {
+          let nativeTab = getTabOrActive(tabId);
 
-        let tab = tabManager.wrapTab(window.gBrowser.selectedTab);
-        await tabListener.awaitTabReady(tab.nativeTab);
-
-        return tab.capture(context, options);
-      },
+          let {FullZoom} = nativeTab.ownerGlobal;
 
-      async detectLanguage(tabId) {
-        let tab = await promiseTabWhenReady(tabId);
-
-        return tab.sendMessage(context, "Extension:DetectLanguage");
-      },
-
-      async executeScript(tabId, details) {
-        let tab = await promiseTabWhenReady(tabId);
-
-        return tab.executeScript(context, details);
-      },
+          return {
+            mode: "automatic",
+            scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
+            defaultZoomFactor: 1,
+          };
+        },
 
-      async insertCSS(tabId, details) {
-        let tab = await promiseTabWhenReady(tabId);
-
-        return tab.insertCSS(context, details);
-      },
+        getZoomSettings(tabId) {
+          return Promise.resolve(this._getZoomSettings(tabId));
+        },
 
-      async removeCSS(tabId, details) {
-        let tab = await promiseTabWhenReady(tabId);
+        setZoomSettings(tabId, settings) {
+          let nativeTab = getTabOrActive(tabId);
 
-        return tab.removeCSS(context, details);
-      },
+          let currentSettings = this._getZoomSettings(tabTracker.getId(nativeTab));
 
-      async move(tabIds, moveProperties) {
-        let index = moveProperties.index;
-        let tabsMoved = [];
-        if (!Array.isArray(tabIds)) {
-          tabIds = [tabIds];
-        }
+          if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
+            return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
+          }
+          return Promise.resolve();
+        },
 
-        let destinationWindow = null;
-        if (moveProperties.windowId !== null) {
-          destinationWindow = windowTracker.getWindow(moveProperties.windowId);
-          // Fail on an invalid window.
-          if (!destinationWindow) {
-            return Promise.reject({message: `Invalid window ID: ${moveProperties.windowId}`});
-          }
-        }
+        onZoomChange: new SingletonEventManager(context, "tabs.onZoomChange", fire => {
+          let getZoomLevel = browser => {
+            let {ZoomManager} = browser.ownerGlobal;
 
-        /*
-          Indexes are maintained on a per window basis so that a call to
-            move([tabA, tabB], {index: 0})
-              -> tabA to 0, tabB to 1 if tabA and tabB are in the same window
-            move([tabA, tabB], {index: 0})
-              -> tabA to 0, tabB to 0 if tabA and tabB are in different windows
-        */
-        let indexMap = new Map();
+            return ZoomManager.getZoomForBrowser(browser);
+          };
 
-        let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
-        for (let nativeTab of tabs) {
-          // If the window is not specified, use the window from the tab.
-          let window = destinationWindow || nativeTab.ownerGlobal;
-          let gBrowser = window.gBrowser;
+          // Stores the last known zoom level for each tab's browser.
+          // WeakMap[<browser> -> number]
+          let zoomLevels = new WeakMap();
 
-          let insertionPoint = indexMap.get(window) || index;
-          // If the index is -1 it should go to the end of the tabs.
-          if (insertionPoint == -1) {
-            insertionPoint = gBrowser.tabs.length;
-          }
-
-          // We can only move pinned tabs to a point within, or just after,
-          // the current set of pinned tabs. Unpinned tabs, likewise, can only
-          // be moved to a position after the current set of pinned tabs.
-          // Attempts to move a tab to an illegal position are ignored.
-          let numPinned = gBrowser._numPinnedTabs;
-          let ok = nativeTab.pinned ? insertionPoint <= numPinned : insertionPoint >= numPinned;
-          if (!ok) {
-            continue;
+          // Store the zoom level for all existing tabs.
+          for (let window of windowTracker.browserWindows()) {
+            for (let nativeTab of window.gBrowser.tabs) {
+              let browser = nativeTab.linkedBrowser;
+              zoomLevels.set(browser, getZoomLevel(browser));
+            }
           }
 
-          indexMap.set(window, insertionPoint + 1);
-
-          if (nativeTab.ownerGlobal != window) {
-            // If the window we are moving the tab in is different, then move the tab
-            // to the new window.
-            nativeTab = gBrowser.adoptTab(nativeTab, insertionPoint, false);
-          } else {
-            // If the window we are moving is the same, just move the tab.
-            gBrowser.moveTabTo(nativeTab, insertionPoint);
-          }
-          tabsMoved.push(nativeTab);
-        }
-
-        return tabsMoved.map(nativeTab => tabManager.convert(nativeTab));
-      },
-
-      duplicate(tabId) {
-        let nativeTab = tabTracker.getTab(tabId);
-
-        let gBrowser = nativeTab.ownerGlobal.gBrowser;
-        let newTab = gBrowser.duplicateTab(nativeTab);
-
-        return new Promise(resolve => {
-          // We need to use SSTabRestoring because any attributes set before
-          // are ignored. SSTabRestored is too late and results in a jump in
-          // the UI. See http://bit.ly/session-store-api for more information.
-          newTab.addEventListener("SSTabRestoring", function() {
-            // As the tab is restoring, move it to the correct position.
-
-            // Pinned tabs that are duplicated are inserted
-            // after the existing pinned tab and pinned.
-            if (nativeTab.pinned) {
-              gBrowser.pinTab(newTab);
-            }
-            gBrowser.moveTabTo(newTab, nativeTab._tPos + 1);
-          }, {once: true});
-
-          newTab.addEventListener("SSTabRestored", function() {
-            // Once it has been restored, select it and return the promise.
-            gBrowser.selectedTab = newTab;
-
-            resolve(tabManager.convert(newTab));
-          }, {once: true});
-        });
-      },
-
-      getZoom(tabId) {
-        let nativeTab = getTabOrActive(tabId);
-
-        let {ZoomManager} = nativeTab.ownerGlobal;
-        let zoom = ZoomManager.getZoomForBrowser(nativeTab.linkedBrowser);
-
-        return Promise.resolve(zoom);
-      },
-
-      setZoom(tabId, zoom) {
-        let nativeTab = getTabOrActive(tabId);
-
-        let {FullZoom, ZoomManager} = nativeTab.ownerGlobal;
-
-        if (zoom === 0) {
-          // A value of zero means use the default zoom factor.
-          return FullZoom.reset(nativeTab.linkedBrowser);
-        } else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
-          FullZoom.setZoom(zoom, nativeTab.linkedBrowser);
-        } else {
-          return Promise.reject({
-            message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
-          });
-        }
-
-        return Promise.resolve();
-      },
-
-      _getZoomSettings(tabId) {
-        let nativeTab = getTabOrActive(tabId);
-
-        let {FullZoom} = nativeTab.ownerGlobal;
-
-        return {
-          mode: "automatic",
-          scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
-          defaultZoomFactor: 1,
-        };
-      },
-
-      getZoomSettings(tabId) {
-        return Promise.resolve(this._getZoomSettings(tabId));
-      },
-
-      setZoomSettings(tabId, settings) {
-        let nativeTab = getTabOrActive(tabId);
-
-        let currentSettings = this._getZoomSettings(tabTracker.getId(nativeTab));
-
-        if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
-          return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
-        }
-        return Promise.resolve();
-      },
-
-      onZoomChange: new SingletonEventManager(context, "tabs.onZoomChange", fire => {
-        let getZoomLevel = browser => {
-          let {ZoomManager} = browser.ownerGlobal;
-
-          return ZoomManager.getZoomForBrowser(browser);
-        };
-
-        // Stores the last known zoom level for each tab's browser.
-        // WeakMap[<browser> -> number]
-        let zoomLevels = new WeakMap();
-
-        // Store the zoom level for all existing tabs.
-        for (let window of windowTracker.browserWindows()) {
-          for (let nativeTab of window.gBrowser.tabs) {
-            let browser = nativeTab.linkedBrowser;
+          let tabCreated = (eventName, event) => {
+            let browser = event.nativeTab.linkedBrowser;
             zoomLevels.set(browser, getZoomLevel(browser));
-          }
-        }
-
-        let tabCreated = (eventName, event) => {
-          let browser = event.nativeTab.linkedBrowser;
-          zoomLevels.set(browser, getZoomLevel(browser));
-        };
+          };
 
 
-        let zoomListener = event => {
-          let browser = event.originalTarget;
+          let zoomListener = event => {
+            let browser = event.originalTarget;
 
-          // For non-remote browsers, this event is dispatched on the document
-          // rather than on the <browser>.
-          if (browser instanceof Ci.nsIDOMDocument) {
-            browser = browser.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDocShell)
-                             .chromeEventHandler;
-          }
+            // For non-remote browsers, this event is dispatched on the document
+            // rather than on the <browser>.
+            if (browser instanceof Ci.nsIDOMDocument) {
+              browser = browser.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIDocShell)
+                               .chromeEventHandler;
+            }
 
-          let {gBrowser} = browser.ownerGlobal;
-          let nativeTab = gBrowser.getTabForBrowser(browser);
-          if (!nativeTab) {
-            // We only care about zoom events in the top-level browser of a tab.
-            return;
-          }
+            let {gBrowser} = browser.ownerGlobal;
+            let nativeTab = gBrowser.getTabForBrowser(browser);
+            if (!nativeTab) {
+              // We only care about zoom events in the top-level browser of a tab.
+              return;
+            }
 
-          let oldZoomFactor = zoomLevels.get(browser);
-          let newZoomFactor = getZoomLevel(browser);
+            let oldZoomFactor = zoomLevels.get(browser);
+            let newZoomFactor = getZoomLevel(browser);
 
-          if (oldZoomFactor != newZoomFactor) {
-            zoomLevels.set(browser, newZoomFactor);
+            if (oldZoomFactor != newZoomFactor) {
+              zoomLevels.set(browser, newZoomFactor);
 
-            let tabId = tabTracker.getId(nativeTab);
-            fire.async({
-              tabId,
-              oldZoomFactor,
-              newZoomFactor,
-              zoomSettings: self.tabs._getZoomSettings(tabId),
-            });
-          }
-        };
+              let tabId = tabTracker.getId(nativeTab);
+              fire.async({
+                tabId,
+                oldZoomFactor,
+                newZoomFactor,
+                zoomSettings: self.tabs._getZoomSettings(tabId),
+              });
+            }
+          };
 
-        tabTracker.on("tab-attached", tabCreated);
-        tabTracker.on("tab-created", tabCreated);
+          tabTracker.on("tab-attached", tabCreated);
+          tabTracker.on("tab-created", tabCreated);
 
-        windowTracker.addListener("FullZoomChange", zoomListener);
-        windowTracker.addListener("TextZoomChange", zoomListener);
-        return () => {
-          tabTracker.off("tab-attached", tabCreated);
-          tabTracker.off("tab-created", tabCreated);
+          windowTracker.addListener("FullZoomChange", zoomListener);
+          windowTracker.addListener("TextZoomChange", zoomListener);
+          return () => {
+            tabTracker.off("tab-attached", tabCreated);
+            tabTracker.off("tab-created", tabCreated);
 
-          windowTracker.removeListener("FullZoomChange", zoomListener);
-          windowTracker.removeListener("TextZoomChange", zoomListener);
-        };
-      }).api(),
-    },
-  };
-  return self;
-});
+            windowTracker.removeListener("FullZoomChange", zoomListener);
+            windowTracker.removeListener("TextZoomChange", zoomListener);
+          };
+        }).api(),
+      },
+    };
+    return self;
+  }
+};
--- a/browser/components/extensions/ext-url-overrides.js
+++ b/browser/components/extensions/ext-url-overrides.js
@@ -1,49 +1,53 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 
 // Bug 1320736 tracks creating a generic precedence manager for handling
 // multiple addons modifying the same properties, and bug 1330494 has been filed
 // to track utilizing this manager for chrome_url_overrides. Until those land,
 // the edge cases surrounding multiple addons using chrome_url_overrides will
 // be ignored and precedence will be first come, first serve.
 let overrides = {
   // A queue of extensions in line to override the newtab page (sorted oldest to newest).
   newtab: [],
 };
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_chrome_url_overrides", (type, directive, extension, manifest) => {
-  if (manifest.chrome_url_overrides.newtab) {
-    let newtab = manifest.chrome_url_overrides.newtab;
-    let url = extension.baseURI.resolve(newtab);
-
-    // Only set the newtab URL if no other extension is overriding it.
-    if (!overrides.newtab.length) {
-      aboutNewTabService.newTabURL = url;
-    }
+this.urlOverrides = class extends ExtensionAPI {
+  onManifestEntry(entryName) {
+    let {extension} = this;
+    let {manifest} = extension;
 
-    overrides.newtab.push({id: extension.id, url});
-  }
-});
+    if (manifest.chrome_url_overrides.newtab) {
+      let newtab = manifest.chrome_url_overrides.newtab;
+      let url = extension.baseURI.resolve(newtab);
 
-extensions.on("shutdown", (type, extension) => {
-  let i = overrides.newtab.findIndex(o => o.id === extension.id);
-  if (i !== -1) {
-    overrides.newtab.splice(i, 1);
+      // Only set the newtab URL if no other extension is overriding it.
+      if (!overrides.newtab.length) {
+        aboutNewTabService.newTabURL = url;
+      }
 
-    if (overrides.newtab.length) {
-      aboutNewTabService.newTabURL = overrides.newtab[0].url;
-    } else {
-      aboutNewTabService.resetNewTabURL();
+      overrides.newtab.push({id: extension.id, url});
     }
   }
-});
-/* eslint-enable mozilla/balanced-listeners */
+
+  onShutdown(reason) {
+    let {extension} = this;
+
+    let i = overrides.newtab.findIndex(o => o.id === extension.id);
+    if (i !== -1) {
+      overrides.newtab.splice(i, 1);
+
+      if (overrides.newtab.length) {
+        aboutNewTabService.newTabURL = overrides.newtab[0].url;
+      } else {
+        aboutNewTabService.resetNewTabURL();
+      }
+    }
+  }
+};
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -7,18 +7,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 /* globals TabBase, WindowBase, TabTrackerBase, WindowTrackerBase, TabManagerBase, WindowManagerBase */
 Cu.import("resource://gre/modules/ExtensionTabs.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
 var {
   ExtensionError,
   SingletonEventManager,
   defineLazyGetter,
 } = ExtensionUtils;
 
 let tabTracker;
 let windowTracker;
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -5,233 +5,234 @@
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   SingletonEventManager,
   promiseObserved,
 } = ExtensionUtils;
 
 function onXULFrameLoaderCreated({target}) {
   target.messageManager.sendAsyncMessage("AllowScriptsToClose", {});
 }
 
-extensions.registerSchemaAPI("windows", "addon_parent", context => {
-  let {extension} = context;
-
-  const {windowManager} = extension;
+this.windows = class extends ExtensionAPI {
+  getAPI(context) {
+    let {extension} = context;
 
-  return {
-    windows: {
-      onCreated:
-      new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
-        fire.async(windowManager.convert(window));
-      }).api(),
-
-      onRemoved:
-      new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
-        fire.async(windowTracker.getId(window));
-      }).api(),
+    const {windowManager} = extension;
 
-      onFocusChanged: new SingletonEventManager(context, "windows.onFocusChanged", fire => {
-        // Keep track of the last windowId used to fire an onFocusChanged event
-        let lastOnFocusChangedWindowId;
+    return {
+      windows: {
+        onCreated:
+        new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
+          fire.async(windowManager.convert(window));
+        }).api(),
 
-        let listener = event => {
-          // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
-          // event when switching focus between two Firefox windows.
-          Promise.resolve().then(() => {
-            let window = Services.focus.activeWindow;
-            let windowId = window ? windowTracker.getId(window) : Window.WINDOW_ID_NONE;
-            if (windowId !== lastOnFocusChangedWindowId) {
-              fire.async(windowId);
-              lastOnFocusChangedWindowId = windowId;
-            }
-          });
-        };
-        windowTracker.addListener("focus", listener);
-        windowTracker.addListener("blur", listener);
-        return () => {
-          windowTracker.removeListener("focus", listener);
-          windowTracker.removeListener("blur", listener);
-        };
-      }).api(),
+        onRemoved:
+        new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
+          fire.async(windowTracker.getId(window));
+        }).api(),
+
+        onFocusChanged: new SingletonEventManager(context, "windows.onFocusChanged", fire => {
+          // Keep track of the last windowId used to fire an onFocusChanged event
+          let lastOnFocusChangedWindowId;
 
-      get: function(windowId, getInfo) {
-        let window = windowTracker.getWindow(windowId, context);
-        if (!window) {
-          return Promise.reject({message: `Invalid window ID: ${windowId}`});
-        }
-        return Promise.resolve(windowManager.convert(window, getInfo));
-      },
-
-      getCurrent: function(getInfo) {
-        let window = context.currentWindow || windowTracker.topWindow;
-        return Promise.resolve(windowManager.convert(window, getInfo));
-      },
-
-      getLastFocused: function(getInfo) {
-        let window = windowTracker.topWindow;
-        return Promise.resolve(windowManager.convert(window, getInfo));
-      },
-
-      getAll: function(getInfo) {
-        let windows = Array.from(windowManager.getAll(), win => win.convert(getInfo));
+          let listener = event => {
+            // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
+            // event when switching focus between two Firefox windows.
+            Promise.resolve().then(() => {
+              let window = Services.focus.activeWindow;
+              let windowId = window ? windowTracker.getId(window) : Window.WINDOW_ID_NONE;
+              if (windowId !== lastOnFocusChangedWindowId) {
+                fire.async(windowId);
+                lastOnFocusChangedWindowId = windowId;
+              }
+            });
+          };
+          windowTracker.addListener("focus", listener);
+          windowTracker.addListener("blur", listener);
+          return () => {
+            windowTracker.removeListener("focus", listener);
+            windowTracker.removeListener("blur", listener);
+          };
+        }).api(),
 
-        return Promise.resolve(windows);
-      },
-
-      create: function(createData) {
-        let needResize = (createData.left !== null || createData.top !== null ||
-                          createData.width !== null || createData.height !== null);
-
-        if (needResize) {
-          if (createData.state !== null && createData.state != "normal") {
-            return Promise.reject({message: `"state": "${createData.state}" may not be combined with "left", "top", "width", or "height"`});
+        get: function(windowId, getInfo) {
+          let window = windowTracker.getWindow(windowId, context);
+          if (!window) {
+            return Promise.reject({message: `Invalid window ID: ${windowId}`});
           }
-          createData.state = "normal";
-        }
+          return Promise.resolve(windowManager.convert(window, getInfo));
+        },
+
+        getCurrent: function(getInfo) {
+          let window = context.currentWindow || windowTracker.topWindow;
+          return Promise.resolve(windowManager.convert(window, getInfo));
+        },
+
+        getLastFocused: function(getInfo) {
+          let window = windowTracker.topWindow;
+          return Promise.resolve(windowManager.convert(window, getInfo));
+        },
 
-        function mkstr(s) {
-          let result = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
-          result.data = s;
-          return result;
-        }
+        getAll: function(getInfo) {
+          let windows = Array.from(windowManager.getAll(), win => win.convert(getInfo));
+
+          return Promise.resolve(windows);
+        },
 
-        let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+        create: function(createData) {
+          let needResize = (createData.left !== null || createData.top !== null ||
+                            createData.width !== null || createData.height !== null);
 
-        if (createData.tabId !== null) {
-          if (createData.url !== null) {
-            return Promise.reject({message: "`tabId` may not be used in conjunction with `url`"});
+          if (needResize) {
+            if (createData.state !== null && createData.state != "normal") {
+              return Promise.reject({message: `"state": "${createData.state}" may not be combined with "left", "top", "width", or "height"`});
+            }
+            createData.state = "normal";
           }
 
-          if (createData.allowScriptsToClose) {
-            return Promise.reject({message: "`tabId` may not be used in conjunction with `allowScriptsToClose`"});
+          function mkstr(s) {
+            let result = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+            result.data = s;
+            return result;
           }
 
-          let tab = tabTracker.getTab(createData.tabId);
+          let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
 
-          // Private browsing tabs can only be moved to private browsing
-          // windows.
-          let incognito = PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser);
-          if (createData.incognito !== null && createData.incognito != incognito) {
-            return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
-          }
-          createData.incognito = incognito;
+          if (createData.tabId !== null) {
+            if (createData.url !== null) {
+              return Promise.reject({message: "`tabId` may not be used in conjunction with `url`"});
+            }
 
-          args.appendElement(tab, /* weak = */ false);
-        } else if (createData.url !== null) {
-          if (Array.isArray(createData.url)) {
-            let array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
-            for (let url of createData.url) {
-              array.appendElement(mkstr(url), /* weak = */ false);
+            if (createData.allowScriptsToClose) {
+              return Promise.reject({message: "`tabId` may not be used in conjunction with `allowScriptsToClose`"});
             }
-            args.appendElement(array, /* weak = */ false);
-          } else {
-            args.appendElement(mkstr(createData.url), /* weak = */ false);
-          }
-        } else {
-          args.appendElement(mkstr(aboutNewTabService.newTabURL), /* weak = */ false);
-        }
+
+            let tab = tabTracker.getTab(createData.tabId);
 
-        let features = ["chrome"];
-
-        if (createData.type === null || createData.type == "normal") {
-          features.push("dialog=no", "all");
-        } else {
-          // All other types create "popup"-type windows by default.
-          features.push("dialog", "resizable", "minimizable", "centerscreen", "titlebar", "close");
-        }
+            // Private browsing tabs can only be moved to private browsing
+            // windows.
+            let incognito = PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser);
+            if (createData.incognito !== null && createData.incognito != incognito) {
+              return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
+            }
+            createData.incognito = incognito;
 
-        if (createData.incognito !== null) {
-          if (createData.incognito) {
-            features.push("private");
+            args.appendElement(tab, /* weak = */ false);
+          } else if (createData.url !== null) {
+            if (Array.isArray(createData.url)) {
+              let array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+              for (let url of createData.url) {
+                array.appendElement(mkstr(url), /* weak = */ false);
+              }
+              args.appendElement(array, /* weak = */ false);
+            } else {
+              args.appendElement(mkstr(createData.url), /* weak = */ false);
+            }
           } else {
-            features.push("non-private");
+            args.appendElement(mkstr(aboutNewTabService.newTabURL), /* weak = */ false);
           }
-        }
-
-        let {allowScriptsToClose, url} = createData;
-        if (allowScriptsToClose === null) {
-          allowScriptsToClose = typeof url === "string" && url.startsWith("moz-extension://");
-        }
-
-        let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
-                                            features.join(","), args);
-
-        let win = windowManager.getWrapper(window);
-        win.updateGeometry(createData);
 
-        // TODO: focused, type
+          let features = ["chrome"];
 
-        return new Promise(resolve => {
-          window.addEventListener("load", function() {
-            if (["maximized", "normal"].includes(createData.state)) {
-              window.document.documentElement.setAttribute("sizemode", createData.state);
-            }
-            resolve(promiseObserved("browser-delayed-startup-finished", win => win == window));
-          }, {once: true});
-        }).then(() => {
-          // Some states only work after delayed-startup-finished
-          if (["minimized", "fullscreen", "docked"].includes(createData.state)) {
-            win.state = createData.state;
+          if (createData.type === null || createData.type == "normal") {
+            features.push("dialog=no", "all");
+          } else {
+            // All other types create "popup"-type windows by default.
+            features.push("dialog", "resizable", "minimizable", "centerscreen", "titlebar", "close");
           }
-          if (allowScriptsToClose) {
-            for (let {linkedBrowser} of window.gBrowser.tabs) {
-              onXULFrameLoaderCreated({target: linkedBrowser});
-              linkedBrowser.addEventListener( // eslint-disable-line mozilla/balanced-listeners
-                                             "XULFrameLoaderCreated", onXULFrameLoaderCreated);
+
+          if (createData.incognito !== null) {
+            if (createData.incognito) {
+              features.push("private");
+            } else {
+              features.push("non-private");
             }
           }
-          return win.convert({populate: true});
-        });
-      },
 
-      update: function(windowId, updateInfo) {
-        if (updateInfo.state !== null && updateInfo.state != "normal") {
-          if (updateInfo.left !== null || updateInfo.top !== null ||
-              updateInfo.width !== null || updateInfo.height !== null) {
-            return Promise.reject({message: `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`});
+          let {allowScriptsToClose, url} = createData;
+          if (allowScriptsToClose === null) {
+            allowScriptsToClose = typeof url === "string" && url.startsWith("moz-extension://");
           }
-        }
+
+          let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
+                                              features.join(","), args);
+
+          let win = windowManager.getWrapper(window);
+          win.updateGeometry(createData);
+
+          // TODO: focused, type
 
-        let win = windowManager.get(windowId, context);
-        if (updateInfo.focused) {
-          Services.focus.activeWindow = win.window;
-        }
-
-        if (updateInfo.state !== null) {
-          win.state = updateInfo.state;
-        }
+          return new Promise(resolve => {
+            window.addEventListener("load", function() {
+              if (["maximized", "normal"].includes(createData.state)) {
+                window.document.documentElement.setAttribute("sizemode", createData.state);
+              }
+              resolve(promiseObserved("browser-delayed-startup-finished", win => win == window));
+            }, {once: true});
+          }).then(() => {
+            // Some states only work after delayed-startup-finished
+            if (["minimized", "fullscreen", "docked"].includes(createData.state)) {
+              win.state = createData.state;
+            }
+            if (allowScriptsToClose) {
+              for (let {linkedBrowser} of window.gBrowser.tabs) {
+                onXULFrameLoaderCreated({target: linkedBrowser});
+                linkedBrowser.addEventListener( // eslint-disable-line mozilla/balanced-listeners
+                                               "XULFrameLoaderCreated", onXULFrameLoaderCreated);
+              }
+            }
+            return win.convert({populate: true});
+          });
+        },
 
-        if (updateInfo.drawAttention) {
-          // Bug 1257497 - Firefox can't cancel attention actions.
-          win.window.getAttention();
-        }
+        update: function(windowId, updateInfo) {
+          if (updateInfo.state !== null && updateInfo.state != "normal") {
+            if (updateInfo.left !== null || updateInfo.top !== null ||
+                updateInfo.width !== null || updateInfo.height !== null) {
+              return Promise.reject({message: `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`});
+            }
+          }
 
-        win.updateGeometry(updateInfo);
+          let win = windowManager.get(windowId, context);
+          if (updateInfo.focused) {
+            Services.focus.activeWindow = win.window;
+          }
 
-        // TODO: All the other properties, focused=false...
+          if (updateInfo.state !== null) {
+            win.state = updateInfo.state;
+          }
 
-        return Promise.resolve(win.convert());
-      },
+          if (updateInfo.drawAttention) {
+            // Bug 1257497 - Firefox can't cancel attention actions.
+            win.window.getAttention();
+          }
 
-      remove: function(windowId) {
-        let window = windowTracker.getWindow(windowId, context);
-        window.close();
+          win.updateGeometry(updateInfo);
+
+          // TODO: All the other properties, focused=false...
+
+          return Promise.resolve(win.convert());
+        },
+
+        remove: function(windowId) {
+          let window = windowTracker.getWindow(windowId, context);
+          window.close();
 
-        return new Promise(resolve => {
-          let listener = () => {
-            windowTracker.removeListener("domwindowclosed", listener);
-            resolve();
-          };
-          windowTracker.addListener("domwindowclosed", listener);
-        });
+          return new Promise(resolve => {
+            let listener = () => {
+              windowTracker.removeListener("domwindowclosed", listener);
+              resolve();
+            };
+            windowTracker.addListener("domwindowclosed", listener);
+          });
+        },
       },
-    },
-  };
-});
+    };
+  }
+};
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -1,51 +1,6 @@
-# scripts
-category webextension-scripts bookmarks chrome://browser/content/ext-bookmarks.js
-category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
-category webextension-scripts browsingData chrome://browser/content/ext-browsingData.js
-category webextension-scripts chrome-settings-overrides chrome://browser/content/ext-chrome-settings-overrides.js
-category webextension-scripts commands chrome://browser/content/ext-commands.js
-category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
-category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
-category webextension-scripts devtools chrome://browser/content/ext-devtools.js
-category webextension-scripts devtools-inspectedWindow chrome://browser/content/ext-devtools-inspectedWindow.js
-category webextension-scripts devtools-network chrome://browser/content/ext-devtools-network.js
-category webextension-scripts devtools-panels chrome://browser/content/ext-devtools-panels.js
-category webextension-scripts history chrome://browser/content/ext-history.js
-category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
-category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
-category webextension-scripts sessions chrome://browser/content/ext-sessions.js
-category webextension-scripts sidebarAction chrome://browser/content/ext-sidebarAction.js
-category webextension-scripts tabs chrome://browser/content/ext-tabs.js
-category webextension-scripts url-overrides chrome://browser/content/ext-url-overrides.js
+category webextension-scripts browser chrome://browser/content/ext-browser.js
 category webextension-scripts utils chrome://browser/content/ext-utils.js
-category webextension-scripts windows chrome://browser/content/ext-windows.js
-
-# scripts specific for devtools extension contexts.
-category webextension-scripts-devtools devtools-inspectedWindow chrome://browser/content/ext-c-devtools-inspectedWindow.js
-category webextension-scripts-devtools devtools-panels chrome://browser/content/ext-c-devtools-panels.js
+category webextension-scripts-devtools browser chrome://browser/content/ext-c-browser.js
+category webextension-scripts-addon browser chrome://browser/content/ext-c-browser.js
 
-# scripts that must run in the same process as addon code.
-category webextension-scripts-addon contextMenus chrome://browser/content/ext-c-contextMenus.js
-category webextension-scripts-addon omnibox chrome://browser/content/ext-c-omnibox.js
-category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
-
-# schemas
-category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
-category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
-category webextension-schemas browsing_data chrome://browser/content/schemas/browsing_data.json
-category webextension-schemas chrome_settings_overrides chrome://browser/content/schemas/chrome_settings_overrides.json
-category webextension-schemas commands chrome://browser/content/schemas/commands.json
-category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
 category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
-category webextension-schemas devtools chrome://browser/content/schemas/devtools.json
-category webextension-schemas devtools_inspected_window chrome://browser/content/schemas/devtools_inspected_window.json
-category webextension-schemas devtools_network chrome://browser/content/schemas/devtools_network.json
-category webextension-schemas devtools_panels chrome://browser/content/schemas/devtools_panels.json
-category webextension-schemas history chrome://browser/content/schemas/history.json
-category webextension-schemas omnibox chrome://browser/content/schemas/omnibox.json
-category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
-category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
-category webextension-schemas sidebar_action chrome://browser/content/schemas/sidebar_action.json
-category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
-category webextension-schemas url_overrides chrome://browser/content/schemas/url_overrides.json
-category webextension-schemas windows chrome://browser/content/schemas/windows.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -8,32 +8,34 @@ browser.jar:
     content/browser/extension-mac.css
     content/browser/extension-mac-panel.css
 #endif
 #ifdef XP_WIN
     content/browser/extension-win-panel.css
 #endif
     content/browser/extension.svg
     content/browser/ext-bookmarks.js
+    content/browser/ext-browser.js
     content/browser/ext-browserAction.js
     content/browser/ext-browsingData.js
     content/browser/ext-chrome-settings-overrides.js
     content/browser/ext-commands.js
     content/browser/ext-contextMenus.js
-    content/browser/ext-desktop-runtime.js
     content/browser/ext-devtools.js
     content/browser/ext-devtools-inspectedWindow.js
     content/browser/ext-devtools-network.js
     content/browser/ext-devtools-panels.js
     content/browser/ext-history.js
     content/browser/ext-omnibox.js
     content/browser/ext-pageAction.js
     content/browser/ext-sessions.js
     content/browser/ext-sidebarAction.js
     content/browser/ext-tabs.js
     content/browser/ext-url-overrides.js
     content/browser/ext-utils.js
     content/browser/ext-windows.js
+    content/browser/ext-c-browser.js
     content/browser/ext-c-contextMenus.js
     content/browser/ext-c-devtools-inspectedWindow.js
     content/browser/ext-c-devtools-panels.js
+    content/browser/ext-c-devtools.js
     content/browser/ext-c-omnibox.js
     content/browser/ext-c-tabs.js
--- a/browser/components/extensions/schemas/context_menus.json
+++ b/browser/components/extensions/schemas/context_menus.json
@@ -215,84 +215,16 @@
             "name": "callback",
             "optional": true,
             "description": "Called when the item has been created in the browser. If there were any problems creating the item, details will be available in $(ref:runtime.lastError).",
             "parameters": []
           }
         ]
       },
       {
-        "name": "createInternal",
-        "type": "function",
-        "allowedContexts": ["addon_parent_only"],
-        "async": "callback",
-        "description": "Identical to contextMenus.create, except: the 'id' field is required and allows an integer, 'onclick' is not allowed, and the method is async (and the return value is not a menu item ID).",
-        "parameters": [
-          {
-            "type": "object",
-            "name": "createProperties",
-            "properties": {
-              "type": {
-                "$ref": "ItemType",
-                "optional": true
-              },
-              "id": {
-                "choices": [
-                  { "type": "integer" },
-                  { "type": "string" }
-                ]
-              },
-              "title": {
-                "type": "string",
-                "optional": true
-              },
-              "checked": {
-                "type": "boolean",
-                "optional": true
-              },
-              "contexts": {
-                "type": "array",
-                "items": {
-                  "$ref": "ContextType"
-                },
-                "minItems": 1,
-                "optional": true
-              },
-              "parentId": {
-                "choices": [
-                  { "type": "integer" },
-                  { "type": "string" }
-                ],
-                "optional": true
-              },
-              "documentUrlPatterns": {
-                "type": "array",
-                "items": {"type": "string"},
-                "optional": true
-              },
-              "targetUrlPatterns": {
-                "type": "array",
-                "items": {"type": "string"},
-                "optional": true
-              },
-              "enabled": {
-                "type": "boolean",
-                "optional": true
-              }
-            }
-          },
-          {
-            "type": "function",
-            "name": "callback",
-            "optional": true,
-            "parameters": []
-          }
-        ]
-      },
-      {
         "name": "update",
         "type": "function",
         "description": "Updates a previously created context menu item.",
         "async": "callback",
         "parameters": [
           {
             "choices": [
               { "type": "integer" },
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -20,16 +20,17 @@ add_task(function* () {
         title: "Click me!",
         contexts: ["image"],
       });
       browser.contextMenus.create({
         id: "clickme-page",
         title: "Click me!",
         contexts: ["page"],
       });
+      browser.contextMenus.onClicked.addListener(() => {});
       browser.test.notifyPass();
     },
   });
 
   yield extension.startup();
   yield extension.awaitFinish();
 
   let contentAreaContextMenu = yield openContextMenu("#img1");
new file mode 100644
--- /dev/null
+++ b/config/check_js_opcode.py
@@ -0,0 +1,43 @@
+# vim: set ts=8 sts=4 et sw=4 tw=99:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#----------------------------------------------------------------------------
+# This script checks bytecode documentation in js/src/vm/Opcodes.h
+#----------------------------------------------------------------------------
+
+from __future__ import print_function
+
+import os
+import sys
+
+scriptname = os.path.basename(__file__);
+topsrcdir = os.path.dirname(os.path.dirname(__file__))
+
+def log_pass(text):
+    print('TEST-PASS | {} | {}'.format(scriptname, text))
+
+def log_fail(text):
+    print('TEST-UNEXPECTED-FAIL | {} | {}'.format(scriptname, text))
+
+def check_opcode():
+    sys.path.insert(0, os.path.join(topsrcdir, 'js', 'src', 'vm'))
+    import opcode
+
+    try:
+        opcode.get_opcodes(topsrcdir)
+    except Exception as e:
+        log_fail(e.args[0])
+
+    log_pass('ok')
+    return True
+
+def main():
+    if not check_opcode():
+        sys.exit(1)
+
+    sys.exit(0)
+
+if __name__ == '__main__':
+    main()
--- a/gfx/2d/Matrix.h
+++ b/gfx/2d/Matrix.h
@@ -1231,16 +1231,26 @@ public:
   {
     typedef Matrix4x4Typed<TargetUnits, SourceUnits> InvertedMatrix;
     InvertedMatrix clone = InvertedMatrix::FromUnknownMatrix(ToUnknownMatrix());
     DebugOnly<bool> inverted = clone.Invert();
     MOZ_ASSERT(inverted, "Attempted to get the inverse of a non-invertible matrix");
     return clone;
   }
 
+  Maybe<Matrix4x4Typed<TargetUnits, SourceUnits>> MaybeInverse() const
+  {
+    typedef Matrix4x4Typed<TargetUnits, SourceUnits> InvertedMatrix;
+    InvertedMatrix clone = InvertedMatrix::FromUnknownMatrix(ToUnknownMatrix());
+    if (clone.Invert()) {
+      return Some(clone);
+    }
+    return Nothing();
+  }
+  
   void Normalize()
   {
       for (int i = 0; i < 4; i++) {
           for (int j = 0; j < 4; j++) {
               (*this)[i][j] /= (*this)[3][3];
          }
       }
   }
--- a/gfx/layers/apz/src/HitTestingTreeNode.cpp
+++ b/gfx/layers/apz/src/HitTestingTreeNode.cpp
@@ -250,17 +250,21 @@ Maybe<LayerPoint>
 HitTestingTreeNode::Untransform(const ParentLayerPoint& aPoint) const
 {
   // convert into Layer coordinate space
   LayerToParentLayerMatrix4x4 transform = mTransform *
       CompleteAsyncTransform(
         mApzc
       ? mApzc->GetCurrentAsyncTransformWithOverscroll(AsyncPanZoomController::NORMAL)
       : AsyncTransformComponentMatrix());
-  return UntransformBy(transform.Inverse(), aPoint);
+  Maybe<ParentLayerToLayerMatrix4x4> inverse = transform.MaybeInverse();
+  if (inverse) {
+    return UntransformBy(inverse.ref(), aPoint);
+  }
+  return Nothing();
 }
 
 HitTestResult
 HitTestingTreeNode::HitTest(const ParentLayerPoint& aPoint) const
 {
   // This should only ever get called if the point is inside the clip region
   // for this node.
   MOZ_ASSERT(!IsOutsideClip(aPoint));
--- a/gfx/layers/d3d11/CompositorD3D11.cpp
+++ b/gfx/layers/d3d11/CompositorD3D11.cpp
@@ -27,16 +27,17 @@
 
 #include "mozilla/EnumeratedArray.h"
 #include "mozilla/Telemetry.h"
 #include "BlendShaderConstants.h"
 
 #include "D3D11ShareHandleImage.h"
 
 #include <VersionHelpers.h> // For IsWindows8OrGreater
+#include <winsdkver.h>
 
 namespace mozilla {
 
 using namespace gfx;
 
 namespace layers {
 
 static bool CanUsePartialPresents(ID3D11Device* aDevice);
@@ -486,21 +487,24 @@ CompositorD3D11::Initialize(nsCString* c
 
   {
     RefPtr<IDXGIFactory> dxgiFactory;
     dxgiAdapter->GetParent(IID_PPV_ARGS(dxgiFactory.StartAssignment()));
 
     RefPtr<IDXGIFactory2> dxgiFactory2;
     hr = dxgiFactory->QueryInterface((IDXGIFactory2**)getter_AddRefs(dxgiFactory2));
 
+#if (_WIN32_WINDOWS_MAXVER >= 0x0A00)
     if (gfxPrefs::Direct3D11UseDoubleBuffering() && SUCCEEDED(hr) && dxgiFactory2 && IsWindows10OrGreater()) {
       // DXGI_SCALING_NONE is not available on Windows 7 with Platform Update.
       // This looks awful for things like the awesome bar and browser window resizing
       // so we don't use a flip buffer chain here. When using EFFECT_SEQUENTIAL
       // it looks like windows doesn't stretch the surface when resizing.
+      // We chose not to run this before Windows 10 because it appears sometimes this breaks
+      // our ability to test ASAP compositing.
       RefPtr<IDXGISwapChain1> swapChain;
 
       DXGI_SWAP_CHAIN_DESC1 swapDesc;
       ::ZeroMemory(&swapDesc, sizeof(swapDesc));
       swapDesc.Width = 0;
       swapDesc.Height = 0;
       swapDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
       swapDesc.SampleDesc.Count = 1;
@@ -522,17 +526,19 @@ CompositorD3D11::Initialize(nsCString* c
         *out_failureReason = "FEATURE_FAILURE_D3D11_SWAP_CHAIN";
         return false;
       }
 
       DXGI_RGBA color = { 1.0f, 1.0f, 1.0f, 1.0f };
       swapChain->SetBackgroundColor(&color);
 
       mSwapChain = swapChain;
-    } else {
+    } else
+#endif
+    {
       DXGI_SWAP_CHAIN_DESC swapDesc;
       ::ZeroMemory(&swapDesc, sizeof(swapDesc));
       swapDesc.BufferDesc.Width = 0;
       swapDesc.BufferDesc.Height = 0;
       swapDesc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
       swapDesc.BufferDesc.RefreshRate.Numerator = 60;
       swapDesc.BufferDesc.RefreshRate.Denominator = 1;
       swapDesc.SampleDesc.Count = 1;
--- a/js/src/Makefile.in
+++ b/js/src/Makefile.in
@@ -88,24 +88,27 @@ check-style::
 	(cd $(srcdir) && $(PYTHON) $(topsrcdir)/config/check_spidermonkey_style.py);
 
 check-masm::
 	(cd $(srcdir) && $(PYTHON) $(topsrcdir)/config/check_macroassembler_style.py);
 
 check-js-msg::
 	(cd $(topsrcdir) && $(PYTHON) $(topsrcdir)/config/check_js_msg_encoding.py);
 
+check-opcode::
+	(cd $(topsrcdir) && $(PYTHON) $(topsrcdir)/config/check_js_opcode.py);
+
 check-jit-test::
 	$(JITTEST_SANITIZER_ENV) $(wildcard $(RUN_TEST_PROGRAM)) $(PYTHON) -u $(srcdir)/jit-test/jit_test.py \
 	        --no-slow --no-progress --format=automation --jitflags=all \
 			$(JITTEST_VALGRIND_FLAG) \
 			$(JITTEST_EXTRA_ARGS) \
 	        $(DIST)/bin/$(JS_SHELL_NAME)$(BIN_SUFFIX) $(JITTEST_TEST_ARGS)
 
-check:: check-style check-masm check-js-msg
+check:: check-style check-masm check-js-msg check-opcode
 
 check-jstests:
 	$(wildcard $(RUN_TEST_PROGRAM)) $(PYTHON) -u $(srcdir)/tests/jstests.py \
 		--no-progress --format=automation --timeout 300 \
 		$(JSTESTS_EXTRA_ARGS) \
 		$(DIST)/bin/$(JS_SHELL_NAME)$(BIN_SUFFIX)
 
 # FIXME:
--- a/js/src/vm/Opcodes.h
+++ b/js/src/vm/Opcodes.h
@@ -426,17 +426,17 @@ 1234567890123456789012345678901234567890
      *          v[n], v[n-1], ..., v[1], v[0], v[n]
      */ \
     macro(JSOP_DUPAT,     44, "dupat",      NULL,         4,  0,  1,  JOF_UINT24) \
     \
     /*
      * Push a well-known symbol onto the operand stack.
      *   Category: Literals
      *   Type: Constants
-     *   Operands: uint8_t n, the JS::SymbolCode of the symbol to use
+     *   Operands: uint8_t symbol (the JS::SymbolCode of the symbol to use)
      *   Stack: => symbol
      */ \
     macro(JSOP_SYMBOL,    45, "symbol",     NULL,         2,  0,  1,  JOF_UINT8) \
     \
     /*
      * Pops the top of stack value and attempts to delete the given property
      * from it. Pushes 'true' onto success, else throws a TypeError per strict
      * mode property-deletion requirements.
@@ -581,17 +581,17 @@ 1234567890123456789012345678901234567890
      *   Stack: => val
      */ \
     macro(JSOP_DOUBLE,    60, "double",     NULL,         5,  0,  1, JOF_DOUBLE) \
     /*
      * Pushes string constant onto the stack.
      *   Category: Literals
      *   Type: Constants
      *   Operands: uint32_t atomIndex
-     *   Stack: => string
+     *   Stack: => atom
      */ \
     macro(JSOP_STRING,    61, "string",     NULL,         5,  0,  1, JOF_ATOM) \
     /*
      * Pushes '0' onto the stack.
      *   Category: Literals
      *   Type: Constants
      *   Operands:
      *   Stack: => 0
--- a/js/src/vm/make_opcode_doc.py
+++ b/js/src/vm/make_opcode_doc.py
@@ -8,302 +8,38 @@
     Output is written to stdout and should be pasted into the following
     MDN page:
     https://developer.mozilla.org/en-US/docs/SpiderMonkey/Internals/Bytecode
 """
 
 from __future__ import print_function
 import re
 import sys
+
+import os
+sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
+import opcode
+
 from xml.sax.saxutils import escape
 
 SOURCE_BASE = 'http://dxr.mozilla.org/mozilla-central/source'
 
-def error(message):
-    print("Error: {message}".format(message=message), file=sys.stderr)
-    sys.exit(1)
-
-quoted_pat = re.compile(r"([^A-Za-z0-9]|^)'([^']+)'")
-js_pat = re.compile(r"([^A-Za-z0-9]|^)(JS[A-Z0-9_\*]+)")
-def codify(text):
-    text = re.sub(quoted_pat, '\\1<code>\\2</code>', text)
-    text = re.sub(js_pat, '\\1<code>\\2</code>', text)
-
-    return text
-
-space_star_space_pat = re.compile('^\s*\* ?', re.M)
-def get_comment_body(comment):
-    return re.sub(space_star_space_pat, '', comment).split('\n')
-
-def get_stack_count(stack):
-    if stack == "":
-        return 0
-    if '...' in stack:
-        return -1
-    return len(stack.split(','))
-
-def parse_index(comment):
-    index = []
-    current_types = None
-    category_name = ''
-    category_pat = re.compile('\[([^\]]+)\]')
-    for line in get_comment_body(comment):
-        m = category_pat.search(line)
-        if m:
-            category_name = m.group(1)
-            if category_name == 'Index':
-                continue
-            current_types = []
-            index.append((category_name, current_types))
-        else:
-            type_name = line.strip()
-            if type_name and current_types is not None:
-                current_types.append((type_name, []))
-
-    return index
-
-class OpcodeInfo:
-    def __init__(self):
-        self.name = ''
-        self.value = ''
-        self.length = ''
-        self.length_override = ''
-        self.nuses = ''
-        self.nuses_override = ''
-        self.ndefs = ''
-        self.ndefs_override = ''
-        self.flags = ''
-        self.operands = ''
-        self.stack_uses = ''
-        self.stack_defs = ''
-
-        self.desc = ''
-
-        self.category_name = ''
-        self.type_name = ''
-
-        self.group = []
-        self.sort_key = ''
-
-def find_by_name(list, name):
-    for (n, body) in list:
-        if n == name:
-            return body
-
-    return None
-
-def add_to_index(index, opcode):
-    types = find_by_name(index, opcode.category_name)
-    if types is None:
-        error("Category is not listed in index: "
-              "{name}".format(name=opcode.category_name))
-    opcodes = find_by_name(types, opcode.type_name)
-    if opcodes is None:
-        if opcode.type_name:
-            error("Type is not listed in {category}: "
-                  "{name}".format(category=opcode.category_name,
-                                  name=opcode.type_name))
-        types.append((opcode.type_name, [opcode]))
-        return
-
-    opcodes.append(opcode)
-
-def format_desc(descs):
-    current_type = ''
-    desc = ''
-    for (type, line) in descs:
-        if type != current_type:
-            if current_type:
-                desc += '</{name}>\n'.format(name=current_type)
-            current_type = type
-            if type:
-                desc += '<{name}>'.format(name=current_type)
-        if current_type:
-            desc += line + "\n"
-    if current_type:
-        desc += '</{name}>'.format(name=current_type)
-
-    return desc
-
-tag_pat = re.compile('^\s*[A-Za-z]+:\s*|\s*$')
-def get_tag_value(line):
-    return re.sub(tag_pat, '', line)
-
-def get_opcodes(dir):
-    iter_pat = re.compile(r"/\*(.*?)\*/"  # either a documentation comment...
-                          r"|"
-                          r"macro\("      # or a macro(...) call
-                                 r"([^,]+),\s*"     # op
-                                 r"([0-9]+),\s*"    # val
-                                 r"[^,]+,\s*"       # name
-                                 r"[^,]+,\s*"       # image
-                                 r"([0-9\-]+),\s*"  # length
-                                 r"([0-9\-]+),\s*"  # nuses
-                                 r"([0-9\-]+),\s*"  # ndefs
-                                 r"([^\)]+)"        # format
-                          r"\)", re.S)
-    stack_pat = re.compile('^(.*?)\s*=>\s*(.*?)$')
-
-    index = []
-
-    opcode = OpcodeInfo()
-    merged = opcode
-
-    with open('{dir}/js/src/vm/Opcodes.h'.format(dir=dir), 'r') as f:
-        data = f.read()
-
-    for m in re.finditer(iter_pat, data):
-        comment = m.group(1)
-        name = m.group(2)
-
-        if comment:
-            if '[Index]' in comment:
-                index = parse_index(comment)
-                continue
-
-            if 'Operands:' not in comment:
-                continue
-
-            state = 'desc'
-            stack = ''
-            descs = []
-
-            for line in get_comment_body(comment):
-                if line.startswith('  Category:'):
-                    state = 'category'
-                    opcode.category_name = get_tag_value(line)
-                elif line.startswith('  Type:'):
-                    state = 'type'
-                    opcode.type_name = get_tag_value(line)
-                elif line.startswith('  Operands:'):
-                    state = 'operands'
-                    opcode.operands = get_tag_value(line)
-                elif line.startswith('  Stack:'):
-                    state = 'stack'
-                    stack = get_tag_value(line)
-                elif line.startswith('  len:'):
-                    state = 'len'
-                    opcode.length_override = get_tag_value(line)
-                elif line.startswith('  nuses:'):
-                    state = 'nuses'
-                    opcode.nuses_override = get_tag_value(line)
-                elif line.startswith('  ndefs:'):
-                    state = 'ndefs'
-                    opcode.ndefs_override = get_tag_value(line)
-                elif state == 'desc':
-                    if line.startswith(' '):
-                        descs.append(('pre', escape(line[1:])))
-                    else:
-                        line = line.strip()
-                        if line == '':
-                            descs.append(('', line))
-                        else:
-                            descs.append(('p', codify(escape(line))))
-                elif line.startswith('  '):
-                    if state == 'operands':
-                        opcode.operands += line.strip()
-                    elif state == 'stack':
-                        stack += line.strip()
-                    elif state == 'len':
-                        opcode.length_override += line.strip()
-                    elif state == 'nuses':
-                        opcode.nuses_override += line.strip()
-                    elif state == 'ndefs':
-                        opcode.ndefs_override += line.strip()
-
-            opcode.desc = format_desc(descs)
-
-            m2 = stack_pat.search(stack)
-            if m2:
-                opcode.stack_uses = m2.group(1)
-                opcode.stack_defs = m2.group(2)
-
-            merged = opcode
-        elif name and not name.startswith('JSOP_UNUSED'):
-            opcode.name = name
-            opcode.value = int(m.group(3))
-            opcode.length = m.group(4)
-            opcode.nuses = m.group(5)
-            opcode.ndefs = m.group(6)
-
-            flags = []
-            for flag in m.group(7).split('|'):
-                if flag != 'JOF_BYTE':
-                    flags.append(flag.replace('JOF_', ''))
-            opcode.flags = ', '.join(flags)
-
-            if merged == opcode:
-                opcode.sort_key = opcode.name
-                if opcode.category_name == '':
-                    error("Category is not specified for "
-                          "{name}".format(name=opcode.name))
-                add_to_index(index, opcode)
-            else:
-                if merged.length != opcode.length:
-                    error("length should be same for merged section: "
-                          "{value1}({name1}) != "
-                          "{value2}({name2})".format(name1=merged.name,
-                                                     value1=merged.length,
-                                                     name2=opcode.name,
-                                                     value2=opcode.length))
-                if merged.nuses != opcode.nuses:
-                    error("nuses should be same for merged section: "
-                          "{value1}({name1}) != "
-                          "{value2}({name2})".format(name1=merged.name,
-                                                     value1=merged.nuses,
-                                                     name2=opcode.name,
-                                                     value2=opcode.nuses))
-                if merged.ndefs != opcode.ndefs:
-                    error("ndefs should be same for merged section: "
-                          "{value1}({name1}) != "
-                          "{value2}({name2})".format(name1=merged.name,
-                                                     value1=merged.ndefs,
-                                                     name2=opcode.name,
-                                                     value2=opcode.ndefs))
-                merged.group.append(opcode)
-                if opcode.name < merged.name:
-                    merged.sort_key = opcode.name
-
-            # Verify stack notation.
-            nuses = int(merged.nuses)
-            ndefs = int(merged.ndefs)
-
-            stack_nuses = get_stack_count(merged.stack_uses)
-            stack_ndefs = get_stack_count(merged.stack_defs)
-
-            if nuses != -1 and stack_nuses != -1 and nuses != stack_nuses:
-                error("nuses should match stack notation: {name}: "
-                      "{nuses} != {stack_nuses} "
-                      "(stack_nuses)".format(name=name,
-                                             nuses=nuses,
-                                             stack_nuses=stack_nuses,
-                                             stack_uses=merged.stack_uses))
-            if ndefs != -1 and stack_ndefs != -1 and ndefs != stack_ndefs:
-                error("ndefs should match stack notation: {name}: "
-                      "{ndefs} != {stack_ndefs} "
-                      "(stack_ndefs)".format(name=name,
-                                             ndefs=ndefs,
-                                             stack_ndefs=stack_ndefs,
-                                             stack_defs=merged.stack_defs))
-
-            opcode = OpcodeInfo()
-
-    return index
-
 def override(value, override_value):
     if override_value != '':
         return override_value
 
     return value
 
 def format_flags(flags):
-    if flags == '':
+    flags = filter(lambda x: x != 'JOF_BYTE', flags)
+    if len(flags) == 0:
         return ''
 
-    return ' ({flags})'.format(flags=flags)
+    flags = map(lambda x: x.replace('JOF_', ''), flags)
+    return ' ({flags})'.format(flags=', '.join(flags))
 
 def print_opcode(opcode):
     names_template = '{name} [-{nuses}, +{ndefs}]{flags}'
     opcodes = sorted([opcode] + opcode.group,
                      key=lambda opcode: opcode.name)
     names = map(lambda code: names_template.format(name=escape(code.name),
                                                    nuses=override(code.nuses,
                                                                   opcode.nuses_override),
@@ -389,10 +125,16 @@ def print_doc(index):
             print('</dl>')
 
 if __name__ == '__main__':
     if len(sys.argv) < 2:
         print("Usage: make_opcode_doc.py PATH_TO_MOZILLA_CENTRAL",
               file=sys.stderr)
         sys.exit(1)
     dir = sys.argv[1]
-    index = get_opcodes(dir)
+
+    try:
+        index, _ = opcode.get_opcodes(dir)
+    except Exception as e:
+        print("Error: {}".format(e.args[0]), file=sys.stderr)
+        sys.exit(1)
+
     print_doc(index)
new file mode 100644
--- /dev/null
+++ b/js/src/vm/opcode.py
@@ -0,0 +1,357 @@
+#!/usr/bin/python -B
+
+from __future__ import print_function
+import re
+import sys
+from xml.sax.saxutils import escape
+
+quoted_pat = re.compile(r"([^A-Za-z0-9]|^)'([^']+)'")
+js_pat = re.compile(r"([^A-Za-z0-9]|^)(JS[A-Z0-9_\*]+)")
+def codify(text):
+    text = re.sub(quoted_pat, '\\1<code>\\2</code>', text)
+    text = re.sub(js_pat, '\\1<code>\\2</code>', text)
+
+    return text
+
+space_star_space_pat = re.compile('^\s*\* ?', re.M)
+def get_comment_body(comment):
+    return re.sub(space_star_space_pat, '', comment).split('\n')
+
+quote_pat = re.compile('"([^"]+)"')
+str_pat = re.compile('js_([^_]+)_str')
+def parse_name(s):
+    m = quote_pat.search(s)
+    if m:
+        return m.group(1)
+    m = str_pat.search(s)
+    if m:
+        return m.group(1)
+    return s
+
+csv_pat = re.compile(', *')
+def parse_csv(s):
+    a = csv_pat.split(s)
+    if len(a) == 1 and a[0] == '':
+        return []
+    return a
+
+def get_stack_count(stack):
+    if stack == '':
+        return 0
+    if '...' in stack:
+        return -1
+    return len(stack.split(','))
+
+def parse_index(comment):
+    index = []
+    current_types = None
+    category_name = ''
+    category_pat = re.compile('\[([^\]]+)\]')
+    for line in get_comment_body(comment):
+        m = category_pat.search(line)
+        if m:
+            category_name = m.group(1)
+            if category_name == 'Index':
+                continue
+            current_types = []
+            index.append((category_name, current_types))
+        else:
+            type_name = line.strip()
+            if type_name and current_types is not None:
+                current_types.append((type_name, []))
+
+    return index
+
+# Holds the information stored in the comment with the following format:
+#   /*
+#    * {desc}
+#    *   Category: {category_name}
+#    *   Type: {type_name}
+#    *   Operands: {operands}
+#    *   Stack: {stack_uses} => {stack_defs}
+#    *   length: {length_override}
+#    *   nuses: {nuses_override}
+#    *   ndefs: {ndefs_override}
+#    */
+class CommentInfo:
+    def __init__(self):
+        self.desc = ''
+        self.category_name = ''
+        self.type_name = ''
+        self.operands = ''
+        self.stack_uses = ''
+        self.stack_defs = ''
+        self.length_override = ''
+        self.nuses_override = ''
+        self.ndefs_override = ''
+
+# Holds the information stored in the macro with the following format:
+#   macro({name}, {value}, {display_name}, {image}, {length}, {nuses}, {ndefs},
+#         {flags})
+# and the information from CommentInfo.
+class OpcodeInfo:
+    def __init__(self, comment_info):
+        self.name = ''
+        self.value = ''
+        self.display_name = ''
+        self.image = ''
+        self.length = ''
+        self.nuses = ''
+        self.ndefs = ''
+        self.flags = ''
+
+        self.operands_array = []
+        self.stack_uses_array = []
+        self.stack_defs_array = []
+
+        self.desc = comment_info.desc
+        self.category_name = comment_info.category_name
+        self.type_name = comment_info.type_name
+        self.operands = comment_info.operands
+        self.operands_array = comment_info.operands_array
+        self.stack_uses = comment_info.stack_uses
+        self.stack_uses_array = comment_info.stack_uses_array
+        self.stack_defs = comment_info.stack_defs
+        self.stack_defs_array = comment_info.stack_defs_array
+        self.length_override = comment_info.length_override
+        self.nuses_override = comment_info.nuses_override
+        self.ndefs_override = comment_info.ndefs_override
+
+        # List of OpcodeInfo that corresponds to macros after this.
+        #   /*
+        #    * comment
+        #    */
+        #   macro(JSOP_SUB, ...)
+        #   macro(JSOP_MUL, ...)
+        #   macro(JSOP_DIV, ...)
+        self.group = []
+
+        self.sort_key = ''
+
+def find_by_name(list, name):
+    for (n, body) in list:
+        if n == name:
+            return body
+
+    return None
+
+def add_to_index(index, opcode):
+    types = find_by_name(index, opcode.category_name)
+    if types is None:
+        raise Exception('Category is not listed in index: '
+                        '{name}'.format(name=opcode.category_name))
+    opcodes = find_by_name(types, opcode.type_name)
+    if opcodes is None:
+        if opcode.type_name:
+            raise Exception('Type is not listed in {category}: '
+                            '{name}'.format(category=opcode.category_name,
+                                            name=opcode.type_name))
+        types.append((opcode.type_name, [opcode]))
+        return
+
+    opcodes.append(opcode)
+
+def format_desc(descs):
+    current_type = ''
+    desc = ''
+    for (type, line) in descs:
+        if type != current_type:
+            if current_type:
+                desc += '</{name}>\n'.format(name=current_type)
+            current_type = type
+            if type:
+                desc += '<{name}>'.format(name=current_type)
+        if current_type:
+            desc += line + '\n'
+    if current_type:
+        desc += '</{name}>'.format(name=current_type)
+
+    return desc
+
+tag_pat = re.compile('^\s*[A-Za-z]+:\s*|\s*$')
+def get_tag_value(line):
+    return re.sub(tag_pat, '', line)
+
+def get_opcodes(dir):
+    iter_pat = re.compile(r"/\*(.*?)\*/"  # either a documentation comment...
+                          r"|"
+                          r"macro\("      # or a macro(...) call
+                                 r"(?P<name>[^,]+),\s*"
+                                 r"(?P<value>[0-9]+),\s*"
+                                 r"(?P<display_name>[^,]+,)\s*"
+                                 r"(?P<image>[^,]+),\s*"
+                                 r"(?P<length>[0-9\-]+),\s*"
+                                 r"(?P<nuses>[0-9\-]+),\s*"
+                                 r"(?P<ndefs>[0-9\-]+),\s*"
+                                 r"(?P<flags>[^\)]+)"
+                          r"\)", re.S)
+    stack_pat = re.compile(r"^(?P<uses>.*?)"
+                           r"\s*=>\s*"
+                           r"(?P<defs>.*?)$")
+
+    opcodes = dict()
+    index = []
+
+    with open('{dir}/js/src/vm/Opcodes.h'.format(dir=dir), 'r') as f:
+        data = f.read()
+
+    comment_info = None
+    opcode = None
+
+    # The first opcode after the comment.
+    group_head = None
+
+    for m in re.finditer(iter_pat, data):
+        comment = m.group(1)
+        name = m.group('name')
+
+        if comment:
+            if '[Index]' in comment:
+                index = parse_index(comment)
+                continue
+
+            if 'Operands:' not in comment:
+                continue
+
+            group_head = None
+
+            comment_info = CommentInfo()
+
+            state = 'desc'
+            stack = ''
+            descs = []
+
+            for line in get_comment_body(comment):
+                if line.startswith('  Category:'):
+                    state = 'category'
+                    comment_info.category_name = get_tag_value(line)
+                elif line.startswith('  Type:'):
+                    state = 'type'
+                    comment_info.type_name = get_tag_value(line)
+                elif line.startswith('  Operands:'):
+                    state = 'operands'
+                    comment_info.operands = get_tag_value(line)
+                elif line.startswith('  Stack:'):
+                    state = 'stack'
+                    stack = get_tag_value(line)
+                elif line.startswith('  len:'):
+                    state = 'len'
+                    comment_info.length_override = get_tag_value(line)
+                elif line.startswith('  nuses:'):
+                    state = 'nuses'
+                    comment_info.nuses_override = get_tag_value(line)
+                elif line.startswith('  ndefs:'):
+                    state = 'ndefs'
+                    comment_info.ndefs_override = get_tag_value(line)
+                elif state == 'desc':
+                    if line.startswith(' '):
+                        descs.append(('pre', escape(line[1:])))
+                    else:
+                        line = line.strip()
+                        if line == '':
+                            descs.append(('', line))
+                        else:
+                            descs.append(('p', codify(escape(line))))
+                elif line.startswith('  '):
+                    if state == 'operands':
+                        comment_info.operands += line.strip()
+                    elif state == 'stack':
+                        stack += line.strip()
+                    elif state == 'len':
+                        comment_info.length_override += line.strip()
+                    elif state == 'nuses':
+                        comment_info.nuses_override += line.strip()
+                    elif state == 'ndefs':
+                        comment_info.ndefs_override += line.strip()
+
+            comment_info.desc = format_desc(descs)
+
+            comment_info.operands_array = parse_csv(comment_info.operands)
+            comment_info.stack_uses_array = parse_csv(comment_info.stack_uses)
+            comment_info.stack_defs_array = parse_csv(comment_info.stack_defs)
+
+            m2 = stack_pat.search(stack)
+            if m2:
+                comment_info.stack_uses = m2.group('uses')
+                comment_info.stack_defs = m2.group('defs')
+        elif name and not name.startswith('JSOP_UNUSED'):
+            opcode = OpcodeInfo(comment_info)
+
+            opcode.name = name
+            opcode.value = int(m.group('value'))
+            opcode.display_name = parse_name(m.group('display_name'))
+            opcode.image = parse_name(m.group('image'))
+            opcode.length = m.group('length')
+            opcode.nuses = m.group('nuses')
+            opcode.ndefs = m.group('ndefs')
+            opcode.flags = m.group('flags').split('|')
+
+            if not group_head:
+                group_head = opcode
+
+                opcode.sort_key = opcode.name
+                if opcode.category_name == '':
+                    raise Exception('Category is not specified for '
+                                    '{name}'.format(name=opcode.name))
+                add_to_index(index, opcode)
+            else:
+                if group_head.length != opcode.length:
+                    raise Exception('length should be same for opcodes of the'
+                                    ' same group: '
+                                    '{value1}({name1}) != '
+                                    '{value2}({name2})'.format(
+                                        name1=group_head.name,
+                                        value1=group_head.length,
+                                        name2=opcode.name,
+                                        value2=opcode.length))
+                if group_head.nuses != opcode.nuses:
+                    raise Exception('nuses should be same for opcodes of the'
+                                    ' same group: '
+                                    '{value1}({name1}) != '
+                                    '{value2}({name2})'.format(
+                                        name1=group_head.name,
+                                        value1=group_head.nuses,
+                                        name2=opcode.name,
+                                        value2=opcode.nuses))
+                if group_head.ndefs != opcode.ndefs:
+                    raise Exception('ndefs should be same for opcodes of the'
+                                    ' same group: '
+                                    '{value1}({name1}) != '
+                                    '{value2}({name2})'.format(
+                                        name1=group_head.name,
+                                        value1=group_head.ndefs,
+                                        name2=opcode.name,
+                                        value2=opcode.ndefs))
+
+                group_head.group.append(opcode)
+
+                if opcode.name < group_head.name:
+                    group_head.sort_key = opcode.name
+
+            opcodes[name] = opcode
+
+            # Verify stack notation.
+            nuses = int(opcode.nuses)
+            ndefs = int(opcode.ndefs)
+
+            stack_nuses = get_stack_count(opcode.stack_uses)
+            stack_ndefs = get_stack_count(opcode.stack_defs)
+
+            if nuses != -1 and stack_nuses != -1 and nuses != stack_nuses:
+                raise Exception('nuses should match stack notation: {name}: '
+                                '{nuses} != {stack_nuses} '
+                                '(stack_nuses)'.format(
+                                    name=name,
+                                    nuses=nuses,
+                                    stack_nuses=stack_nuses,
+                                    stack_uses=opcode.stack_uses))
+            if ndefs != -1 and stack_ndefs != -1 and ndefs != stack_ndefs:
+                raise Exception('ndefs should match stack notation: {name}: '
+                                '{ndefs} != {stack_ndefs} '
+                                '(stack_ndefs)'.format(
+                                    name=name,
+                                    ndefs=ndefs,
+                                    stack_ndefs=stack_ndefs,
+                                    stack_defs=opcode.stack_defs))
+
+    return index, opcodes
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -1721,21 +1721,21 @@ nsIFrame::UpdateVisibilitySynchronously(
     return;
   }
 
   if (presShell->AssumeAllFramesVisible()) {
     presShell->EnsureFrameInApproximatelyVisibleList(this);
     return;
   }
 
-  bool visible = true;
+  bool visible = StyleVisibility()->IsVisible();
   nsIFrame* f = GetParent();
   nsRect rect = GetRectRelativeToSelf();
   nsIFrame* rectFrame = this;
-  while (f) {
+  while (f && visible) {
     nsIScrollableFrame* sf = do_QueryFrame(f);
     if (sf) {
       nsRect transformedRect =
         nsLayoutUtils::TransformFrameRectToAncestor(rectFrame, rect, f);
       if (!sf->IsRectNearlyVisible(transformedRect)) {
         visible = false;
         break;
       }
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/ext-android.js
@@ -0,0 +1,22 @@
+"use strict";
+
+extensions.registerModules({
+  pageAction: {
+    url: "chrome://browser/content/ext-pageAction.js",
+    schema: "chrome://browser/content/schemas/page_action.json",
+    scopes: ["addon_parent"],
+    manifest: ["page_action"],
+    paths: [
+      ["pageAction"],
+    ],
+  },
+  tabs: {
+    url: "chrome://browser/content/ext-tabs.js",
+    schema: "chrome://browser/content/schemas/tabs.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["tabs"],
+    ],
+  },
+});
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/ext-c-android.js
@@ -0,0 +1,11 @@
+"use strict";
+
+extensions.registerModules({
+  tabs: {
+    url: "chrome://browser/content/ext-c-tabs.js",
+    scopes: ["addon_child"],
+    paths: [
+      ["tabs"],
+    ],
+  },
+});
--- a/mobile/android/components/extensions/ext-c-tabs.js
+++ b/mobile/android/components/extensions/ext-c-tabs.js
@@ -1,35 +1,37 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-extensions.registerSchemaAPI("tabs", "addon_child", context => {
-  return {
-    tabs: {
-      connect: function(tabId, connectInfo) {
-        let name = "";
-        if (connectInfo && connectInfo.name !== null) {
-          name = connectInfo.name;
-        }
-        let recipient = {
-          extensionId: context.extension.id,
-          tabId,
-        };
-        if (connectInfo && connectInfo.frameId !== null) {
-          recipient.frameId = connectInfo.frameId;
-        }
-        return context.messenger.connect(context.messageManager, name, recipient);
+this.tabs = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      tabs: {
+        connect: function(tabId, connectInfo) {
+          let name = "";
+          if (connectInfo && connectInfo.name !== null) {
+            name = connectInfo.name;
+          }
+          let recipient = {
+            extensionId: context.extension.id,
+            tabId,
+          };
+          if (connectInfo && connectInfo.frameId !== null) {
+            recipient.frameId = connectInfo.frameId;
+          }
+          return context.messenger.connect(context.messageManager, name, recipient);
+        },
+
+        sendMessage: function(tabId, message, options, responseCallback) {
+          let recipient = {
+            extensionId: context.extension.id,
+            tabId: tabId,
+          };
+          if (options && options.frameId !== null) {
+            recipient.frameId = options.frameId;
+          }
+          return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
+        },
       },
-
-      sendMessage: function(tabId, message, options, responseCallback) {
-        let recipient = {
-          extensionId: context.extension.id,
-          tabId: tabId,
-        };
-        if (options && options.frameId !== null) {
-          recipient.frameId = options.frameId;
-        }
-        return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
-      },
-    },
-  };
-});
+    };
+  }
+};
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -107,65 +107,70 @@ PageAction.prototype = {
     return this.popupUrl;
   },
 
   shutdown() {
     this.hide();
   },
 };
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
-  let pageAction = new PageAction(manifest.page_action, extension);
-  pageActionMap.set(extension, pageAction);
-});
+this.pageAction = class extends ExtensionAPI {
+  onManifestEntry(entryName) {
+    let {extension} = this;
+    let {manifest} = extension;
+
+    let pageAction = new PageAction(manifest.page_action, extension);
+    pageActionMap.set(extension, pageAction);
+  }
 
-extensions.on("shutdown", (type, extension) => {
-  if (pageActionMap.has(extension)) {
-    pageActionMap.get(extension).shutdown();
-    pageActionMap.delete(extension);
+  onShutdown(reason) {
+    let {extension} = this;
+
+    if (pageActionMap.has(extension)) {
+      pageActionMap.get(extension).shutdown();
+      pageActionMap.delete(extension);
+    }
   }
-});
-/* eslint-enable mozilla/balanced-listeners */
 
-extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
-  const {extension} = context;
-  const {tabManager} = extension;
+  getAPI(context) {
+    const {extension} = context;
+    const {tabManager} = extension;
 
-  return {
-    pageAction: {
-      onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
-        let listener = (event, tab) => {
-          fire.async(tabManager.convert(tab));
-        };
-        pageActionMap.get(extension).on("click", listener);
-        return () => {
-          pageActionMap.get(extension).off("click", listener);
-        };
-      }).api(),
+    return {
+      pageAction: {
+        onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
+          let listener = (event, tab) => {
+            fire.async(tabManager.convert(tab));
+          };
+          pageActionMap.get(extension).on("click", listener);
+          return () => {
+            pageActionMap.get(extension).off("click", listener);
+          };
+        }).api(),
 
-      show(tabId) {
-        return pageActionMap.get(extension)
-                            .show(tabId, context)
-                            .then(() => {});
-      },
+        show(tabId) {
+          return pageActionMap.get(extension)
+                              .show(tabId, context)
+                              .then(() => {});
+        },
 
-      hide(tabId) {
-        pageActionMap.get(extension).hide(tabId);
-        return Promise.resolve();
-      },
+        hide(tabId) {
+          pageActionMap.get(extension).hide(tabId);
+          return Promise.resolve();
+        },
 
-      setPopup(details) {
-        // TODO: Use the Tabs API to get the tab from details.tabId.
-        let tab = null;
-        let url = details.popup && context.uri.resolve(details.popup);
-        pageActionMap.get(extension).setPopup(tab, url);
+        setPopup(details) {
+          // TODO: Use the Tabs API to get the tab from details.tabId.
+          let tab = null;
+          let url = details.popup && context.uri.resolve(details.popup);
+          pageActionMap.get(extension).setPopup(tab, url);
+        },
+
+        getPopup(details) {
+          // TODO: Use the Tabs API to get the tab from details.tabId.
+          let tab = null;
+          let popup = pageActionMap.get(extension).getPopup(tab);
+          return Promise.resolve(popup);
+        },
       },
-
-      getPopup(details) {
-        // TODO: Use the Tabs API to get the tab from details.tabId.
-        let tab = null;
-        let popup = pageActionMap.get(extension).getPopup(tab);
-        return Promise.resolve(popup);
-      },
-    },
-  };
-});
+    };
+  }
+};
--- a/mobile/android/components/extensions/ext-tabs.js
+++ b/mobile/android/components/extensions/ext-tabs.js
@@ -10,18 +10,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
 var {
   SingletonEventManager,
 } = ExtensionUtils;
 
 // This function is pretty tightly tied to Extension.jsm.
 // Its job is to fill in the |tab| property of the sender.
 function getSender(extension, target, sender) {
   let tabId;
@@ -123,314 +121,316 @@ let tabListener = {
         this.initTabReady();
         this.tabReadyPromises.set(nativeTab, deferred);
       }
     }
     return deferred.promise;
   },
 };
 
-extensions.registerSchemaAPI("tabs", "addon_parent", context => {
-  let {extension} = context;
+this.tabs = class extends ExtensionAPI {
+  getAPI(context) {
+    let {extension} = context;
+
+    let {tabManager} = extension;
 
-  let {tabManager} = extension;
-
-  function getTabOrActive(tabId) {
-    if (tabId !== null) {
-      return tabTracker.getTab(tabId);
+    function getTabOrActive(tabId) {
+      if (tabId !== null) {
+        return tabTracker.getTab(tabId);
+      }
+      return tabTracker.activeTab;
     }
-    return tabTracker.activeTab;
-  }
 
-  async function promiseTabWhenReady(tabId) {
-    let tab;
-    if (tabId !== null) {
-      tab = tabManager.get(tabId);
-    } else {
-      tab = tabManager.getWrapper(tabTracker.activeTab);
+    async function promiseTabWhenReady(tabId) {
+      let tab;
+      if (tabId !== null) {
+        tab = tabManager.get(tabId);
+      } else {
+        tab = tabManager.getWrapper(tabTracker.activeTab);
+      }
+
+      await tabListener.awaitTabReady(tab.nativeTab);
+
+      return tab;
     }
 
-    await tabListener.awaitTabReady(tab.nativeTab);
+    let self = {
+      tabs: {
+        onActivated: new GlobalEventManager(context, "tabs.onActivated", "Tab:Selected", (fire, data) => {
+          let tab = tabManager.get(data.id);
 
-    return tab;
-  }
+          fire.async({tabId: tab.id, windowId: tab.windowId});
+        }).api(),
 
-  let self = {
-    tabs: {
-      onActivated: new GlobalEventManager(context, "tabs.onActivated", "Tab:Selected", (fire, data) => {
-        let tab = tabManager.get(data.id);
+        onCreated: new SingletonEventManager(context, "tabs.onCreated", fire => {
+          let listener = (eventName, event) => {
+            fire.async(tabManager.convert(event.nativeTab));
+          };
 
-        fire.async({tabId: tab.id, windowId: tab.windowId});
-      }).api(),
-
-      onCreated: new SingletonEventManager(context, "tabs.onCreated", fire => {
-        let listener = (eventName, event) => {
-          fire.async(tabManager.convert(event.nativeTab));
-        };
+          tabTracker.on("tab-created", listener);
+          return () => {
+            tabTracker.off("tab-created", listener);
+          };
+        }).api(),
 
-        tabTracker.on("tab-created", listener);
-        return () => {
-          tabTracker.off("tab-created", listener);
-        };
-      }).api(),
+        /**
+         * Since multiple tabs currently can't be highlighted, onHighlighted
+         * essentially acts an alias for self.tabs.onActivated but returns
+         * the tabId in an array to match the API.
+         * @see  https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
+        */
+        onHighlighted: new GlobalEventManager(context, "tabs.onHighlighted", "Tab:Selected", (fire, data) => {
+          let tab = tabManager.get(data.id);
+
+          fire.async({tabIds: [tab.id], windowId: tab.windowId});
+        }).api(),
 
-      /**
-       * Since multiple tabs currently can't be highlighted, onHighlighted
-       * essentially acts an alias for self.tabs.onActivated but returns
-       * the tabId in an array to match the API.
-       * @see  https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
-      */
-      onHighlighted: new GlobalEventManager(context, "tabs.onHighlighted", "Tab:Selected", (fire, data) => {
-        let tab = tabManager.get(data.id);
+        onAttached: new SingletonEventManager(context, "tabs.onAttached", fire => {
+          return () => {};
+        }).api(),
+
+        onDetached: new SingletonEventManager(context, "tabs.onDetached", fire => {
+          return () => {};
+        }).api(),
 
-        fire.async({tabIds: [tab.id], windowId: tab.windowId});
-      }).api(),
+        onRemoved: new SingletonEventManager(context, "tabs.onRemoved", fire => {
+          let listener = (eventName, event) => {
+            fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
+          };
 
-      onAttached: new SingletonEventManager(context, "tabs.onAttached", fire => {
-        return () => {};
-      }).api(),
-
-      onDetached: new SingletonEventManager(context, "tabs.onDetached", fire => {
-        return () => {};
-      }).api(),
+          tabTracker.on("tab-removed", listener);
+          return () => {
+            tabTracker.off("tab-removed", listener);
+          };
+        }).api(),
 
-      onRemoved: new SingletonEventManager(context, "tabs.onRemoved", fire => {
-        let listener = (eventName, event) => {
-          fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
-        };
+        onReplaced: new SingletonEventManager(context, "tabs.onReplaced", fire => {
+          return () => {};
+        }).api(),
 
-        tabTracker.on("tab-removed", listener);
-        return () => {
-          tabTracker.off("tab-removed", listener);
-        };
-      }).api(),
+        onMoved: new SingletonEventManager(context, "tabs.onMoved", fire => {
+          return () => {};
+        }).api(),
+
+        onUpdated: new SingletonEventManager(context, "tabs.onUpdated", fire => {
+          const restricted = ["url", "favIconUrl", "title"];
 
-      onReplaced: new SingletonEventManager(context, "tabs.onReplaced", fire => {
-        return () => {};
-      }).api(),
-
-      onMoved: new SingletonEventManager(context, "tabs.onMoved", fire => {
-        return () => {};
-      }).api(),
-
-      onUpdated: new SingletonEventManager(context, "tabs.onUpdated", fire => {
-        const restricted = ["url", "favIconUrl", "title"];
+          function sanitize(extension, changeInfo) {
+            let result = {};
+            let nonempty = false;
+            for (let prop in changeInfo) {
+              if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
+                nonempty = true;
+                result[prop] = changeInfo[prop];
+              }
+            }
+            return [nonempty, result];
+          }
 
-        function sanitize(extension, changeInfo) {
-          let result = {};
-          let nonempty = false;
-          for (let prop in changeInfo) {
-            if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
-              nonempty = true;
-              result[prop] = changeInfo[prop];
+          let fireForTab = (tab, changed) => {
+            let [needed, changeInfo] = sanitize(extension, changed);
+            if (needed) {
+              fire.async(tab.id, changeInfo, tab.convert());
             }
-          }
-          return [nonempty, result];
-        }
+          };
 
-        let fireForTab = (tab, changed) => {
-          let [needed, changeInfo] = sanitize(extension, changed);
-          if (needed) {
-            fire.async(tab.id, changeInfo, tab.convert());
-          }
-        };
+          let listener = event => {
+            let needed = [];
+            let nativeTab;
+            switch (event.type) {
+              case "DOMTitleChanged": {
+                let {BrowserApp} = getBrowserWindow(event.target.ownerGlobal);
 
-        let listener = event => {
-          let needed = [];
-          let nativeTab;
-          switch (event.type) {
-            case "DOMTitleChanged": {
-              let {BrowserApp} = getBrowserWindow(event.target.ownerGlobal);
+                nativeTab = BrowserApp.getTabForWindow(event.target.ownerGlobal);
+                needed.push("title");
+                break;
+              }
 
-              nativeTab = BrowserApp.getTabForWindow(event.target.ownerGlobal);
-              needed.push("title");
-              break;
+              case "DOMAudioPlaybackStarted":
+              case "DOMAudioPlaybackStopped": {
+                let {BrowserApp} = event.target.ownerGlobal;
+                nativeTab = BrowserApp.getTabForBrowser(event.originalTarget);
+                needed.push("audible");
+                break;
+              }
             }
 
-            case "DOMAudioPlaybackStarted":
-            case "DOMAudioPlaybackStopped": {
-              let {BrowserApp} = event.target.ownerGlobal;
-              nativeTab = BrowserApp.getTabForBrowser(event.originalTarget);
-              needed.push("audible");
-              break;
+            if (!nativeTab) {
+              return;
+            }
+
+            let tab = tabManager.getWrapper(nativeTab);
+            let changeInfo = {};
+            for (let prop of needed) {
+              changeInfo[prop] = tab[prop];
+            }
+
+            fireForTab(tab, changeInfo);
+          };
+
+          let statusListener = ({browser, status, url}) => {
+            let {BrowserApp} = browser.ownerGlobal;
+            let nativeTab = BrowserApp.getTabForBrowser(browser);
+            if (nativeTab) {
+              let changed = {status};
+              if (url) {
+                changed.url = url;
+              }
+
+              fireForTab(tabManager.wrapTab(nativeTab), changed);
+            }
+          };
+
+          windowTracker.addListener("status", statusListener);
+          windowTracker.addListener("DOMTitleChanged", listener);
+          return () => {
+            windowTracker.removeListener("status", statusListener);
+            windowTracker.removeListener("DOMTitleChanged", listener);
+          };
+        }).api(),
+
+        async create(createProperties) {
+          let window = createProperties.windowId !== null ?
+            windowTracker.getWindow(createProperties.windowId, context) :
+            windowTracker.topWindow;
+
+          let {BrowserApp} = window;
+          let url;
+
+          if (createProperties.url !== null) {
+            url = context.uri.resolve(createProperties.url);
+
+            if (!context.checkLoadURL(url, {dontReportErrors: true})) {
+              return Promise.reject({message: `Illegal URL: ${url}`});
             }
           }
 
-          if (!nativeTab) {
-            return;
+          let options = {};
+
+          let active = true;
+          if (createProperties.active !== null) {
+            active = createProperties.active;
+          }
+          options.selected = active;
+
+          if (createProperties.index !== null) {
+            options.tabIndex = createProperties.index;
           }
 
-          let tab = tabManager.getWrapper(nativeTab);
-          let changeInfo = {};
-          for (let prop of needed) {
-            changeInfo[prop] = tab[prop];
+          // Make sure things like about:blank and data: URIs never inherit,
+          // and instead always get a NullPrincipal.
+          options.disallowInheritPrincipal = true;
+
+          tabListener.initTabReady();
+          let nativeTab = BrowserApp.addTab(url, options);
+
+          if (createProperties.url) {
+            tabListener.initializingTabs.add(nativeTab);
           }
 
-          fireForTab(tab, changeInfo);
-        };
+          return tabManager.convert(nativeTab);
+        },
+
+        async remove(tabs) {
+          if (!Array.isArray(tabs)) {
+            tabs = [tabs];
+          }
 
-        let statusListener = ({browser, status, url}) => {
-          let {BrowserApp} = browser.ownerGlobal;
-          let nativeTab = BrowserApp.getTabForBrowser(browser);
-          if (nativeTab) {
-            let changed = {status};
-            if (url) {
-              changed.url = url;
+          for (let tabId of tabs) {
+            let nativeTab = tabTracker.getTab(tabId);
+            nativeTab.browser.ownerGlobal.BrowserApp.closeTab(nativeTab);
+          }
+        },
+
+        async update(tabId, updateProperties) {
+          let nativeTab = getTabOrActive(tabId);
+
+          let {BrowserApp} = nativeTab.browser.ownerGlobal;
+
+          if (updateProperties.url !== null) {
+            let url = context.uri.resolve(updateProperties.url);
+
+            if (!context.checkLoadURL(url, {dontReportErrors: true})) {
+              return Promise.reject({message: `Illegal URL: ${url}`});
             }
 
-            fireForTab(tabManager.wrapTab(nativeTab), changed);
-          }
-        };
-
-        windowTracker.addListener("status", statusListener);
-        windowTracker.addListener("DOMTitleChanged", listener);
-        return () => {
-          windowTracker.removeListener("status", statusListener);
-          windowTracker.removeListener("DOMTitleChanged", listener);
-        };
-      }).api(),
-
-      async create(createProperties) {
-        let window = createProperties.windowId !== null ?
-          windowTracker.getWindow(createProperties.windowId, context) :
-          windowTracker.topWindow;
-
-        let {BrowserApp} = window;
-        let url;
-
-        if (createProperties.url !== null) {
-          url = context.uri.resolve(createProperties.url);
-
-          if (!context.checkLoadURL(url, {dontReportErrors: true})) {
-            return Promise.reject({message: `Illegal URL: ${url}`});
-          }
-        }
-
-        let options = {};
-
-        let active = true;
-        if (createProperties.active !== null) {
-          active = createProperties.active;
-        }
-        options.selected = active;
-
-        if (createProperties.index !== null) {
-          options.tabIndex = createProperties.index;
-        }
-
-        // Make sure things like about:blank and data: URIs never inherit,
-        // and instead always get a NullPrincipal.
-        options.disallowInheritPrincipal = true;
-
-        tabListener.initTabReady();
-        let nativeTab = BrowserApp.addTab(url, options);
-
-        if (createProperties.url) {
-          tabListener.initializingTabs.add(nativeTab);
-        }
-
-        return tabManager.convert(nativeTab);
-      },
-
-      async remove(tabs) {
-        if (!Array.isArray(tabs)) {
-          tabs = [tabs];
-        }
-
-        for (let tabId of tabs) {
-          let nativeTab = tabTracker.getTab(tabId);
-          nativeTab.browser.ownerGlobal.BrowserApp.closeTab(nativeTab);
-        }
-      },
-
-      async update(tabId, updateProperties) {
-        let nativeTab = getTabOrActive(tabId);
-
-        let {BrowserApp} = nativeTab.browser.ownerGlobal;
-
-        if (updateProperties.url !== null) {
-          let url = context.uri.resolve(updateProperties.url);
-
-          if (!context.checkLoadURL(url, {dontReportErrors: true})) {
-            return Promise.reject({message: `Illegal URL: ${url}`});
+            nativeTab.browser.loadURI(url);
           }
 
-          nativeTab.browser.loadURI(url);
-        }
-
-        if (updateProperties.active !== null) {
-          if (updateProperties.active) {
-            BrowserApp.selectTab(nativeTab);
-          } else {
-            // Not sure what to do here? Which tab should we select?
+          if (updateProperties.active !== null) {
+            if (updateProperties.active) {
+              BrowserApp.selectTab(nativeTab);
+            } else {
+              // Not sure what to do here? Which tab should we select?
+            }
           }
-        }
-        // FIXME: highlighted/selected, muted, pinned, openerTabId
+          // FIXME: highlighted/selected, muted, pinned, openerTabId
 
-        return tabManager.convert(nativeTab);
-      },
+          return tabManager.convert(nativeTab);
+        },
 
-      async reload(tabId, reloadProperties) {
-        let nativeTab = getTabOrActive(tabId);
+        async reload(tabId, reloadProperties) {
+          let nativeTab = getTabOrActive(tabId);
 
-        let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
-        if (reloadProperties && reloadProperties.bypassCache) {
-          flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
-        }
-        nativeTab.browser.reloadWithFlags(flags);
-      },
+          let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+          if (reloadProperties && reloadProperties.bypassCache) {
+            flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+          }
+          nativeTab.browser.reloadWithFlags(flags);
+        },
 
-      async get(tabId) {
-        return tabManager.get(tabId).convert();
-      },
+        async get(tabId) {
+          return tabManager.get(tabId).convert();
+        },
 
-      async getCurrent() {
-        if (context.tabId) {
-          return tabManager.get(context.tabId).convert();
-        }
-      },
+        async getCurrent() {
+          if (context.tabId) {
+            return tabManager.get(context.tabId).convert();
+          }
+        },
 
-      async query(queryInfo) {
-        if (queryInfo.url !== null) {
-          if (!extension.hasPermission("tabs")) {
-            return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
+        async query(queryInfo) {
+          if (queryInfo.url !== null) {
+            if (!extension.hasPermission("tabs")) {
+              return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
+            }
+
+            queryInfo = Object.assign({}, queryInfo);
+            queryInfo.url = new MatchPattern(queryInfo.url);
           }
 
-          queryInfo = Object.assign({}, queryInfo);
-          queryInfo.url = new MatchPattern(queryInfo.url);
-        }
+          return Array.from(tabManager.query(queryInfo, context),
+                            tab => tab.convert());
+        },
 
-        return Array.from(tabManager.query(queryInfo, context),
-                          tab => tab.convert());
-      },
+        async captureVisibleTab(windowId, options) {
+          let window = windowId == null ?
+            windowTracker.topWindow :
+            windowTracker.getWindow(windowId, context);
 
-      async captureVisibleTab(windowId, options) {
-        let window = windowId == null ?
-          windowTracker.topWindow :
-          windowTracker.getWindow(windowId, context);
+          let tab = tabManager.wrapTab(window.BrowserApp.selectedTab);
+          await tabListener.awaitTabReady(tab.nativeTab);
 
-        let tab = tabManager.wrapTab(window.BrowserApp.selectedTab);
-        await tabListener.awaitTabReady(tab.nativeTab);
+          return tab.capture(context, options);
+        },
 
-        return tab.capture(context, options);
-      },
+        async executeScript(tabId, details) {
+          let tab = await promiseTabWhenReady(tabId);
 
-      async executeScript(tabId, details) {
-        let tab = await promiseTabWhenReady(tabId);
+          return tab.executeScript(context, details);
+        },
+
+        async insertCSS(tabId, details) {
+          let tab = await promiseTabWhenReady(tabId);
 
-        return tab.executeScript(context, details);
-      },
+          return tab.insertCSS(context, details);
+        },
 
-      async insertCSS(tabId, details) {
-        let tab = await promiseTabWhenReady(tabId);
+        async removeCSS(tabId, details) {
+          let tab = await promiseTabWhenReady(tabId);
 
-        return tab.insertCSS(context, details);
+          return tab.removeCSS(context, details);
+        },
       },
-
-      async removeCSS(tabId, details) {
-        let tab = await promiseTabWhenReady(tabId);
-
-        return tab.removeCSS(context, details);
-      },
-    },
-  };
-  return self;
-});
+    };
+    return self;
+  }
+};
--- a/mobile/android/components/extensions/extensions-mobile.manifest
+++ b/mobile/android/components/extensions/extensions-mobile.manifest
@@ -1,9 +1,4 @@
 # scripts
-category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
-category webextension-scripts tabs chrome://browser/content/ext-tabs.js
+category webextension-scripts android chrome://browser/content/ext-android.js
 category webextension-scripts utils chrome://browser/content/ext-utils.js
-category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
-
-# schemas
-category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
-category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
+category webextension-scripts-addon android chrome://browser/content/ext-c-android.js
--- a/mobile/android/components/extensions/jar.mn
+++ b/mobile/android/components/extensions/jar.mn
@@ -1,9 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 chrome.jar:
+    content/ext-android.js
+    content/ext-c-android.js
     content/ext-c-tabs.js
     content/ext-pageAction.js
     content/ext-tabs.js
     content/ext-utils.js
--- a/parser/html/nsHtml5Portability.cpp
+++ b/parser/html/nsHtml5Portability.cpp
@@ -86,17 +86,17 @@ void
 nsHtml5Portability::releaseString(nsString* str)
 {
   delete str;
 }
 
 bool
 nsHtml5Portability::localEqualsBuffer(nsIAtom* local, char16_t* buf, int32_t offset, int32_t length)
 {
-  return local->Equals(nsDependentSubstring(buf + offset, buf + offset + length));
+  return local->Equals(buf + offset, length);
 }
 
 bool
 nsHtml5Portability::lowerCaseLiteralIsPrefixOfIgnoreAsciiCaseString(const char* lowerCaseLiteral, nsString* string)
 {
   if (!string) {
     return false;
   }
--- a/security/nss/TAG-INFO
+++ b/security/nss/TAG-INFO
@@ -1,1 +1,1 @@
-215207b4864c
+1fb7e5f584de
--- a/security/nss/automation/ossfuzz/build.sh
+++ b/security/nss/automation/ossfuzz/build.sh
@@ -5,17 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 #
 ################################################################################
 
 # List of targets disabled for oss-fuzz.
 declare -A disabled=([pkcs8]=1)
 
 # List of targets we want to fuzz in TLS and non-TLS mode.
-declare -A tls_targets=([tls-client]=1 [tls-server]=1)
+declare -A tls_targets=([tls-client]=1 [tls-server]=1 [dtls-client]=1 [dtls-server]=1)
 
 # Helper function that copies a fuzzer binary and its seed corpus.
 copy_fuzzer()
 {
     local fuzzer=$1
     local name=$2
 
     # Copy the binary.
--- a/security/nss/automation/taskcluster/graph/src/extend.js
+++ b/security/nss/automation/taskcluster/graph/src/extend.js
@@ -378,21 +378,27 @@ async function scheduleFuzzing() {
   scheduleFuzzingRun(mpi_base, `MPI (invmod)`, `mpi-invmod`, 256, "invmod");
 
   // Schedule TLS fuzzing runs (non-fuzzing mode).
   let tls_base = merge(run_base, {group: "TLS"});
   scheduleFuzzingRun(tls_base, "TLS Client", "tls-client", 20000, "client-nfm",
                      "tls-client-no_fuzzer_mode");
   scheduleFuzzingRun(tls_base, "TLS Server", "tls-server", 20000, "server-nfm",
                      "tls-server-no_fuzzer_mode");
+  scheduleFuzzingRun(tls_base, "DTLS Client", "dtls-client", 20000,
+                     "dtls-client-nfm", "dtls-client-no_fuzzer_mode");
+  scheduleFuzzingRun(tls_base, "DTLS Server", "dtls-server", 20000,
+                     "dtls-server-nfm", "dtls-server-no_fuzzer_mode");
 
   // Schedule TLS fuzzing runs (fuzzing mode).
   let tls_fm_base = merge(tls_base, {parent: task_build_tls});
   scheduleFuzzingRun(tls_fm_base, "TLS Client", "tls-client", 20000, "client");
   scheduleFuzzingRun(tls_fm_base, "TLS Server", "tls-server", 20000, "server");
+  scheduleFuzzingRun(tls_fm_base, "DTLS Client", "dtls-client", 20000, "dtls-client");
+  scheduleFuzzingRun(tls_fm_base, "DTLS Server", "dtls-server", 20000, "dtls-server");
 
   return queue.submit();
 }
 
 /*****************************************************************************/
 
 async function scheduleTestBuilds() {
   let base = {
--- a/security/nss/coreconf/coreconf.dep
+++ b/security/nss/coreconf/coreconf.dep
@@ -5,8 +5,9 @@
 
 /*
  * A dummy header file that is a dependency for all the object files.
  * Used to force a full recompilation of NSS in Mozilla's Tinderbox
  * depend builds.  See comments in rules.mk.
  */
 
 #error "Do not include this header file."
+
--- a/security/nss/fuzz/fuzz.gyp
+++ b/security/nss/fuzz/fuzz.gyp
@@ -296,20 +296,55 @@
       ],
       'dependencies': [
         '<(DEPTH)/exports.gyp:nss_exports',
         '<(DEPTH)/cpputil/cpputil.gyp:cpputil',
         'nssfuzz-tls-base',
       ],
     },
     {
+      'target_name': 'nssfuzz-dtls-client',
+      'type': 'executable',
+      'sources': [
+        'tls_client_config.cc',
+        'tls_client_target.cc',
+      ],
+      'defines': [
+        'IS_DTLS'
+      ],
+      'dependencies': [
+        '<(DEPTH)/exports.gyp:nss_exports',
+        '<(DEPTH)/cpputil/cpputil.gyp:cpputil',
+        'nssfuzz-tls-base',
+      ],
+    },
+    {
+      'target_name': 'nssfuzz-dtls-server',
+      'type': 'executable',
+      'sources': [
+        'tls_server_certs.cc',
+        'tls_server_config.cc',
+        'tls_server_target.cc',
+      ],
+      'defines': [
+        'IS_DTLS'
+      ],
+      'dependencies': [
+        '<(DEPTH)/exports.gyp:nss_exports',
+        '<(DEPTH)/cpputil/cpputil.gyp:cpputil',
+        'nssfuzz-tls-base',
+      ],
+    },
+    {
       'target_name': 'nssfuzz',
       'type': 'none',
       'dependencies': [
         'nssfuzz-certDN',
+        'nssfuzz-dtls-client',
+        'nssfuzz-dtls-server',
         'nssfuzz-pkcs8',
         'nssfuzz-quickder',
         'nssfuzz-tls-client',
         'nssfuzz-tls-server',
       ],
       'conditions': [
         ['OS=="linux"', {
           'dependencies': [
new file mode 100644
--- /dev/null
+++ b/security/nss/fuzz/options/dtls-client-no_fuzzer_mode.options
@@ -0,0 +1,3 @@
+[libfuzzer]
+max_len = 20000
+
new file mode 100644
--- /dev/null
+++ b/security/nss/fuzz/options/dtls-client.options
@@ -0,0 +1,3 @@
+[libfuzzer]
+max_len = 20000
+
new file mode 100644
--- /dev/null
+++ b/security/nss/fuzz/options/dtls-server-no_fuzzer_mode.options
@@ -0,0 +1,3 @@
+[libfuzzer]
+max_len = 20000
+
new file mode 100644
--- /dev/null
+++ b/security/nss/fuzz/options/dtls-server.options
@@ -0,0 +1,3 @@
+[libfuzzer]
+max_len = 20000
+
--- a/security/nss/fuzz/tls_client_target.cc
+++ b/security/nss/fuzz/tls_client_target.cc
@@ -11,16 +11,30 @@
 #include "ssl.h"
 
 #include "shared.h"
 #include "tls_client_config.h"
 #include "tls_common.h"
 #include "tls_mutators.h"
 #include "tls_socket.h"
 
+#ifdef IS_DTLS
+__attribute__((constructor)) static void set_is_dtls() {
+  TlsMutators::SetIsDTLS();
+}
+#endif
+
+PRFileDesc* ImportFD(PRFileDesc* model, PRFileDesc* fd) {
+#ifdef IS_DTLS
+  return DTLS_ImportFD(model, fd);
+#else
+  return SSL_ImportFD(model, fd);
+#endif
+}
+
 static SECStatus AuthCertificateHook(void* arg, PRFileDesc* fd, PRBool checksig,
                                      PRBool isServer) {
   assert(!isServer);
   auto config = reinterpret_cast<ClientConfig*>(arg);
   return config->FailCertificateAuthentication() ? SECFailure : SECSuccess;
 }
 
 static void SetSocketOptions(PRFileDesc* fd,
@@ -44,19 +58,21 @@ static void SetSocketOptions(PRFileDesc*
 
   rv = SSL_OptionSet(fd, SSL_CBC_RANDOM_IV, config->EnableCbcRandomIv());
   assert(rv == SECSuccess);
 
   rv = SSL_OptionSet(fd, SSL_REQUIRE_SAFE_NEGOTIATION,
                      config->RequireSafeNegotiation());
   assert(rv == SECSuccess);
 
+#ifndef IS_DTLS
   rv =
       SSL_OptionSet(fd, SSL_ENABLE_RENEGOTIATION, SSL_RENEGOTIATE_UNRESTRICTED);
   assert(rv == SECSuccess);
+#endif
 }
 
 // This is only called when we set SSL_ENABLE_FALSE_START=1,
 // so we can always just set *canFalseStart=true.
 static SECStatus CanFalseStartCallback(PRFileDesc* fd, void* arg,
                                        PRBool* canFalseStart) {
   *canFalseStart = true;
   return SECSuccess;
@@ -82,36 +98,37 @@ extern "C" int LLVMFuzzerTestOneInput(co
 
   // Reset the RNG state.
   assert(RNG_RandomUpdate(NULL, 0) == SECSuccess);
 
   // Create and import dummy socket.
   std::unique_ptr<DummyPrSocket> socket(new DummyPrSocket(data, len));
   static PRDescIdentity id = PR_GetUniqueIdentity("fuzz-client");
   ScopedPRFileDesc fd(DummyIOLayerMethods::CreateFD(id, socket.get()));
-  PRFileDesc* ssl_fd = SSL_ImportFD(nullptr, fd.get());
+  PRFileDesc* ssl_fd = ImportFD(nullptr, fd.get());
   assert(ssl_fd == fd.get());
 
   // Probably not too important for clients.
   SSL_SetURL(ssl_fd, "server");
 
   SetSocketOptions(ssl_fd, config);
   EnableAllCipherSuites(ssl_fd);
   SetupCallbacks(ssl_fd, config.get());
   DoHandshake(ssl_fd, false);
 
   return 0;
 }
 
 extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size,
                                           size_t max_size, unsigned int seed) {
-  return CustomMutate({TlsMutatorDropRecord, TlsMutatorShuffleRecords,
-                       TlsMutatorDuplicateRecord, TlsMutatorTruncateRecord,
-                       TlsMutatorFragmentRecord},
+  using namespace TlsMutators;
+  return CustomMutate({DropRecord, ShuffleRecords, DuplicateRecord,
+                       TruncateRecord, FragmentRecord},
                       data, size, max_size, seed);
 }
 
 extern "C" size_t LLVMFuzzerCustomCrossOver(const uint8_t* data1, size_t size1,
                                             const uint8_t* data2, size_t size2,
                                             uint8_t* out, size_t max_out_size,
                                             unsigned int seed) {
-  return TlsCrossOver(data1, size1, data2, size2, out, max_out_size, seed);
+  return TlsMutators::CrossOver(data1, size1, data2, size2, out, max_out_size,
+                                seed);
 }
--- a/security/nss/fuzz/tls_mutators.cc
+++ b/security/nss/fuzz/tls_mutators.cc
@@ -5,16 +5,20 @@
 #include "shared.h"
 #include "tls_parser.h"
 
 #include "ssl.h"
 #include "sslimpl.h"
 
 using namespace nss_test;
 
+// Number of additional bytes in the TLS header.
+// Used to properly skip DTLS seqnums.
+static size_t gExtraHeaderBytes = 0;
+
 // Helper class to simplify TLS record manipulation.
 class Record {
  public:
   static std::unique_ptr<Record> Create(const uint8_t *data, size_t size,
                                         size_t remaining) {
     return std::unique_ptr<Record>(new Record(data, size, remaining));
   }
 
@@ -28,19 +32,19 @@ class Record {
     uint8_t *dest = const_cast<uint8_t *>(other->data());
     // Make room for the record we want to insert.
     memmove(dest + size_, other->data(), other->size() + other->remaining());
     // Insert the record.
     memcpy(dest, buf, size_);
   }
 
   void truncate(size_t length) {
-    assert(length >= 5);
+    assert(length >= 5 + gExtraHeaderBytes);
     uint8_t *dest = const_cast<uint8_t *>(data_);
-    (void)ssl_EncodeUintX(length - 5, 2, &dest[3]);
+    (void)ssl_EncodeUintX(length - 5 - gExtraHeaderBytes, 2, &dest[3]);
     memmove(dest + length, data_ + size_, remaining_);
   }
 
   void drop() {
     uint8_t *dest = const_cast<uint8_t *>(data_);
     memmove(dest, data_ + size_, remaining_);
   }
 
@@ -61,41 +65,42 @@ class Record {
 std::vector<std::unique_ptr<Record>> ParseRecords(const uint8_t *data,
                                                   size_t size) {
   std::vector<std::unique_ptr<Record>> records;
   TlsParser parser(data, size);
 
   while (parser.remaining()) {
     size_t offset = parser.consumed();
 
-    uint32_t type;
-    if (!parser.Read(&type, 1)) {
-      break;
-    }
-
-    uint32_t version;
-    if (!parser.Read(&version, 2)) {
+    // Skip type, version, and DTLS seqnums.
+    if (!parser.Skip(3 + gExtraHeaderBytes)) {
       break;
     }
 
     DataBuffer fragment;
     if (!parser.ReadVariable(&fragment, 2)) {
       break;
     }
 
-    records.push_back(
-        Record::Create(data + offset, fragment.len() + 5, parser.remaining()));
+    records.push_back(Record::Create(data + offset,
+                                     fragment.len() + 5 + gExtraHeaderBytes,
+                                     parser.remaining()));
   }
 
   return records;
 }
 
+namespace TlsMutators {
+
+// Handle seqnums in DTLS transcripts.
+void SetIsDTLS() { gExtraHeaderBytes = 8; }
+
 // Mutator that drops whole TLS records.
-size_t TlsMutatorDropRecord(uint8_t *data, size_t size, size_t max_size,
-                            unsigned int seed) {
+size_t DropRecord(uint8_t *data, size_t size, size_t max_size,
+                  unsigned int seed) {
   std::mt19937 rng(seed);
 
   // Find TLS records in the corpus.
   auto records = ParseRecords(data, size);
   if (records.empty()) {
     return 0;
   }
 
@@ -106,18 +111,18 @@ size_t TlsMutatorDropRecord(uint8_t *dat
   // Drop the record.
   rec->drop();
 
   // Return the new final size.
   return size - rec->size();
 }
 
 // Mutator that shuffles TLS records in a transcript.
-size_t TlsMutatorShuffleRecords(uint8_t *data, size_t size, size_t max_size,
-                                unsigned int seed) {
+size_t ShuffleRecords(uint8_t *data, size_t size, size_t max_size,
+                      unsigned int seed) {
   std::mt19937 rng(seed);
 
   // Store the original corpus.
   uint8_t buf[size];
   memcpy(buf, data, size);
 
   // Find TLS records in the corpus.
   auto records = ParseRecords(buf, sizeof(buf));
@@ -137,18 +142,18 @@ size_t TlsMutatorShuffleRecords(uint8_t 
     dest += rec->size();
   }
 
   // Final size hasn't changed.
   return size;
 }
 
 // Mutator that duplicates a single TLS record and randomly inserts it.
-size_t TlsMutatorDuplicateRecord(uint8_t *data, size_t size, size_t max_size,
-                                 unsigned int seed) {
+size_t DuplicateRecord(uint8_t *data, size_t size, size_t max_size,
+                       unsigned int seed) {
   std::mt19937 rng(seed);
 
   // Find TLS records in the corpus.
   const auto records = ParseRecords(data, size);
   if (records.empty()) {
     return 0;
   }
 
@@ -162,49 +167,55 @@ size_t TlsMutatorDuplicateRecord(uint8_t
   // Insert before random record.
   rec->insert_before(records.at(dist(rng)));
 
   // Return the new final size.
   return size + rec->size();
 }
 
 // Mutator that truncates a TLS record.
-size_t TlsMutatorTruncateRecord(uint8_t *data, size_t size, size_t max_size,
-                                unsigned int seed) {
+size_t TruncateRecord(uint8_t *data, size_t size, size_t max_size,
+                      unsigned int seed) {
   std::mt19937 rng(seed);
 
   // Find TLS records in the corpus.
   const auto records = ParseRecords(data, size);
   if (records.empty()) {
     return 0;
   }
 
   // Pick a record to truncate at random.
   std::uniform_int_distribution<size_t> dist(0, records.size() - 1);
   auto &rec = records.at(dist(rng));
 
   // Need a record with data.
-  if (rec->size() <= 5) {
+  if (rec->size() <= 5 + gExtraHeaderBytes) {
     return 0;
   }
 
   // Truncate.
-  std::uniform_int_distribution<size_t> dist2(5, rec->size() - 1);
+  std::uniform_int_distribution<size_t> dist2(5 + gExtraHeaderBytes,
+                                              rec->size() - 1);
   size_t new_length = dist2(rng);
   rec->truncate(new_length);
 
   // Return the new final size.
   return size + new_length - rec->size();
 }
 
 // Mutator that splits a TLS record in two.
-size_t TlsMutatorFragmentRecord(uint8_t *data, size_t size, size_t max_size,
-                                unsigned int seed) {
+size_t FragmentRecord(uint8_t *data, size_t size, size_t max_size,
+                      unsigned int seed) {
   std::mt19937 rng(seed);
 
+  // We can't deal with DTLS yet.
+  if (gExtraHeaderBytes > 0) {
+    return 0;
+  }
+
   if (size + 5 > max_size) {
     return 0;
   }
 
   // Find TLS records in the corpus.
   const auto records = ParseRecords(data, size);
   if (records.empty()) {
     return 0;
@@ -233,19 +244,19 @@ size_t TlsMutatorFragmentRecord(uint8_t 
   memcpy(content + new_length, rdata, 3);
   (void)ssl_EncodeUintX(content_length - new_length, 2,
                         &content[new_length + 3]);
 
   return size + 5;
 }
 
 // Cross-over function that merges and shuffles two transcripts.
-size_t TlsCrossOver(const uint8_t *data1, size_t size1, const uint8_t *data2,
-                    size_t size2, uint8_t *out, size_t max_out_size,
-                    unsigned int seed) {
+size_t CrossOver(const uint8_t *data1, size_t size1, const uint8_t *data2,
+                 size_t size2, uint8_t *out, size_t max_out_size,
+                 unsigned int seed) {
   std::mt19937 rng(seed);
 
   // Find TLS records in the corpus.
   auto records1 = ParseRecords(data1, size1);
   if (records1.empty()) {
     return 0;
   }
 
@@ -269,8 +280,10 @@ size_t TlsCrossOver(const uint8_t *data1
 
     // Write record to its new position.
     memcpy(out + total, rec->data(), length);
     total += length;
   }
 
   return total;
 }
+
+}  // namespace TlsMutators
--- a/security/nss/fuzz/tls_mutators.h
+++ b/security/nss/fuzz/tls_mutators.h
@@ -1,23 +1,29 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef tls_mutators_h__
 #define tls_mutators_h__
 
-size_t TlsMutatorDropRecord(uint8_t *data, size_t size, size_t max_size,
-                            unsigned int seed);
-size_t TlsMutatorShuffleRecords(uint8_t *data, size_t size, size_t max_size,
-                                unsigned int seed);
-size_t TlsMutatorDuplicateRecord(uint8_t *data, size_t size, size_t max_size,
-                                 unsigned int seed);
-size_t TlsMutatorTruncateRecord(uint8_t *data, size_t size, size_t max_size,
-                                unsigned int seed);
-size_t TlsMutatorFragmentRecord(uint8_t *data, size_t size, size_t max_size,
-                                unsigned int seed);
+namespace TlsMutators {
+
+void SetIsDTLS();
 
-size_t TlsCrossOver(const uint8_t *data1, size_t size1, const uint8_t *data2,
-                    size_t size2, uint8_t *out, size_t max_out_size,
-                    unsigned int seed);
+size_t DropRecord(uint8_t *data, size_t size, size_t max_size,
+                  unsigned int seed);
+size_t ShuffleRecords(uint8_t *data, size_t size, size_t max_size,
+                      unsigned int seed);
+size_t DuplicateRecord(uint8_t *data, size_t size, size_t max_size,
+                       unsigned int seed);
+size_t TruncateRecord(uint8_t *data, size_t size, size_t max_size,
+                      unsigned int seed);
+size_t FragmentRecord(uint8_t *data, size_t size, size_t max_size,
+                      unsigned int seed);
+
+size_t CrossOver(const uint8_t *data1, size_t size1, const uint8_t *data2,
+                 size_t size2, uint8_t *out, size_t max_out_size,
+                 unsigned int seed);
+
+}  // namespace TlsMutators
 
 #endif  // tls_mutators_h__
--- a/security/nss/fuzz/tls_server_target.cc
+++ b/security/nss/fuzz/tls_server_target.cc
@@ -12,16 +12,30 @@
 
 #include "shared.h"
 #include "tls_common.h"
 #include "tls_mutators.h"
 #include "tls_server_certs.h"
 #include "tls_server_config.h"
 #include "tls_socket.h"
 
+#ifdef IS_DTLS
+__attribute__((constructor)) static void set_is_dtls() {
+  TlsMutators::SetIsDTLS();
+}
+#endif
+
+PRFileDesc* ImportFD(PRFileDesc* model, PRFileDesc* fd) {
+#ifdef IS_DTLS
+  return DTLS_ImportFD(model, fd);
+#else
+  return SSL_ImportFD(model, fd);
+#endif
+}
+
 class SSLServerSessionCache {
  public:
   SSLServerSessionCache() {
     assert(SSL_ConfigServerSessionIDCache(1024, 0, 0, ".") == SECSuccess);
   }
 
   ~SSLServerSessionCache() {
     assert(SSL_ShutdownServerSessionIDCache() == SECSuccess);
@@ -51,19 +65,21 @@ static void SetSocketOptions(PRFileDesc*
 
   rv = SSL_OptionSet(fd, SSL_CBC_RANDOM_IV, config->EnableCbcRandomIv());
   assert(rv == SECSuccess);
 
   rv = SSL_OptionSet(fd, SSL_REQUIRE_SAFE_NEGOTIATION,
                      config->RequireSafeNegotiation());
   assert(rv == SECSuccess);
 
+#ifndef IS_DTLS
   rv =
       SSL_OptionSet(fd, SSL_ENABLE_RENEGOTIATION, SSL_RENEGOTIATE_UNRESTRICTED);
   assert(rv == SECSuccess);
+#endif
 }
 
 static PRStatus InitModelSocket(void* arg) {
   PRFileDesc* fd = reinterpret_cast<PRFileDesc*>(arg);
 
   EnableAllProtocolVersions();
   EnableAllCipherSuites(fd);
   InstallServerCertificates(fd);
@@ -83,42 +99,43 @@ extern "C" int LLVMFuzzerTestOneInput(co
 
   // Clear the cache. We never want to resume as we couldn't reproduce that.
   SSL_ClearSessionCache();
 
   // Reset the RNG state.
   assert(RNG_RandomUpdate(NULL, 0) == SECSuccess);
 
   // Create model socket.
-  static ScopedPRFileDesc model(SSL_ImportFD(nullptr, PR_NewTCPSocket()));
+  static ScopedPRFileDesc model(ImportFD(nullptr, PR_NewTCPSocket()));
   assert(model);
 
   // Initialize the model socket once.
   static PRCallOnceType initModelOnce;
   PR_CallOnceWithArg(&initModelOnce, InitModelSocket, model.get());
 
   // Create and import dummy socket.
   std::unique_ptr<DummyPrSocket> socket(new DummyPrSocket(data, len));
   static PRDescIdentity id = PR_GetUniqueIdentity("fuzz-server");
   ScopedPRFileDesc fd(DummyIOLayerMethods::CreateFD(id, socket.get()));
-  PRFileDesc* ssl_fd = SSL_ImportFD(model.get(), fd.get());
+  PRFileDesc* ssl_fd = ImportFD(model.get(), fd.get());
   assert(ssl_fd == fd.get());
 
   SetSocketOptions(ssl_fd, config);
   DoHandshake(ssl_fd, true);
 
   return 0;
 }
 
 extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size,
                                           size_t max_size, unsigned int seed) {
-  return CustomMutate({TlsMutatorDropRecord, TlsMutatorShuffleRecords,
-                       TlsMutatorDuplicateRecord, TlsMutatorTruncateRecord,
-                       TlsMutatorFragmentRecord},
+  using namespace TlsMutators;
+  return CustomMutate({DropRecord, ShuffleRecords, DuplicateRecord,
+                       TruncateRecord, FragmentRecord},
                       data, size, max_size, seed);
 }
 
 extern "C" size_t LLVMFuzzerCustomCrossOver(const uint8_t* data1, size_t size1,
                                             const uint8_t* data2, size_t size2,
                                             uint8_t* out, size_t max_out_size,
                                             unsigned int seed) {
-  return TlsCrossOver(data1, size1, data2, size2, out, max_out_size, seed);
+  return TlsMutators::CrossOver(data1, size1, data2, size2, out, max_out_size,
+                                seed);
 }
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -12,18 +12,21 @@ module.exports = { // eslint-disable-lin
     "Ci": true,
     "Components": true,
     "Cr": true,
     "Cu": true,
     "dump": true,
     "TextDecoder": false,
     "TextEncoder": false,
     // Specific to WebExtensions:
+    "AppConstants": true,
     "Extension": true,
+    "ExtensionAPI": true,
     "ExtensionManagement": true,
+    "ExtensionUtils": true,
     "extensions": true,
     "getContainerForCookieStoreId": true,
     "getCookieStoreIdForContainer": true,
     "global": true,
     "isContainerCookieStoreId": true,
     "isDefaultCookieStoreId": true,
     "isPrivateCookieStoreId": true,
     "isValidCookieStoreId": true,
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -733,16 +733,20 @@ this.Extension = class extends Extension
   on(hook, f) {
     return this.emitter.on(hook, f);
   }
 
   off(hook, f) {
     return this.emitter.off(hook, f);
   }
 
+  once(hook, f) {
+    return this.emitter.once(hook, f);
+  }
+
   emit(event, ...args) {
     if (PROXIED_EVENTS.has(event)) {
       Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
     }
 
     return this.emitter.emit(event, ...args);
   }
 
@@ -867,16 +871,18 @@ this.Extension = class extends Extension
     }
 
     this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
 
     let promises = [];
     for (let directive in manifest) {
       if (manifest[directive] !== null) {
         promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
+
+        promises.push(Management.asyncEmitManifestEntry(this, directive));
       }
     }
 
     let data = Services.ppmm.initialProcessData;
     if (!data["Extension:Extensions"]) {
       data["Extension:Extensions"] = [];
     }
     let serial = this.serialize();
@@ -955,16 +961,17 @@ this.Extension = class extends Extension
       // and it is used to run code that needs to be executed before
       // any of the "startup" listeners.
       this.emit("startup", this);
       Management.emit("startup", this);
 
       await this.runManifest(this.manifest);
 
       Management.emit("ready", this);
+      this.emit("ready");
     } catch (e) {
       dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
       Cu.reportError(e);
 
       if (started) {
         ExtensionManagement.shutdownExtension(this.uuid);
       }
 
@@ -1022,16 +1029,17 @@ this.Extension = class extends Extension
 
     for (let api of this.apis) {
       api.destroy();
     }
 
     ParentAPIManager.shutdownExtension(this.id);
 
     Management.emit("shutdown", this);
+    this.emit("shutdown");
 
     Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
 
     MessageChannel.abortResponses({extensionId: this.id});
 
     ExtensionManagement.shutdownExtension(this.uuid);
 
     return this.cleanupGeneratedFile();
--- a/toolkit/components/extensions/ExtensionAPI.jsm
+++ b/toolkit/components/extensions/ExtensionAPI.jsm
@@ -23,21 +23,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 const global = this;
 
 class ExtensionAPI {
   constructor(extension) {
     this.extension = extension;
+
+    extension.once("shutdown", () => {
+      if (this.onShutdown) {
+        this.onShutdown(extension.shutdownReason);
+      }
+      this.extension = null;
+    });
   }
 
   destroy() {
   }
 
+  onManifestEntry(entry) {
+  }
+
   getAPI(context) {
     throw new Error("Not Implemented");
   }
 }
 
 var ExtensionAPIs = {
   apis: ExtensionManagement.APIs.apis,
 
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -49,16 +49,17 @@ const {
   getMessageManager,
   getUniqueId,
   injectAPI,
   promiseEvent,
 } = ExtensionUtils;
 
 const {
   BaseContext,
+  CanOfAPIs,
   LocalAPIImplementation,
   SchemaAPIInterface,
   SchemaAPIManager,
 } = ExtensionCommon;
 
 var ExtensionChild;
 
 /**
@@ -463,53 +464,39 @@ class Messenger {
 }
 
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("addon");
     this.initialized = false;
   }
 
-  generateAPIs(...args) {
+  lazyInit() {
     if (!this.initialized) {
       this.initialized = true;
       for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_ADDON)) {
         this.loadScript(value);
       }
     }
-    return super.generateAPIs(...args);
-  }
-
-  registerSchemaAPI(namespace, envType, getAPI) {
-    if (envType == "addon_child") {
-      super.registerSchemaAPI(namespace, envType, getAPI);
-    }
   }
 }();
 
 var devtoolsAPIManager = new class extends SchemaAPIManager {
   constructor() {
     super("devtools");
     this.initialized = false;
   }
 
-  generateAPIs(...args) {
+  lazyInit() {
     if (!this.initialized) {
       this.initialized = true;
       for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS)) {
         this.loadScript(value);
       }
     }
-    return super.generateAPIs(...args);
-  }
-
-  registerSchemaAPI(namespace, envType, getAPI) {
-    if (envType == "devtools_child") {
-      super.registerSchemaAPI(namespace, envType, getAPI);
-    }
   }
 }();
 
 /**
  * An object that runs an remote implementation of an API.
  */
 class ProxyAPIImplementation extends SchemaAPIInterface {
   /**
@@ -588,25 +575,26 @@ class ProxyAPIImplementation extends Sch
   }
 }
 
 // We create one instance of this class for every extension context that
 // needs to use remote APIs. It uses the message manager to communicate
 // with the ParentAPIManager singleton in ExtensionParent.jsm. It
 // handles asynchronous function calls as well as event listeners.
 class ChildAPIManager {
-  constructor(context, messageManager, localApis, contextData) {
+  constructor(context, messageManager, localAPICan, contextData) {
     this.context = context;
     this.messageManager = messageManager;
     this.url = contextData.url;
 
     // The root namespace of all locally implemented APIs. If an extension calls
     // an API that does not exist in this object, then the implementation is
     // delegated to the ParentAPIManager.
-    this.localApis = localApis;
+    this.localApis = localAPICan.root;
+    this.apiCan = localAPICan;
 
     this.id = `${context.extension.id}.${context.contextId}`;
 
     MessageChannel.addListener(messageManager, "API:RunListener", this);
     messageManager.addMessageListener("API:CallResult", this);
 
     this.messageFilterStrict = {childId: this.id};
 
@@ -777,19 +765,18 @@ class ChildAPIManager {
         allowedContexts.includes("devtools_only")) {
       return false;
     }
 
     return true;
   }
 
   getImplementation(namespace, name) {
-    let obj = namespace.split(".").reduce(
-      (object, prop) => object && object[prop],
-      this.localApis);
+    this.apiCan.findAPIPath(`${namespace}.${name}`);
+    let obj = this.apiCan.findAPIPath(namespace);
 
     if (obj && name in obj) {
       return new LocalAPIImplementation(obj, name, this.context);
     }
 
     return this.getFallbackImplementation(namespace, name);
   }
 
@@ -939,20 +926,22 @@ class ExtensionPageContextChild extends 
 
   unload() {
     super.unload();
     this.extension.views.delete(this);
   }
 }
 
 defineLazyGetter(ExtensionPageContextChild.prototype, "childManager", function() {
+  apiManager.lazyInit();
+
   let localApis = {};
-  apiManager.generateAPIs(this, localApis);
+  let can = new CanOfAPIs(this, apiManager, localApis);
 
-  let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
+  let childManager = new ChildAPIManager(this, this.messageManager, can, {
     envType: "addon_parent",
     viewType: this.viewType,
     url: this.uri.spec,
     incognito: this.incognito,
   });
 
   this.callOnClose(childManager);
 
@@ -986,20 +975,22 @@ class DevToolsContextChild extends Exten
 
   unload() {
     super.unload();
     this.extension.devtoolsViews.delete(this);
   }
 }
 
 defineLazyGetter(DevToolsContextChild.prototype, "childManager", function() {
+  devtoolsAPIManager.lazyInit();
+
   let localApis = {};
-  devtoolsAPIManager.generateAPIs(this, localApis);
+  let can = new CanOfAPIs(this, apiManager, localApis);
 
-  let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
+  let childManager = new ChildAPIManager(this, this.messageManager, can, {
     envType: "devtools_parent",
     viewType: this.viewType,
     url: this.uri.spec,
     incognito: this.incognito,
   });
 
   this.callOnClose(childManager);
 
@@ -1057,17 +1048,21 @@ class ContentGlobal {
           this.global.docShell.isAppTab = true;
         }
 
         if (data.devtoolsToolboxInfo) {
           this.devtoolsToolboxInfo = data.devtoolsToolboxInfo;
         }
 
         promiseEvent(this.global, "DOMContentLoaded", true).then(() => {
-          this.global.sendAsyncMessage("Extension:ExtensionViewLoaded");
+          let windowId = getInnerWindowID(this.global.content);
+          let context = ExtensionChild.extensionContexts.get(windowId);
+
+          this.global.sendAsyncMessage("Extension:ExtensionViewLoaded",
+                                       {childId: context && context.childManager.id});
         });
 
         /* FALLTHROUGH */
       case "Extension:SetTabAndWindowId":
         this.handleSetTabAndWindowId(data);
         break;
     }
   }
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -23,16 +23,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
+  DefaultMap,
+  DefaultWeakMap,
   EventEmitter,
   ExtensionError,
   SpreadArgs,
   getConsole,
   getInnerWindowID,
   getUniqueId,
   runSafeSync,
   runSafeSyncWithoutClone,
@@ -543,16 +545,224 @@ class LocalAPIImplementation extends Sch
     return this.pathObj[this.name].hasListener.call(null, listener);
   }
 
   removeListener(listener) {
     this.pathObj[this.name].removeListener.call(null, listener);
   }
 }
 
+// Recursively copy properties from source to dest.
+function deepCopy(dest, source) {
+  for (let prop in source) {
+    let desc = Object.getOwnPropertyDescriptor(source, prop);
+    if (typeof(desc.value) == "object") {
+      if (!(prop in dest)) {
+        dest[prop] = {};
+      }
+      deepCopy(dest[prop], source[prop]);
+    } else {
+      Object.defineProperty(dest, prop, desc);
+    }
+  }
+}
+
+/**
+ * Manages loading and accessing a set of APIs for a specific extension
+ * context.
+ *
+ * @param {BaseContext} context
+ *        The context to manage APIs for.
+ * @param {SchemaAPIManager} apiManager
+ *        The API manager holding the APIs to manage.
+ * @param {object} root
+ *        The root object into which APIs will be injected.
+ */
+class CanOfAPIs {
+  constructor(context, apiManager, root) {
+    this.context = context;
+    this.scopeName = context.envType;
+    this.apiManager = apiManager;
+    this.root = root;
+
+    this.apiPaths = new Map();
+
+    this.apis = new Map();
+  }
+
+  /**
+   * Synchronously loads and initializes an ExtensionAPI instance.
+   *
+   * @param {string} name
+   *        The name of the API to load.
+   */
+  loadAPI(name) {
+    if (this.apis.has(name)) {
+      return;
+    }
+
+    let {extension} = this.context;
+
+    let api = this.apiManager.getAPI(name, extension, this.scopeName);
+    if (!api) {
+      return;
+    }
+
+    this.apis.set(name, api);
+
+    deepCopy(this.root, api.getAPI(this.context));
+  }
+
+  /**
+   * Asynchronously loads and initializes an ExtensionAPI instance.
+   *
+   * @param {string} name
+   *        The name of the API to load.
+   */
+  async asyncLoadAPI(name) {
+    if (this.apis.has(name)) {
+      return;
+    }
+
+    let {extension} = this.context;
+    if (!Schemas.checkPermissions(name, extension)) {
+      return;
+    }
+
+    let api = await this.apiManager.asyncGetAPI(name, extension, this.scopeName);
+    // Check again, because async;
+    if (this.apis.has(name)) {
+      return;
+    }
+
+    this.apis.set(name, api);
+
+    deepCopy(this.root, api.getAPI(this.context));
+  }
+
+  /**
+   * Finds the API at the given path from the root object, and
+   * synchronously loads the API that implements it if it has not
+   * already been loaded.
+   *
+   * @param {string} path
+   *        The "."-separated path to find.
+   * @returns {*}
+   */
+  findAPIPath(path) {
+    if (this.apiPaths.has(path)) {
+      return this.apiPaths.get(path);
+    }
+
+    let obj = this.root;
+    let modules = this.apiManager.modulePaths;
+
+    for (let key of path.split(".")) {
+      if (!obj) {
+        return;
+      }
+      modules = modules.get(key);
+
+      for (let name of modules.modules) {
+        if (!this.apis.has(name)) {
+          this.loadAPI(name);
+        }
+      }
+
+      obj = obj[key];
+    }
+
+    this.apiPaths.set(path, obj);
+    return obj;
+  }
+
+  /**
+   * Finds the API at the given path from the root object, and
+   * asynchronously loads the API that implements it if it has not
+   * already been loaded.
+   *
+   * @param {string} path
+   *        The "."-separated path to find.
+   * @returns {Promise<*>}
+   */
+  async asyncFindAPIPath(path) {
+    if (this.apiPaths.has(path)) {
+      return this.apiPaths.get(path);
+    }
+
+    let obj = this.root;
+    let modules = this.apiManager.modulePaths;
+
+    for (let key of path.split(".")) {
+      if (!obj) {
+        return;
+      }
+      modules = modules.get(key);
+
+      for (let name of modules.modules) {
+        if (!this.apis.has(name)) {
+          await this.asyncLoadAPI(name);
+        }
+      }
+
+      if (typeof obj[key] === "function") {
+        obj = obj[key].bind(obj);
+      } else {
+        obj = obj[key];
+      }
+    }
+
+    this.apiPaths.set(path, obj);
+    return obj;
+  }
+}
+
+class DeepMap extends DefaultMap {
+  constructor() {
+    super(() => new DeepMap());
+
+    this.modules = new Set();
+  }
+
+  getPath(path) {
+    return path.reduce((map, key) => map.get(key), this);
+  }
+}
+
+/**
+ * @class APIModule
+ * @abstract
+ *
+ * @property {string} url
+ *       The URL of the script which contains the module's
+ *       implementation. This script must define a global property
+ *       matching the modules name, which must be a class constructor
+ *       which inherits from {@link ExtensionAPI}.
+ *
+ * @property {string} schema
+ *       The URL of the JSON schema which describes the module's API.
+ *
+ * @property {Array<string>} scopes
+ *       The list of scope names into which the API may be loaded.
+ *
+ * @property {Array<string>} manifest
+ *       The list of top-level manifest properties which will trigger
+ *       the module to be loaded, and its `onManifestEntry` method to be
+ *       called.
+ *
+ * @property {Array<string>} events
+ *       The list events which will trigger the module to be loaded, and
+ *       its appropriate event handler method to be called. Currently
+ *       only accepts "startup".
+ *
+ * @property {Array<Array<string>>} paths
+ *       A list of paths from the root API object which, when accessed,
+ *       will cause the API module to be instantiated and injected.
+ */
+
 /**
  * This object loads the ext-*.js scripts that define the extension API.
  *
  * This class instance is shared with the scripts that it loads, so that the
  * ext-*.js scripts and the instantiator can communicate with each other.
  */
 class SchemaAPIManager extends EventEmitter {
   /**
@@ -562,44 +772,312 @@ class SchemaAPIManager extends EventEmit
    *     "content" - A content process.
    *     "devtools" - A devtools process.
    *     "proxy" - A proxy script process.
    */
   constructor(processType) {
     super();
     this.processType = processType;
     this.global = this._createExtGlobal();
+
+    this.modules = new Map();
+    this.modulePaths = new DeepMap();
+    this.manifestKeys = new Map();
+    this.eventModules = new DefaultMap(() => new Set());
+
+    this.schemaURLs = new Set();
+
+    this.apis = new DefaultWeakMap(() => new Map());
+
     this._scriptScopes = [];
-    this._schemaApis = {
-      addon_parent: [],
-      addon_child: [],
-      content_parent: [],
-      content_child: [],
-      devtools_parent: [],
-      devtools_child: [],
-      proxy_script: [],
-    };
+  }
+
+  /**
+   * Registers a set of ExtensionAPI modules to be lazily loaded and
+   * managed by this manager.
+   *
+   * @param {object} obj
+   *        An object containing property for eacy API module to be
+   *        registered. Each value should be an object implementing the
+   *        APIModule interface.
+   */
+  registerModules(obj) {
+    for (let [name, details] of Object.entries(obj)) {
+      details.namespaceName = name;
+
+      if (this.modules.has(name)) {
+        throw new Error(`Module '${name}' already registered`);
+      }
+      this.modules.set(name, details);
+
+      if (details.schema) {
+        this.schemaURLs.add(details.schema);
+      }
+
+      for (let event of details.events || []) {
+        this.eventModules.get(event).add(name);
+      }
+
+      for (let key of details.manifest || []) {
+        if (this.manifestKeys.has(key)) {
+          throw new Error(`Manifest key '${key}' already registered by '${this.manifestKeys.get(key)}'`);
+        }
+
+        this.manifestKeys.set(key, name);
+      }
+
+      for (let path of details.paths || []) {
+        this.modulePaths.getPath(path).modules.add(name);
+      }
+    }
+  }
+
+  /**
+   * Emits an `onManifestEntry` event for the top-level manifest entry
+   * on all relevant {@link ExtensionAPI} instances for the given
+   * extension.
+   *
+   * The API modules will be synchronously loaded if they have not been
+   * loaded already.
+   *
+   * @param {Extension} extension
+   *        The extension for which to emit the events.
+   * @param {string} entry
+   *        The name of the top-level manifest entry.
+   *
+   * @returns {*}
+   */
+  emitManifestEntry(extension, entry) {
+    let apiName = this.manifestKeys.get(entry);
+    if (apiName) {
+      let api = this.getAPI(apiName, extension);
+      return api.onManifestEntry(entry);
+    }
+  }
+  /**
+   * Emits an `onManifestEntry` event for the top-level manifest entry
+   * on all relevant {@link ExtensionAPI} instances for the given
+   * extension.
+   *
+   * The API modules will be asynchronously loaded if they have not been
+   * loaded already.
+   *
+   * @param {Extension} extension
+   *        The extension for which to emit the events.
+   * @param {string} entry
+   *        The name of the top-level manifest entry.
+   *
+   * @returns {Promise<*>}
+   */
+  async asyncEmitManifestEntry(extension, entry) {
+    let apiName = this.manifestKeys.get(entry);
+    if (apiName) {
+      let api = await this.asyncGetAPI(apiName, extension);
+      return api.onManifestEntry(entry);
+    }
+  }
+
+  /**
+   * Returns the {@link ExtensionAPI} instance for the given API module,
+   * for the given extension, in the given scope, synchronously loading
+   * and instantiating it if necessary.
+   *
+   * @param {string} name
+   *        The name of the API module to load.
+   * @param {Extension} extension
+   *        The extension for which to load the API.
+   * @param {string} [scope = null]
+   *        The scope type for which to retrieve the API, or null if not
+   *        being retrieved for a particular scope.
+   *
+   * @returns {ExtensionAPI?}
+   */
+  getAPI(name, extension, scope = null) {
+    if (!this._checkGetAPI(name, extension, scope)) {
+      return;
+    }
+
+    let apis = this.apis.get(extension);
+    if (apis.has(name)) {
+      return apis.get(name);
+    }
+
+    let module = this.loadModule(name);
+
+    let api = new module(extension);
+    apis.set(name, api);
+    return api;
   }
+  /**
+   * Returns the {@link ExtensionAPI} instance for the given API module,
+   * for the given extension, in the given scope, asynchronously loading
+   * and instantiating it if necessary.
+   *
+   * @param {string} name
+   *        The name of the API module to load.
+   * @param {Extension} extension
+   *        The extension for which to load the API.
+   * @param {string} [scope = null]
+   *        The scope type for which to retrieve the API, or null if not
+   *        being retrieved for a particular scope.
+   *
+   * @returns {Promise<ExtensionAPI>?}
+   */
+  async asyncGetAPI(name, extension, scope = null) {
+    if (!this._checkGetAPI(name, extension, scope)) {
+      return;
+    }
+
+    let apis = this.apis.get(extension);
+    if (apis.has(name)) {
+      return apis.get(name);
+    }
+
+    let module = await this.asyncLoadModule(name);
+
+    // Check again, because async.
+    if (apis.has(name)) {
+      return apis.get(name);
+    }
+
+    let api = new module(extension);
+    apis.set(name, api);
+    return api;
+  }
+
+  /**
+   * Synchronously loads an API module, if not already loaded, and
+   * returns its ExtensionAPI constructor.
+   *
+   * @param {string} name
+   *        The name of the module to load.
+   *
+   * @returns {class}
+   */
+  loadModule(name) {
+    let module = this.modules.get(name);
+    if (module.loaded) {
+      return this.global[name];
+    }
+
+    this._checkLoadModule(module, name);
+
+    Services.scriptloader.loadSubScript(module.url, this.global, "UTF-8");
+
+    module.loaded = true;
+
+    return this._initModule(module, this.global[name]);
+  }
+  /**
+   * aSynchronously loads an API module, if not already loaded, and
+   * returns its ExtensionAPI constructor.
+   *
+   * @param {string} name
+   *        The name of the module to load.
+   *
+   * @returns {Promise<class>}
+   */
+  asyncLoadModule(name) {
+    let module = this.modules.get(name);
+    if (module.loaded) {
+      return Promise.resolve(this.global[name]);
+    }
+    if (module.asyncLoaded) {
+      return module.asyncLoaded;
+    }
+
+    this._checkLoadModule(module, name);
+
+    module.asyncLoaded = ChromeUtils.compileScript(module.url).then(script => {
+      script.executeInGlobal(this.global);
+
+      module.loaded = true;
+
+      return this._initModule(module, this.global[name]);
+    });
+
+    return module.asyncLoaded;
+  }
+
+  /**
+   * Checks whether the given API module may be loaded for the given
+   * extension, in the given scope.
+   *
+   * @param {string} name
+   *        The name of the API module to check.
+   * @param {Extension} extension
+   *        The extension for which to check the API.
+   * @param {string} [scope = null]
+   *        The scope type for which to check the API, or null if not
+   *        being checked for a particular scope.
+   *
+   * @returns {boolean}
+   *        Whether the module may be loaded.
+   */
+  _checkGetAPI(name, extension, scope = null) {
+    let module = this.modules.get(name);
+
+    if (!scope) {
+      return true;
+    }
+
+    if (!module.scopes.includes(scope)) {
+      return false;
+    }
+
+    if (!Schemas.checkPermissions(module.namespaceName, extension)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  _initModule(info, cls) {
+    cls.namespaceName = cls.namespaceName;
+    cls.scopes = new Set(info.scopes);
+
+    return cls;
+  }
+
+  _checkLoadModule(module, name) {
+    if (!module) {
+      throw new Error(`Module '${name}' does not exist`);
+    }
+    if (module.asyncLoaded) {
+      throw new Error(`Module '${name}' currently being lazily loaded`);
+    }
+    if (this.global[name]) {
+      throw new Error(`Module '${name}' conflicts with existing global property`);
+    }
+  }
+
 
   /**
    * Create a global object that is used as the shared global for all ext-*.js
    * scripts that are loaded via `loadScript`.
    *
    * @returns {object} A sandbox that is used as the global by `loadScript`.
    */
   _createExtGlobal() {
     let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
       wantXrays: false,
       sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
     });
 
     Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
 
+    Cu.import("resource://gre/modules/AppConstants.jsm", global);
+    Cu.import("resource://gre/modules/ExtensionAPI.jsm", global);
+
     XPCOMUtils.defineLazyGetter(global, "console", getConsole);
 
+    XPCOMUtils.defineLazyModuleGetter(global, "ExtensionUtils",
+                                      "resource://gre/modules/ExtensionUtils.jsm");
+    XPCOMUtils.defineLazyModuleGetter(global, "XPCOMUtils",
+                                      "resource://gre/modules/XPCOMUtils.jsm");
     XPCOMUtils.defineLazyModuleGetter(global, "require",
                                       "resource://devtools/shared/Loader.jsm");
 
     return global;
   }
 
   /**
    * Load an ext-*.js script. The script runs in its own scope, if it wishes to
@@ -615,95 +1093,35 @@ class SchemaAPIManager extends EventEmit
 
     Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8");
 
     // Save the scope to avoid it being garbage collected.
     this._scriptScopes.push(scope);
   }
 
   /**
-   * Called by an ext-*.js script to register an API.
-   *
-   * @param {string} namespace The API namespace.
-   *     Intended to match the namespace of the generated API, but not used at
-   *     the moment - see bugzil.la/1295774.
-   * @param {string} envType Restricts the API to contexts that run in the
-   *    given environment. Must be one of the following:
-   *     - "addon_parent" - addon APIs that run in the main process.
-   *     - "addon_child" - addon APIs that run in an addon process.
-   *     - "content_parent" - content script APIs that run in the main process.
-   *     - "content_child" - content script APIs that run in a content process.
-   *     - "devtools_parent" - devtools APIs that run in the main process.
-   *     - "devtools_child" - devtools APIs that run in a devtools process.
-   *     - "proxy_script" - proxy script APIs that run in the main process.
-   * @param {function(BaseContext)} getAPI A function that returns an object
-   *     that will be merged with |chrome| and |browser|. The next example adds
-   *     the create, update and remove methods to the tabs API.
-   *
-   *     registerSchemaAPI("tabs", "addon_parent", (context) => ({
-   *       tabs: { create, update },
-   *     }));
-   *     registerSchemaAPI("tabs", "addon_parent", (context) => ({
-   *       tabs: { remove },
-   *     }));
-   */
-  registerSchemaAPI(namespace, envType, getAPI) {
-    this._schemaApis[envType].push({namespace, getAPI});
-  }
-
-  /**
-   * Exports all registered scripts to `obj`.
-   *
-   * @param {BaseContext} context The context for which the API bindings are
-   *     generated.
-   * @param {object} obj The destination of the API.
-   */
-  generateAPIs(context, obj) {
-    let apis = this._schemaApis[context.envType];
-    if (!apis) {
-      Cu.reportError(`No APIs have been registered for ${context.envType}`);
-      return;
-    }
-    SchemaAPIManager.generateAPIs(context, apis, obj);
-  }
-
-  /**
    * Mash together all the APIs from `apis` into `obj`.
    *
    * @param {BaseContext} context The context for which the API bindings are
    *     generated.
    * @param {Array} apis A list of objects, see `registerSchemaAPI`.
    * @param {object} obj The destination of the API.
    */
   static generateAPIs(context, apis, obj) {
-    // Recursively copy properties from source to dest.
-    function copy(dest, source) {
-      for (let prop in source) {
-        let desc = Object.getOwnPropertyDescriptor(source, prop);
-        if (typeof(desc.value) == "object") {
-          if (!(prop in dest)) {
-            dest[prop] = {};
-          }
-          copy(dest[prop], source[prop]);
-        } else {
-          Object.defineProperty(dest, prop, desc);
-        }
-      }
-    }
-
     function hasPermission(perm) {
       return context.extension.hasPermission(perm, true);
     }
     for (let api of apis) {
       if (Schemas.checkPermissions(api.namespace, {hasPermission})) {
         api = api.getAPI(context);
-        copy(obj, api);
+        deepCopy(obj, api);
       }
     }
   }
 }
 
 const ExtensionCommon = {
   BaseContext,
+  CanOfAPIs,
   LocalAPIImplementation,
   SchemaAPIInterface,
   SchemaAPIManager,
 };
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -64,16 +64,17 @@ const {
   getInnerWindowID,
   getWinUtils,
   promiseDocumentReady,
   runSafeSyncWithoutClone,
 } = ExtensionUtils;
 
 const {
   BaseContext,
+  CanOfAPIs,
   SchemaAPIManager,
 } = ExtensionCommon;
 
 const {
   ChildAPIManager,
   Messenger,
 } = ExtensionChild;
 
@@ -89,30 +90,23 @@ function isWhenBeforeOrSame(when1, when2
 }
 
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("content");
     this.initialized = false;
   }
 
-  generateAPIs(...args) {
+  lazyInit() {
     if (!this.initialized) {
       this.initialized = true;
       for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
         this.loadScript(value);
       }
     }
-    return super.generateAPIs(...args);
-  }
-
-  registerSchemaAPI(namespace, envType, getAPI) {
-    if (envType == "content_child") {
-      super.registerSchemaAPI(namespace, envType, getAPI);
-    }
   }
 }();
 
 const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
 const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
 
 const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
 
@@ -619,20 +613,22 @@ defineLazyGetter(ContentScriptContextChi
   let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
   let filter = {extensionId: this.extension.id};
   let optionalFilter = {frameId: this.frameId};
 
   return new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
 });
 
 defineLazyGetter(ContentScriptContextChild.prototype, "childManager", function() {
+  apiManager.lazyInit();
+
   let localApis = {};
-  apiManager.generateAPIs(this, localApis);
+  let can = new CanOfAPIs(this, apiManager, localApis);
 
-  let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
+  let childManager = new ChildAPIManager(this, this.messageManager, can, {
     envType: "content_parent",
     url: this.url,
   });
 
   this.callOnClose(childManager);
 
   return childManager;
 });
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -36,24 +36,24 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   BaseContext,
+  CanOfAPIs,
   SchemaAPIManager,
 } = ExtensionCommon;
 
 var {
   MessageManagerProxy,
   SpreadArgs,
   defineLazyGetter,
-  findPathInObject,
   promiseDocumentLoaded,
   promiseEvent,
   promiseObserved,
 } = ExtensionUtils;
 
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
@@ -72,41 +72,62 @@ let GlobalManager;
 let ParentAPIManager;
 let ProxyMessenger;
 
 // This object loads the ext-*.js scripts that define the extension API.
 let apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("main");
     this.initialized = null;
+
+    this.on("startup", (event, extension) => { // eslint-disable-line mozilla/balanced-listeners
+      let promises = [];
+      for (let apiName of this.eventModules.get("startup")) {
+        promises.push(this.asyncGetAPI(apiName, extension).then(api => {
+          api.onStartup(extension.startupReason);
+        }));
+      }
+
+      return Promise.all(promises);
+    });
   }
 
   // Loads all the ext-*.js scripts currently registered.
   lazyInit() {
     if (this.initialized) {
       return this.initialized;
     }
 
-    // Load order matters here. The base manifest defines types which are
-    // extended by other schemas, so needs to be loaded first.
-    let promise = Schemas.load(BASE_SCHEMA).then(() => {
-      let promises = [];
-      for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
-        promises.push(Schemas.load(url));
+    let scripts = [];
+    for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
+      scripts.push(value);
+    }
+
+    let promise = Promise.all(scripts.map(url => ChromeUtils.compileScript(url))).then(scripts => {
+      for (let script of scripts) {
+        script.executeInGlobal(this.global);
       }
-      for (let url of schemaURLs) {
-        promises.push(Schemas.load(url));
-      }
-      return Promise.all(promises);
+
+      // Load order matters here. The base manifest defines types which are
+      // extended by other schemas, so needs to be loaded first.
+      return Schemas.load(BASE_SCHEMA).then(() => {
+        let promises = [];
+        for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
+          promises.push(Schemas.load(url));
+        }
+        for (let url of this.schemaURLs) {
+          promises.push(Schemas.load(url));
+        }
+        for (let url of schemaURLs) {
+          promises.push(Schemas.load(url));
+        }
+        return Promise.all(promises);
+      });
     });
 
-    for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
-      this.loadScript(value);
-    }
-
     /* eslint-disable mozilla/balanced-listeners */
     Services.mm.addMessageListener("Extension:GetTabAndWindowId", this);
     /* eslint-enable mozilla/balanced-listeners */
 
     this.initialized = promise;
     return this.initialized;
   }
 
@@ -117,23 +138,16 @@ let apiManager = new class extends Schem
       if (result.tabId) {
         if (sync) {
           return result;
         }
         target.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", result);
       }
     }
   }
-
-  registerSchemaAPI(namespace, envType, getAPI) {
-    if (envType == "addon_parent" || envType == "content_parent" ||
-        envType == "devtools_parent") {
-      super.registerSchemaAPI(namespace, envType, getAPI);
-    }
-  }
 }();
 
 // Subscribes to messages related to the extension messaging API and forwards it
 // to the relevant message manager. The "sender" field for the `onMessage` and
 // `onConnect` events are updated if needed.
 ProxyMessenger = {
   _initialized: false,
 
@@ -270,33 +284,34 @@ GlobalManager = {
     }
   },
 
   getExtension(extensionId) {
     return this.extensionMap.get(extensionId);
   },
 
   injectInObject(context, isChromeCompat, dest) {
-    apiManager.generateAPIs(context, dest);
     SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
   },
 };
 
 /**
  * The proxied parent side of a context in ExtensionChild.jsm, for the
  * parent side of a proxied API.
  */
 class ProxyContextParent extends BaseContext {
   constructor(envType, extension, params, xulBrowser, principal) {
     super(envType, extension);
 
     this.uri = NetUtil.newURI(params.url);
 
     this.incognito = params.incognito;
 
+    this.listenerPromises = new Set();
+
     // This message manager is used by ParentAPIManager to send messages and to
     // close the ProxyContext if the underlying message manager closes. This
     // message manager object may change when `xulBrowser` swaps docshells, e.g.
     // when a tab is moved to a different window.
     this.messageManagerProxy = new MessageManagerProxy(xulBrowser);
 
     Object.defineProperty(this, "principal", {
       value: principal, enumerable: true, configurable: true,
@@ -328,20 +343,25 @@ class ProxyContextParent extends BaseCon
       return;
     }
     this.messageManagerProxy.dispose();
     super.unload();
     apiManager.emit("proxy-context-unload", this);
   }
 }
 
-defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
+defineLazyGetter(ProxyContextParent.prototype, "apiCan", function() {
   let obj = {};
+  let can = new CanOfAPIs(this, apiManager, obj);
   GlobalManager.injectInObject(this, false, obj);
-  return obj;
+  return can;
+});
+
+defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
+  return this.apiCan.root;
 });
 
 defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() {
   return Cu.Sandbox(this.principal);
 });
 
 /**
  * The parent side of proxied API context for extension content script
@@ -562,17 +582,17 @@ ParentAPIManager = {
   closeProxyContext(childId) {
     let context = this.proxyContexts.get(childId);
     if (context) {
       context.unload();
       this.proxyContexts.delete(childId);
     }
   },
 
-  call(data, target) {
+  async call(data, target) {
     let context = this.getContextById(data.childId);
     if (context.parentMessageManager !== target.messageManager) {
       throw new Error("Got message on unexpected message manager");
     }
 
     let reply = result => {
       if (!context.parentMessageManager) {
         Services.console.logStringMessage("Cannot send function call result: other side closed connection " +
@@ -585,17 +605,18 @@ ParentAPIManager = {
         Object.assign({
           childId: data.childId,
           callId: data.callId,
         }, result));
     };
 
     try {
       let args = Cu.cloneInto(data.args, context.sandbox);
-      let result = findPathInObject(context.apiObj, data.path)(...args);
+      let fun = await context.apiCan.asyncFindAPIPath(data.path);
+      let result = fun(...args);
 
       if (data.callId) {
         result = result || Promise.resolve();
 
         result.then(result => {
           result = result instanceof SpreadArgs ? [...result] : [result];
 
           reply({result});
@@ -609,17 +630,17 @@ ParentAPIManager = {
         let error = context.normalizeError(e);
         reply({error: {message: error.message}});
       } else {
         Cu.reportError(e);
       }
     }
   },
 
-  addListener(data, target) {
+  async addListener(data, target) {
     let context = this.getContextById(data.childId);
     if (context.parentMessageManager !== target.messageManager) {
       throw new Error("Got message on unexpected message manager");
     }
 
     let {childId} = data;
 
     function listener(...listenerArgs) {
@@ -635,23 +656,37 @@ ParentAPIManager = {
         {
           recipient: {childId},
         });
     }
 
     context.listenerProxies.set(data.listenerId, listener);
 
     let args = Cu.cloneInto(data.args, context.sandbox);
-    findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
+    let promise = context.apiCan.asyncFindAPIPath(data.path);
+
+    // Store pending listener additions so we can be sure they're all
+    // fully initialize before we consider extension startup complete.
+    if (context.viewType === "background" && context.listenerPromises) {
+      const {listenerPromises} = context;
+      listenerPromises.add(promise);
+      let remove = () => { listenerPromises.delete(promise); };
+      promise.then(remove, remove);
+    }
+
+    let handler = await promise;
+    handler.addListener(listener, ...args);
   },
 
-  removeListener(data) {
+  async removeListener(data) {
     let context = this.getContextById(data.childId);
     let listener = context.listenerProxies.get(data.listenerId);
-    findPathInObject(context.apiObj, data.path).removeListener(listener);
+
+    let handler = await context.apiCan.asyncFindAPIPath(data.path);
+    handler.removeListener(listener);
   },
 
   getContextById(childId) {
     let context = this.proxyContexts.get(childId);
     if (!context) {
       throw new Error("WebExtension context not found!");
     }
     return context;
@@ -812,19 +847,19 @@ class HiddenExtensionPage {
 
     return promiseObserved("chrome-document-global-created",
                            win => win.document == chromeShell.document);
   }
 }
 
 function promiseExtensionViewLoaded(browser) {
   return new Promise(resolve => {
-    browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad() {
+    browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad({data}) {
       browser.messageManager.removeMessageListener("Extension:ExtensionViewLoaded", onLoad);
-      resolve();
+      resolve(data.childId && ParentAPIManager.getContextById(data.childId));
     });
   });
 }
 
 /**
  * This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
  * to be called for every ExtensionProxyContext created for an extension page given
  * its related extension, viewType and browser element (both the top level context and any context
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -479,20 +479,22 @@ let IconDetails = {
   // string escape characters are escaped here, since they could lead to a
   // sandbox break.
   escapeUrl(url) {
     return url.replace(/[\\\s"]/g, encodeURIComponent);
   },
 };
 
 const LISTENERS = Symbol("listeners");
+const ONCE_MAP = Symbol("onceMap");
 
 class EventEmitter {
   constructor() {
     this[LISTENERS] = new Map();
+    this[ONCE_MAP] = new WeakMap();
   }
 
   /**
    * Adds the given function as a listener for the given event.
    *
    * The listener function may optionally return a Promise which
    * resolves when it has completed all operations which event
    * dispatchers may need to block on.
@@ -518,23 +520,45 @@ class EventEmitter {
    * @param {function(string, ...any)} listener
    *        The listener function to remove.
    */
   off(event, listener) {
     if (this[LISTENERS].has(event)) {
       let set = this[LISTENERS].get(event);
 
       set.delete(listener);
+      set.delete(this[ONCE_MAP].get(listener));
       if (!set.size) {
         this[LISTENERS].delete(event);
       }
     }
   }
 
   /**
+   * Adds the given function as a listener for the given event once.
+   *
+   * @param {string} event
+   *       The name of the event to listen for.
+   * @param {function(string, ...any)} listener
+   *        The listener to call when events are emitted.
+   */
+  once(event, listener) {
+    let wrapper = (...args) => {
+      this.off(event, wrapper);
+      this[ONCE_MAP].delete(listener);
+
+      return listener(...args);
+    };
+    this[ONCE_MAP].set(listener, wrapper);
+
+    this.on(event, wrapper);
+  }
+
+
+  /**
    * Triggers all listeners for the given event, and returns a promise
    * which resolves when all listeners have been called, and any
    * promises they have returned have likewise resolved.
    *
    * @param {string} event
    *       The name of the event to emit.
    * @param {any} args
    *        Arbitrary arguments to pass to the listener functions, after
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -28,16 +28,17 @@ const CATEGORY_EXTENSION_SCRIPTS_CONTENT
 const PROXY_TIMEOUT_SEC = 10;
 
 const {
   defineLazyGetter,
 } = ExtensionUtils;
 
 const {
   BaseContext,
+  CanOfAPIs,
   LocalAPIImplementation,
   SchemaAPIManager,
 } = ExtensionCommon;
 
 const {
   Messenger,
 } = ExtensionChild;
 
@@ -223,53 +224,47 @@ class ProxyScriptContext extends BaseCon
 }
 
 class ProxyScriptAPIManager extends SchemaAPIManager {
   constructor() {
     super("proxy");
     this.initialized = false;
   }
 
-  generateAPIs(...args) {
+  lazyInit() {
     if (!this.initialized) {
       for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(
           CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
         this.loadScript(value);
       }
       this.initialized = true;
     }
-    return super.generateAPIs(...args);
-  }
-
-  registerSchemaAPI(namespace, envType, getAPI) {
-    if (envType == "proxy_script") {
-      super.registerSchemaAPI(namespace, envType, getAPI);
-    }
   }
 }
 
 class ProxyScriptInjectionContext {
-  constructor(context, localAPIs) {
+  constructor(context, apiCan) {
     this.context = context;
-    this.localAPIs = localAPIs;
+    this.localAPIs = apiCan.root;
+    this.apiCan = apiCan;
   }
 
   shouldInject(namespace, name, allowedContexts) {
     if (this.context.envType !== "proxy_script") {
       throw new Error(`Unexpected context type "${this.context.envType}"`);
     }
 
     // Do not generate proxy script APIs unless explicitly allowed.
     return allowedContexts.includes("proxy");
   }
 
   getImplementation(namespace, name) {
-    let obj = namespace.split(".").reduce(
-      (object, prop) => object && object[prop],
-      this.localAPIs);
+    this.apiCan.findAPIPath(`${namespace}.${name}`);
+    let obj = this.apiCan.findAPIPath(namespace);
+
     if (obj && name in obj) {
       return new LocalAPIImplementation(obj, name, this.context);
     }
   }
 
   get cloneScope() {
     return this.context.cloneScope;
   }
@@ -284,15 +279,16 @@ defineLazyGetter(ProxyScriptContext.prot
   let filter = {extensionId: this.extension.id, toProxyScript: true};
   return new Messenger(this, [this.messageManager], sender, filter);
 });
 
 let proxyScriptAPIManager = new ProxyScriptAPIManager();
 
 defineLazyGetter(ProxyScriptContext.prototype, "browserObj", function() {
   let localAPIs = {};
-  proxyScriptAPIManager.generateAPIs(this, localAPIs);
+  let can = new CanOfAPIs(this, proxyScriptAPIManager, localAPIs);
+  proxyScriptAPIManager.lazyInit();
 
   let browserObj = Cu.createObjectIn(this.sandbox);
-  let injectionContext = new ProxyScriptInjectionContext(this, localAPIs);
+  let injectionContext = new ProxyScriptInjectionContext(this, can);
   Schemas.inject(browserObj, injectionContext);
   return browserObj;
 });
--- a/toolkit/components/extensions/ext-alarms.js
+++ b/toolkit/components/extensions/ext-alarms.js
@@ -1,22 +1,19 @@
 "use strict";
 
-var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   SingletonEventManager,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> Map[name -> Alarm]]
-var alarmsMap = new WeakMap();
+let alarmsMap = new WeakMap();
 
 // WeakMap[Extension -> Set[callback]]
-var alarmCallbacksMap = new WeakMap();
+let alarmCallbacksMap = new WeakMap();
 
 // Manages an alarm created by the extension (alarms API).
 function Alarm(extension, name, alarmInfo) {
   this.extension = extension;
   this.name = name;
   this.when = alarmInfo.when;
   this.delayInMinutes = alarmInfo.delayInMinutes;
   this.periodInMinutes = alarmInfo.periodInMinutes;
@@ -71,85 +68,86 @@ Alarm.prototype = {
     return {
       name: this.name,
       scheduledTime: this.scheduledTime,
       periodInMinutes: this.periodInMinutes,
     };
   },
 };
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("startup", (type, extension) => {
-  alarmsMap.set(extension, new Map());
-  alarmCallbacksMap.set(extension, new Set());
-});
+this.alarms = class extends ExtensionAPI {
+  onShutdown() {
+    let {extension} = this;
+
+    if (alarmsMap.has(extension)) {
+      for (let alarm of alarmsMap.get(extension).values()) {
+        alarm.clear();
+      }
+      alarmsMap.delete(extension);
+      alarmCallbacksMap.delete(extension);
+    }
+  }
 
-extensions.on("shutdown", (type, extension) => {
-  if (alarmsMap.has(extension)) {
-    for (let alarm of alarmsMap.get(extension).values()) {
-      alarm.clear();
-    }
-    alarmsMap.delete(extension);
-    alarmCallbacksMap.delete(extension);
-  }
-});
-/* eslint-enable mozilla/balanced-listeners */
+  getAPI(context) {
+    let {extension} = context;
+
+    alarmsMap.set(extension, new Map());
+    alarmCallbacksMap.set(extension, new Set());
+
+    return {
+      alarms: {
+        create: function(name, alarmInfo) {
+          name = name || "";
+          let alarms = alarmsMap.get(extension);
+          if (alarms.has(name)) {
+            alarms.get(name).clear();
+          }
+          let alarm = new Alarm(extension, name, alarmInfo);
+          alarms.set(alarm.name, alarm);
+        },
 
-extensions.registerSchemaAPI("alarms", "addon_parent", context => {
-  let {extension} = context;
-  return {
-    alarms: {
-      create: function(name, alarmInfo) {
-        name = name || "";
-        let alarms = alarmsMap.get(extension);
-        if (alarms.has(name)) {
-          alarms.get(name).clear();
-        }
-        let alarm = new Alarm(extension, name, alarmInfo);
-        alarms.set(alarm.name, alarm);
-      },
+        get: function(name) {
+          name = name || "";
+          let alarms = alarmsMap.get(extension);
+          if (alarms.has(name)) {
+            return Promise.resolve(alarms.get(name).data);
+          }
+          return Promise.resolve();
+        },
+
+        getAll: function() {
+          let result = Array.from(alarmsMap.get(extension).values(), alarm => alarm.data);
+          return Promise.resolve(result);
+        },
 
-      get: function(name) {
-        name = name || "";
-        let alarms = alarmsMap.get(extension);
-        if (alarms.has(name)) {
-          return Promise.resolve(alarms.get(name).data);
-        }
-        return Promise.resolve();
+        clear: function(name) {
+          name = name || "";
+          let alarms = alarmsMap.get(extension);
+          if (alarms.has(name)) {
+            alarms.get(name).clear();
+            return Promise.resolve(true);
+          }
+          return Promise.resolve(false);
+        },
+
+        clearAll: function() {
+          let cleared = false;
+          for (let alarm of alarmsMap.get(extension).values()) {
+            alarm.clear();
+            cleared = true;
+          }
+          return Promise.resolve(cleared);
+        },
+
+        onAlarm: new SingletonEventManager(context, "alarms.onAlarm", fire => {
+          let callback = alarm => {
+            fire.sync(alarm.data);
+          };
+
+          alarmCallbacksMap.get(extension).add(callback);
+          return () => {
+            alarmCallbacksMap.get(extension).delete(callback);
+          };
+        }).api(),
       },
-
-      getAll: function() {
-        let result = Array.from(alarmsMap.get(extension).values(), alarm => alarm.data);
-        return Promise.resolve(result);
-      },
-
-      clear: function(name) {
-        name = name || "";
-        let alarms = alarmsMap.get(extension);
-        if (alarms.has(name)) {
-          alarms.get(name).clear();
-          return Promise.resolve(true);
-        }
-        return Promise.resolve(false);
-      },
-
-      clearAll: function() {
-        let cleared = false;
-        for (let alarm of alarmsMap.get(extension).values()) {
-          alarm.clear();
-          cleared = true;
-        }
-        return Promise.resolve(cleared);
-      },
-
-      onAlarm: new SingletonEventManager(context, "alarms.onAlarm", fire => {
-        let callback = alarm => {
-          fire.sync(alarm.data);
-        };
-
-        alarmCallbacksMap.get(extension).add(callback);
-        return () => {
-          alarmCallbacksMap.get(extension).delete(callback);
-        };
-      }).api(),
-    },
-  };
-});
+    };
+  }
+};
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -1,27 +1,22 @@
 "use strict";
 
-var {interfaces: Ci, utils: Cu} = Components;
-
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
-const {
+var {
   HiddenExtensionPage,
   promiseExtensionViewLoaded,
 } = ExtensionParent;
 
-// WeakMap[Extension -> BackgroundPage]
-var backgroundPagesMap = new WeakMap();
-
 // Responsible for the background_page section of the manifest.
 class BackgroundPage extends HiddenExtensionPage {
   constructor(extension, options) {
     super(extension, "background");
 
     this.page = options.page || null;
     this.isGenerated = !!options.scripts;
     this.webNav = null;
@@ -33,59 +28,62 @@ class BackgroundPage extends HiddenExten
     }
 
     if (!this.extension.isExtensionURL(this.url)) {
       this.extension.manifestError("Background page must be a file within the extension");
       this.url = this.extension.baseURI.resolve("_blank.html");
     }
   }
 
-  build() {
-    return Task.spawn(function* () {
-      yield this.createBrowserElement();
+  async build() {
+    await this.createBrowserElement();
+
+    extensions.emit("extension-browser-inserted", this.browser);
 
-      extensions.emit("extension-browser-inserted", this.browser);
+    this.browser.loadURI(this.url);
+
+    let context = await promiseExtensionViewLoaded(this.browser);
 
-      this.browser.loadURI(this.url);
-
-      yield promiseExtensionViewLoaded(this.browser);
+    if (this.browser.docShell) {
+      this.webNav = this.browser.docShell.QueryInterface(Ci.nsIWebNavigation);
+      let window = this.webNav.document.defaultView;
 
-      if (this.browser.docShell) {
-        this.webNav = this.browser.docShell.QueryInterface(Ci.nsIWebNavigation);
-        let window = this.webNav.document.defaultView;
+      // Set the add-on's main debugger global, for use in the debugger
+      // console.
+      if (this.extension.addonData.instanceID) {
+        AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
+                    .then(addon => addon.setDebugGlobal(window));
+      }
+    }
 
-        // Set the add-on's main debugger global, for use in the debugger
-        // console.
-        if (this.extension.addonData.instanceID) {
-          AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
-                      .then(addon => addon.setDebugGlobal(window));
-        }
-      }
+    if (context) {
+      // Wait until all event listeners registered by the script so far
+      // to be handled.
+      await Promise.all(context.listenerPromises);
+    }
+    context.listenerPromises = null;
 
-      this.extension.emit("startup");
-    }.bind(this));
+    this.extension.emit("startup");
   }
 
   shutdown() {
     if (this.extension.addonData.instanceID) {
       AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
                   .then(addon => addon.setDebugGlobal(null));
     }
 
     super.shutdown();
   }
 }
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_background", (type, directive, extension, manifest) => {
-  let bgPage = new BackgroundPage(extension, manifest.background);
+this.backgroundPage = class extends ExtensionAPI {
+  onManifestEntry(entryName) {
+    let {manifest} = this.extension;
 
-  backgroundPagesMap.set(extension, bgPage);
-  return bgPage.build();
-});
+    this.bgPage = new BackgroundPage(this.extension, manifest.background);
 
-extensions.on("shutdown", (type, extension) => {
-  if (backgroundPagesMap.has(extension)) {
-    backgroundPagesMap.get(extension).shutdown();
-    backgroundPagesMap.delete(extension);
+    return this.bgPage.build();
   }
-});
-/* eslint-enable mozilla/balanced-listeners */
+
+  onShutdown() {
+    this.bgPage.shutdown();
+  }
+};
--- a/toolkit/components/extensions/ext-browser-content.js
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -1,32 +1,31 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
                                   "resource://gre/modules/Timer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "require",
                                   "resource://devtools/shared/Loader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
                                   "resource://gre/modules/Timer.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "colorUtils", () => {
   return require("devtools/shared/css/color").colorUtils;
 });
 
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 const {
   getWinUtils,
   stylesheetMap,
 } = ExtensionUtils;
 
 /* globals addMessageListener, addEventListener, content, docShell, removeEventListener, sendAsyncMessage */
 
 // Minimum time between two resizes.