Просмотр исходного кода

Merge branch 'refactor/web-extension'

Gerald 8 лет назад
Родитель
Сommit
a2452ce6d5

+ 1 - 1
.eslintrc.yml

@@ -34,10 +34,10 @@ env:
   node: true
 
 globals:
-  chrome: true
   zip: true
   Vue: true
   CodeMirror: true
   Promise: true
+  browser: true
 
 extends: 'eslint:recommended'

+ 2 - 2
gulpfile.js

@@ -141,7 +141,7 @@ gulp.task('copy-files', () => {
   const injectedFilter = gulpFilter(['**/injected.js'], {restore: true});
   const jsFilter = gulpFilter(['**/*.js'], {restore: true});
   const cssFilter = gulpFilter(['**/*.css'], {restore: true});
-  var stream = gulp.src(paths.copy)
+  var stream = gulp.src(paths.copy, {base: 'src'})
   .pipe(injectedFilter)
   .pipe(wrap({
     header: '!function(){\n',
@@ -185,7 +185,7 @@ gulp.task('svg', () => (
       },
     },
   }))
-  .pipe(gulp.dest('dist/images'))
+  .pipe(gulp.dest('dist/public'))
 ));
 
 gulp.task('build', [

+ 1 - 3
src/_locales/zh/messages.yml

@@ -220,9 +220,7 @@ buttonSaveClose:
   message: 保存并关闭
 labelNoScripts:
   description: Message shown when no script is installed.
-  message: >-
-    你居然没有脚本!<br>快<a href=http://gerald.top/tag/userjs target=_blank>点此</a>查看<a
-    href=http://gerald.top target=_blank style='color:orange'>Gerald</a>的脚本吧~
+  message: 奥欧,您还没有任何脚本~
 buttonDisable:
   description: Button to disable a script.
   message: 禁用

+ 52 - 81
src/background/app.js

@@ -3,24 +3,32 @@ var VMDB = require('./db');
 var sync = require('./sync');
 var requests = require('./requests');
 var cache = require('./utils/cache');
-var tabsUtils = require('./utils/tabs');
 var scriptUtils = require('./utils/script');
 var clipboard = require('./utils/clipboard');
 var options = require('./options');
 
 var vmdb = exports.vmdb = new VMDB;
-var VM_VER = chrome.runtime.getManifest().version;
+var VM_VER = browser.runtime.getManifest().version;
 
 options.hook(function (changes) {
   if ('isApplied' in changes) {
     setIcon(changes.isApplied);
   }
-  _.messenger.post({
+  browser.runtime.sendMessage({
     cmd: 'UpdateOptions',
     data: changes,
   });
 });
 
+function broadcast(data) {
+  browser.tabs.query({})
+  .then(function (tabs) {
+    tabs.forEach(function (tab) {
+      browser.tabs.sendMessage(tab.id, data);
+    });
+  });
+}
+
 var autoUpdate = function () {
   function check() {
     checking = true;
@@ -59,12 +67,14 @@ var commands = {
   GetInjected: function (url, src) {
     var data = {
       isApplied: options.get('isApplied'),
-      injectMode: options.get('injectMode'),
       version: VM_VER,
     };
-    if (src.tab && src.url === src.tab.url) {
-      chrome.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
-    }
+    setTimeout(function () {
+      // delayed to wait for the tab URL updated
+      if (src.tab && url === src.tab.url) {
+        browser.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
+      }
+    });
     return data.isApplied
       ? vmdb.getScriptsByURL(url).then(function (res) {
         return Object.assign(data, res);
@@ -76,7 +86,7 @@ var commands = {
     })
     .then(function (script) {
       sync.sync();
-      _.messenger.post({
+      browser.runtime.sendMessage({
         cmd: 'UpdateScript',
         data: script,
       });
@@ -85,7 +95,7 @@ var commands = {
   SetValue: function (data, _src) {
     return vmdb.setValue(data.uri, data.values)
     .then(function () {
-      tabsUtils.broadcast({
+      broadcast({
         cmd: 'UpdateValues',
         data: {
           uri: data.uri,
@@ -112,28 +122,27 @@ var commands = {
   ParseScript: function (data, _src) {
     return vmdb.parseScript(data).then(function (res) {
       var meta = res.data.meta;
-      if (!meta.grant.length && !options.get('ignoreGrant'))
+      if (!meta.grant.length && !options.get('ignoreGrant')) {
         notify({
           id: 'VM-NoGrantWarning',
           title: _.i18n('Warning'),
-          body: _.i18n('msgWarnGrant', [meta.name||_.i18n('labelNoName')]),
+          body: _.i18n('msgWarnGrant', [meta.name || _.i18n('labelNoName')]),
           isClickable: true,
         });
-      _.messenger.post(res);
+      }
+      browser.runtime.sendMessage(res);
       sync.sync();
       return res.data;
     });
   },
   CheckUpdate: function (id, _src) {
     vmdb.getScript(id).then(vmdb.checkUpdate);
-    return false;
   },
   CheckUpdateAll: function (_data, _src) {
     options.set('lastUpdate', Date.now());
     vmdb.getScriptsByIndex('update', 1).then(function (scripts) {
       return Promise.all(scripts.map(vmdb.checkUpdate));
     });
-    return false;
   },
   ParseMeta: function (code, _src) {
     return scriptUtils.parseMeta(code);
@@ -144,57 +153,46 @@ var commands = {
   },
   HttpRequest: function (details, src) {
     requests.httpRequest(details, function (res) {
-      _.messenger.send(src.tab.id, {
+      browser.tabs.sendMessage(src.tab.id, {
         cmd: 'HttpRequested',
         data: res,
       });
     });
-    return false;
   },
   AbortRequest: function (id, _src) {
     return requests.abortRequest(id);
   },
   SetBadge: function (num, src) {
     setBadge(num, src);
-    return false;
   },
   SyncAuthorize: function (_data, _src) {
     sync.authorize();
-    return false;
   },
   SyncRevoke: function (_data, _src) {
     sync.revoke();
-    return false;
   },
   SyncStart: function (_data, _src) {
     sync.sync();
-    return false;
   },
   GetFromCache: function (data, _src) {
     return cache.get(data) || null;
   },
   Notification: function (data, _src) {
-    return new Promise(function (resolve) {
-      chrome.notifications.create({
-        type: 'basic',
-        title: data.title || _.i18n('extName'),
-        message: data.text,
-        iconUrl: data.image || _.defaultImage,
-      }, function (id) {
-        resolve(id);
-      });
+    return browser.notifications.create({
+      type: 'basic',
+      title: data.title || _.i18n('extName'),
+      message: data.text,
+      iconUrl: data.image || _.defaultImage,
     });
   },
   SetClipboard: function (data, _src) {
     clipboard.set(data);
-    return false;
   },
   OpenTab: function (data, _src) {
-    chrome.tabs.create({
+    browser.tabs.create({
       url: data.url,
       active: data.active,
     });
-    return false;
   },
   GetAllOptions: function (_data, _src) {
     return options.getAll();
@@ -210,39 +208,21 @@ var commands = {
     data.forEach(function (item) {
       options.set(item.key, item.value);
     });
-    return false;
   },
 };
 
 vmdb.initialized.then(function () {
-  chrome.runtime.onMessage.addListener(function (req, src, callback) {
+  browser.runtime.onMessage.addListener(function (req, src) {
     var func = commands[req.cmd];
+    var res;
     if (func) {
-      var res = func(req.data, src);
-      if (res === false) return;
-      var finish = function (data) {
-        try {
-          callback(data);
-        } catch (e) {
-          // callback fails if not given in content page
-        }
-      };
-      Promise.resolve(res).then(function (data) {
-        finish({
-          data: data,
-          error: null,
-        });
-      }, function (data) {
-        if (data instanceof Error) {
-          console.error(data);
-          data = data.toString();
-        }
-        finish({
-          error: data,
-        });
-      });
-      return true;
+      res = func(req.data, src);
+      if (typeof res !== 'undefined') {
+        // If res is not instance of native Promise, browser APIs will not wait for it.
+        res = Promise.resolve(res);
+      }
     }
+    return res;
   });
   setTimeout(autoUpdate, 2e4);
   sync.initialize();
@@ -251,7 +231,7 @@ vmdb.initialized.then(function () {
 // Common functions
 
 function notify(options) {
-  chrome.notifications.create(options.id || 'ViolentMonkey', {
+  browser.notifications.create(options.id || 'ViolentMonkey', {
     type: 'basic',
     iconUrl: _.defaultImage,
     title: options.title + ' - ' + _.i18n('extName'),
@@ -266,12 +246,12 @@ var setBadge = function () {
     var o = badges[src.id];
     if (!o) o = badges[src.id] = {num: 0};
     o.num += num;
-    chrome.browserAction.setBadgeBackgroundColor({
+    browser.browserAction.setBadgeBackgroundColor({
       color: '#808',
       tabId: src.tab.id,
     });
     var text = (options.get('showBadge') && o.num || '').toString();
-    chrome.browserAction.setBadgeText({
+    browser.browserAction.setBadgeText({
       text: text,
       tabId: src.tab.id,
     });
@@ -282,40 +262,31 @@ var setBadge = function () {
   };
 }();
 
-_.messenger = function () {
-  return {
-    post: function (data) {
-      chrome.runtime.sendMessage(data);
-    },
-    send: function (tabId, data) {
-      chrome.tabs.sendMessage(tabId, data);
-    },
-  };
-}();
-
 function setIcon(isApplied) {
-  chrome.browserAction.setIcon({
+  browser.browserAction.setIcon({
     path: {
-      19: '/images/icon19' + (isApplied ? '' : 'w') + '.png',
-      38: '/images/icon38' + (isApplied ? '' : 'w') + '.png'
+      19: '/public/images/icon19' + (isApplied ? '' : 'w') + '.png',
+      38: '/public/images/icon38' + (isApplied ? '' : 'w') + '.png'
     },
   });
 }
 setIcon(options.get('isApplied'));
 
-chrome.notifications.onClicked.addListener(function (id) {
-  if (id == 'VM-NoGrantWarning') {
-    tabsUtils.create('http://wiki.greasespot.net/@grant');
+browser.notifications.onClicked.addListener(function (id) {
+  if (id === 'VM-NoGrantWarning') {
+    browser.tabs.create({
+      url: 'http://wiki.greasespot.net/@grant',
+    });
   } else {
-    tabsUtils.broadcast({
+    broadcast({
       cmd: 'NotificationClick',
       data: id,
     });
   }
 });
 
-chrome.notifications.onClosed.addListener(function (id) {
-  tabsUtils.broadcast({
+browser.notifications.onClosed.addListener(function (id) {
+  broadcast({
     cmd: 'NotificationClose',
     data: id,
   });

+ 9 - 8
src/background/db.js

@@ -1,5 +1,6 @@
 var scriptUtils = require('./utils/script');
 var tester = require('./utils/tester');
+var Promise = require('./utils/sync-promise');
 var _ = require('../common');
 
 function VMDB() {
@@ -218,7 +219,7 @@ VMDB.prototype.removeScript = function (id) {
     };
   })
   .then(function () {
-    _.messenger.post({
+    browser.runtime.sendMessage({
       cmd: 'RemoveScript',
       data: id,
     });
@@ -606,35 +607,35 @@ VMDB.prototype.checkUpdate = function () {
         return Promise.resolve();
       res.data.checking = false;
       res.data.message = _.i18n('msgNoUpdate');
-      _.messenger.post(res);
+      browser.runtime.sendMessage(res);
       return Promise.reject();
     };
     var errHandler = function (_xhr) {
       res.data.checking = false;
       res.data.message = _.i18n('msgErrorFetchingUpdateInfo');
-      _.messenger.post(res);
+      browser.runtime.sendMessage(res);
       return Promise.reject();
     };
     var update = function () {
       if (!downloadURL) {
         res.data.message = '<span class="new">' + _.i18n('msgNewVersion') + '</span>';
-        _.messenger.post(res);
+        browser.runtime.sendMessage(res);
         return Promise.reject();
       }
       res.data.message = _.i18n('msgUpdating');
-      _.messenger.post(res);
+      browser.runtime.sendMessage(res);
       return scriptUtils.fetch(downloadURL).then(function (xhr) {
         return xhr.responseText;
       }, function (_xhr) {
         res.data.checking = false;
         res.data.message = _.i18n('msgErrorFetchingScript');
-        _.messenger.post(res);
+        browser.runtime.sendMessage(res);
         return Promise.reject();
       });
     };
     if (!updateURL) return Promise.reject();
     res.data.message = _.i18n('msgCheckingForUpdate');
-    _.messenger.post(res);
+    browser.runtime.sendMessage(res);
     return scriptUtils.fetch(updateURL, null, {
       Accept: 'text/x-userscript-meta',
     }).then(okHandler, errHandler).then(update);
@@ -652,7 +653,7 @@ VMDB.prototype.checkUpdate = function () {
           code: code,
         }).then(function (res) {
           res.data.checking = false;
-          _.messenger.post(res);
+          browser.runtime.sendMessage(res);
         });
       }, function () {
         delete processes[script.id];

+ 12 - 11
src/background/index.html

@@ -1,12 +1,13 @@
-<!DOCTYPE html>
-<html lang="en">
-	<head>
-		<meta charset="utf-8">
-		<title>ViolentMonkey</title>
-    <script src="/lib/define.js"></script>
-		<script src="/common.js"></script>
-	</head>
-  <body>
-    <script src="app.js"></script>
-  </body>
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>ViolentMonkey</title>
+  <script src="/public/lib/define.js"></script>
+  <script src="/public/mylib/browser.js"></script>
+  <script src="/common.js"></script>
+</head>
+<body>
+  <script src="app.js"></script>
+</body>
 </html>

+ 10 - 11
src/background/requests.js

@@ -1,4 +1,3 @@
-var tabsUtils = require('./utils/tabs');
 var cache = require('./utils/cache');
 var _ = require('../common');
 
@@ -113,7 +112,7 @@ function abortRequest(id) {
 }
 
 // Watch URL redirects
-chrome.webRequest.onBeforeRedirect.addListener(function (details) {
+browser.webRequest.onBeforeRedirect.addListener(function (details) {
   var reqId = verify[details.requestId];
   if (reqId) {
     var req = requests[reqId];
@@ -125,7 +124,7 @@ chrome.webRequest.onBeforeRedirect.addListener(function (details) {
 });
 
 // Modifications on headers
-chrome.webRequest.onBeforeSendHeaders.addListener(function (details) {
+browser.webRequest.onBeforeSendHeaders.addListener(function (details) {
   var headers = details.requestHeaders;
   var newHeaders = [];
   var vmHeaders = {};
@@ -160,7 +159,7 @@ chrome.webRequest.onBeforeSendHeaders.addListener(function (details) {
 
 // tasks are not necessary now, turned off
 // Stop redirects
-// chrome.webRequest.onHeadersReceived.addListener(function (details) {
+// browser.webRequest.onHeadersReceived.addListener(function (details) {
 //   var task = tasks[details.requestId];
 //   if (task) {
 //     delete tasks[details.requestId];
@@ -177,20 +176,20 @@ chrome.webRequest.onBeforeSendHeaders.addListener(function (details) {
 //   urls: ['<all_urls>'],
 //   types: ['xmlhttprequest'],
 // }, ['blocking', 'responseHeaders']);
-// chrome.webRequest.onCompleted.addListener(function (details) {
+// browser.webRequest.onCompleted.addListener(function (details) {
 //   delete tasks[details.requestId];
 // }, {
 //   urls: ['<all_urls>'],
 //   types: ['xmlhttprequest'],
 // });
-// chrome.webRequest.onErrorOccurred.addListener(function (details) {
+// browser.webRequest.onErrorOccurred.addListener(function (details) {
 //   delete tasks[details.requestId];
 // }, {
 //   urls: ['<all_urls>'],
 //   types: ['xmlhttprequest'],
 // });
 
-chrome.webRequest.onBeforeRequest.addListener(function (req) {
+browser.webRequest.onBeforeRequest.addListener(function (req) {
   // onBeforeRequest is fired for local files too
   if (/\.user\.js([\?#]|$)/.test(req.url)) {
     // {cancel: true} will redirect to a blocked view
@@ -205,10 +204,10 @@ chrome.webRequest.onBeforeRequest.addListener(function (req) {
     }
     if ((!x.status || x.status == 200) && !/^\s*</.test(x.responseText)) {
       cache.set(req.url, x.responseText);
-      var url = chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url);
-      if (req.tabId < 0) tabsUtils.create(url);
-      else tabsUtils.get(req.tabId).then(function (t) {
-        tabsUtils.create(url + '/' + encodeURIComponent(t.url));
+      var url = browser.runtime.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url);
+      if (req.tabId < 0) browser.tabs.create({url: url});
+      else browser.tabs.get(req.tabId).then(function (tab) {
+        browser.tabs.create({url: url + '/' + encodeURIComponent(tab.url)});
       });
       return noredirect;
     }

+ 2 - 2
src/background/sync/base.js

@@ -175,7 +175,7 @@ var BaseService = serviceFactory({
     return this.events.fire.apply(null, arguments);
   },
   onStateChange: function () {
-    _.messenger.post({
+    browser.runtime.sendMessage({
       cmd: 'UpdateSync',
       data: getStates(),
     });
@@ -406,7 +406,7 @@ var BaseService = serviceFactory({
             }
             return app.vmdb.parseScript(data)
             .then(function (res) {
-              _.messenger.post(res);
+              browser.runtime.sendMessage(res);
             });
           });
         }),

+ 1 - 2
src/background/sync/dropbox.js

@@ -1,5 +1,4 @@
 var base = require('./base');
-var tabsUtils = require('../utils/tabs');
 var searchUtils = require('../utils/search');
 var config = {
   client_id: 'f0q12zup2uys5w8',
@@ -15,7 +14,7 @@ function authorize() {
   var url = 'https://www.dropbox.com/oauth2/authorize';
   var qs = searchUtils.dump(params);
   url += '?' + qs;
-  tabsUtils.create(url);
+  browser.tabs.create({url: url});
 }
 function checkAuth(url) {
   var redirect_uri = config.redirect_uri + '#';

+ 2 - 3
src/background/sync/index.js

@@ -1,8 +1,7 @@
-var tabs = require('../utils/tabs');
 var base = require('./base');
 
-tabs.update(function (tab) {
-  tab.url && base.checkAuthUrl(tab.url) && tabs.remove(tab.id);
+browser.tabs.onUpdated.addListener(function (tabId, changes) {
+  changes.url && base.checkAuthUrl(changes.url) && browser.tabs.remove(tabId);
 });
 
 // import sync modules

+ 1 - 2
src/background/sync/onedrive.js

@@ -2,7 +2,6 @@
 
 var _ = require('src/common');
 var base = require('./base');
-var tabsUtils = require('../utils/tabs');
 var searchUtils = require('../utils/search');
 
 var config = Object.assign({
@@ -23,7 +22,7 @@ function authorize() {
   var url = 'https://login.live.com/oauth20_authorize.srf';
   var qs = searchUtils.dump(params);
   url += '?' + qs;
-  tabsUtils.create(url);
+  browser.tabs.create({url: url});
 }
 function checkAuth(url) {
   var redirect_uri = config.redirect_uri + '?code=';

+ 175 - 0
src/background/utils/sync-promise.js

@@ -0,0 +1,175 @@
+/**
+ * @desc A synchronous Promise implementation.
+ * @author Gerald <[email protected]>
+ *
+ * https://github.com/gera2ld/sync-promise-lite
+ */
+!function (root, factory) {
+  if (typeof module === 'object' && module.exports)
+    module.exports = factory();
+  else
+    root.Promise = root.Promise || factory();
+}(typeof window !== 'undefined' ? window : this, function () {
+
+  var PENDING = 'pending';
+  var FULFILLED = 'fulfilled';
+  var REJECTED = 'rejected';
+
+  function syncCall(func, args) {
+    func.apply(null, args);
+  }
+  function thenFactory(isStatus, getValue, addHandler) {
+    return function (okHandler, errHandler) {
+      var pending = true;
+      var handle;
+      addHandler(function () {
+        pending = false;
+        handle && handle();
+      });
+      return new Promise(function (resolve, reject) {
+        handle = function () {
+          var result;
+          var resolved = isStatus(FULFILLED);
+          var handler = resolved ? okHandler : errHandler;
+          if (handler) {
+            try {
+              result = handler(getValue());
+            } catch (e) {
+              return reject(e);
+            }
+          } else {
+            result = getValue();
+            if (!resolved) return reject(result);
+          }
+          resolve(result);
+        }
+        pending || handle();
+      });
+    };
+  }
+
+  function Promise(resolver) {
+    var status = PENDING;
+    var value;
+    var handlers = [];
+    var uncaught = true;
+    var resolve = function (data) {
+      if (!isStatus(PENDING)) return;
+      if (data && typeof data.then === 'function') {
+        data.then(resolve, reject);
+      } else {
+        status = FULFILLED;
+        value = data;
+        then();
+      }
+    };
+    var reject = function (reason) {
+      if (!isStatus(PENDING)) return;
+      status = REJECTED;
+      value = reason;
+      setTimeout(function () {
+        uncaught && Promise.onUncaught(reason);
+      });
+      then();
+    };
+    var then = function () {
+      handlers.splice(0).forEach(function (func) {
+        syncCall(func);
+      });
+    };
+    var isStatus = function (_status) {
+      return status === _status;
+    };
+    var getValue = function () {
+      return value;
+    };
+    var addHandler = function (handler) {
+      uncaught = false;
+      if (isStatus(PENDING)) handlers.push(handler);
+      else syncCall(handler);
+    };
+    this.then = thenFactory(isStatus, getValue, addHandler);
+    syncCall(resolver, [resolve, reject]);
+  }
+
+  Promise.onUncaught = function (reason) {
+    console.error('Uncaught (in promise)', reason);
+  };
+
+  Promise.prototype.catch = function (errHandler) {
+    return this.then(null, errHandler);
+  };
+
+  Promise.resolve = function (data) {
+    return new Promise(function (resolve) {
+      resolve(data);
+    });
+  };
+
+  Promise.reject = function (data) {
+    return new Promise(function (resolve, reject) {
+      reject(data);
+    });
+  };
+
+  Promise.all = function (promises) {
+    return new Promise(function (resolve, reject) {
+      function rejectAll(reason) {
+        if (results) {
+          results = null;
+          reject(reason);
+        }
+      }
+      function resolveOne(data, i) {
+        if (results) {
+          results[i] = data;
+          pending --;
+          check();
+        }
+      }
+      function check() {
+        results && !pending && resolve(results);
+      }
+      var results = [];
+      var pending = promises.length;
+      promises.forEach(function (promise, i) {
+        if (promise instanceof Promise) {
+          promise.then(function (data) {
+            resolveOne(data, i);
+          }, rejectAll);
+        } else {
+          resolveOne(promise, i);
+        }
+      });
+      check();
+    });
+  };
+
+  Promise.race = function (promises) {
+    return new Promise(function (resolve, reject) {
+      function resolveAll(data) {
+        if (pending) {
+          pending = false;
+          resolve(data);
+        }
+      }
+      function rejectAll(reason) {
+        if (pending) {
+          pending = false;
+          reject(reason);
+        }
+      }
+      var pending = true;
+      promises.forEach(function (promise) {
+        if (promise instanceof Promise) {
+          promise.then(resolveAll, rejectAll);
+        } else {
+          resolveAll(promise);
+        }
+      });
+    });
+  };
+
+  return Promise;
+
+});

+ 0 - 30
src/background/utils/tabs.js

@@ -1,30 +0,0 @@
-module.exports = {
-  create: function (url) {
-    chrome.tabs.create({url: url});
-  },
-  update: function (cb) {
-    chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, _tab) {
-      cb({
-        id: tabId,
-        url: changeInfo.url,
-      });
-    });
-  },
-  remove: function (id) {
-    chrome.tabs.remove(id);
-  },
-  get: function (id) {
-    return new Promise(function (resolve, _reject) {
-      chrome.tabs.get(id, function (tab) {
-        resolve(tab);
-      });
-    });
-  },
-  broadcast: function (data) {
-    chrome.tabs.query({}, function (tabs) {
-      tabs.forEach(function (tab) {
-        chrome.tabs.sendMessage(tab.id, data);
-      });
-    });
-  },
-};

+ 15 - 15
src/cache.js

@@ -1,24 +1,24 @@
-function Cache(allowOverride) {
-  this.data = {};
-  this.allowOverride = allowOverride;
+function getCache() {
+  function put(key, value) {
+    if (key in data) {
+      throw 'Key {' + key + '} already exists!';
+    }
+    data[key] = value;
+  }
+  function get(key) {
+    if (key in data) return data[key];
+    throw 'Cache not found: ' + key;
+  }
+  var data = {};
+  return {get: get, put: put};
 }
-Cache.prototype.put = function (key, fn) {
-  if (key in this.data && !this.allowOverride)
-    throw 'Key {' + key + '} already exists!';
-  this.data[key] = fn;
-};
-Cache.prototype.get = function (key) {
-  var data = this.data;
-  if (key in data) return data[key];
-  throw 'Cache not found: ' + key;
-};
 
 var _ = require('./common');
 Vue.prototype.i18n = _.i18n;
 
 !function () {
   var xhr = new XMLHttpRequest;
-  xhr.open('GET', '/images/sprite.svg', true);
+  xhr.open('GET', '/public/sprite.svg', true);
   xhr.onload = function () {
     var div = document.createElement('div');
     div.style.display = 'none';
@@ -29,4 +29,4 @@ Vue.prototype.i18n = _.i18n;
 }();
 
 /* eslint-disable no-unused-vars */
-var cache = module.exports = new Cache();
+var cache = module.exports = getCache();

+ 9 - 7
src/common.js

@@ -37,10 +37,11 @@ polyfill(Array.prototype, 'find', function (predicate) {
 // Polyfill end
 
 var _ = exports;
+
 _.i18n = function (name, args) {
-  return chrome.i18n.getMessage(name, args) || name;
+  return browser.i18n.getMessage(name, args) || name;
 };
-_.defaultImage = '/images/icon128.png';
+_.defaultImage = '/public/images/icon128.png';
 
 function normalizeKeys(key) {
   if (!key) key = [];
@@ -146,10 +147,9 @@ _.initOptions = function () {
 };
 
 _.sendMessage = function (data) {
-  return new Promise(function (resolve, reject) {
-    chrome.runtime.sendMessage(data, function (res) {
-      res && res.error ? reject(res.error) : resolve(res && res.data);
-    });
+  return browser.runtime.sendMessage(data)
+  .catch(function (err) {
+    console.error(err);
   });
 };
 
@@ -159,8 +159,10 @@ _.debounce = function (func, time) {
     func.apply(thisObj, args);
   }
   var timer;
-  return function (args) {
+  return function () {
     timer && clearTimeout(timer);
+    var args = [];
+    for (var i = 0; i < arguments.length; i++) args.push(arguments[i]);
     timer = setTimeout(run, time, this, args);
   };
 };

+ 24 - 23
src/injected.js

@@ -5,13 +5,7 @@ window.VM = 1;
 function getUniqId() {
   return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
 }
-function sendMessage(data) {
-  return new Promise(function (resolve, reject) {
-    chrome.runtime.sendMessage(data, function (res) {
-      res && res.error ? reject(res.error) : resolve(res && res.data);
-    });
-  });
-}
+function noop() {}
 function includes(arr, item) {
   for (var i = arr.length; i --;) {
     if (arr[i] === item) return true;
@@ -57,6 +51,9 @@ function utf8decode(utftext) {
   return string;
 }
 
+function sendMessage(data) {
+  return browser.runtime.sendMessage(data);
+}
 function getPopup(){
   // XXX: only scripts run in top level window are counted
   top === window && sendMessage({
@@ -65,7 +62,8 @@ function getPopup(){
       ids: ids,
       menus: menus,
     },
-  });
+  })
+  .catch(noop);
 }
 
 var badge = {
@@ -73,11 +71,11 @@ var badge = {
   ready: false,
   willSet: false,
 };
-function getBadge(){
+function getBadge() {
   badge.willSet = true;
   setBadge();
 }
-function setBadge(){
+function setBadge() {
   if (badge.ready && badge.willSet) {
     // XXX: only scripts run in top level window are counted
     top === window && sendMessage({cmd: 'SetBadge', data: badge.number});
@@ -198,7 +196,11 @@ var comm = {
     var comm = this;
     comm.sid = comm.vmid + srcId;
     comm.did = comm.vmid + destId;
-    document.addEventListener(comm.sid, comm['handle' + srcId].bind(comm), false);
+    var handle = comm['handle' + srcId];
+    document.addEventListener(comm.sid, function (e) {
+      var data = JSON.parse(e.detail);
+      handle.call(comm, data);
+    }, false);
     comm.load = comm.checkLoad = function () {};
     // check whether the page is injectable via <script>, whether limited by CSP
     try {
@@ -208,11 +210,11 @@ var comm = {
     }
   },
   post: function (data) {
-    var e = new CustomEvent(this.did, {detail: data});
+    // Firefox issue: data must be stringified to avoid cross-origin problem
+    var e = new CustomEvent(this.did, {detail: JSON.stringify(data)});
     document.dispatchEvent(e);
   },
-  handleR: function (e) {
-    var obj = e.detail;
+  handleR: function (obj) {
     var comm = this;
     var maps = {
       LoadScript: comm.loadScript.bind(comm),
@@ -353,7 +355,7 @@ var comm = {
       };
       details.url = getFullUrl(details.url);
       comm.qrequests.push(t);
-      comm.post({cmd:'GetRequestId'});
+      comm.post({cmd: 'GetRequestId'});
       return t.req;
     };
   },
@@ -527,9 +529,9 @@ var comm = {
         },
       },
       GM_log: {
-        /* eslint-disable no-console */
-        value: function (data) {console.log(data);},
-        /* eslint-enable no-console */
+        value: function (data) {
+          console.log(data);  // eslint-disable-line no-console
+        },
       },
       GM_openInTab: {
         value: function (url, background) {
@@ -685,8 +687,7 @@ function injectScript(data) {
   };
   inject('!' + func.toString() + '(' + JSON.stringify(data[0]) + ',' + JSON.stringify(comm.did) + ',function(' + data[1].join(',') + '){' + data[2] + '})');
 }
-function handleC(e) {
-  var req = e.detail;
+function handleC(req) {
   if (!req) {
     console.error('[Violentmonkey] Invalid data! There might be unsupported data format.');
     return;
@@ -742,8 +743,8 @@ function onNotificationClose(nid) {
 }
 
 // Messages
-chrome.runtime.onMessage.addListener(function (req, src) {
-  var maps = {
+browser.runtime.onMessage.addListener(function (req, src) {
+  var handlers = {
     Command: function (data) {
       comm.post({cmd: 'Command', data: data});
     },
@@ -756,7 +757,7 @@ chrome.runtime.onMessage.addListener(function (req, src) {
     NotificationClick: onNotificationClick,
     NotificationClose: onNotificationClose,
   };
-  var func = maps[req.cmd];
+  var func = handlers[req.cmd];
   if (func) func(req.data, src);
 });
 

+ 7 - 9
src/manifest.json

@@ -3,21 +3,18 @@
   "version": "__VERSION__",
   "manifest_version": 2,
   "description": "__MSG_extDescription__",
-  "author": {
-    "name": "Gerald",
-    "url": "http://gerald.top"
-  },
+  "author": "Gerald",
   "homepage_url": "https://violentmonkey.github.io/",
   "icons": {
-    "16": "images/icon16.png",
-    "48": "images/icon48.png",
-    "128": "images/icon128.png"
+    "16": "public/images/icon16.png",
+    "48": "public/images/icon48.png",
+    "128": "public/images/icon128.png"
   },
   "default_locale": "en",
   "browser_action": {
     "default_icon": {
-      "19": "images/icon19.png",
-      "38": "images/icon38.png"
+      "19": "public/images/icon19.png",
+      "38": "public/images/icon38.png"
     },
     "default_title": "__MSG_extName__",
     "default_popup": "popup/index.html"
@@ -29,6 +26,7 @@
   "content_scripts": [
     {
       "js": [
+        "public/mylib/browser.js",
         "injected.js"
       ],
       "matches": [

+ 2 - 2
src/options/app.js

@@ -102,12 +102,12 @@ var handlers = {
     _.options.update(data);
   },
 };
-chrome.runtime.onMessage.addListener(function (res) {
+browser.runtime.onMessage.addListener(function (res) {
   var handle = handlers[res.cmd];
   handle && handle(res.data);
 });
 window.addEventListener('hashchange', loadHash, false);
-zip.workerScriptsPath = '/lib/zip.js/';
+zip.workerScriptsPath = '/public/lib/zip.js/';
 document.title = _.i18n('extName');
 loadHash();
 initCustomCSS();

+ 5 - 4
src/options/index.html

@@ -3,11 +3,12 @@
 <head>
   <meta charset="utf-8">
   <title></title>
-  <link rel="shortcut icon" type="image/png" href="/images/icon16.png">
+  <link rel="shortcut icon" type="image/png" href="/public/images/icon16.png">
   <link rel="stylesheet" href="style.css">
-  <script src="/lib/vue.min.js"></script>
-  <script src="/lib/zip.js/zip.js"></script>
-  <script src="/lib/define.js"></script>
+  <script src="/public/lib/vue.min.js"></script>
+  <script src="/public/lib/zip.js/zip.js"></script>
+  <script src="/public/lib/define.js"></script>
+  <script src="/public/mylib/browser.js"></script>
   <script src="/common.js"></script>
   <script src="/cache.js"></script>
 </head>

+ 7 - 5
src/options/style.css

@@ -288,11 +288,13 @@ code {
 .script-info {
   margin-left: 3.5rem;
   line-height: 1.5;
-}
-.script-info > * {
-  display: inline-block;
-  margin-right: .8rem;
-  vertical-align: middle;
+  align-items: center;
+  > * {
+    margin-right: .8rem;
+  }
+  .icon {
+    display: block;
+  }
 }
 .script-icon {
   position: absolute;

+ 41 - 14
src/options/utils/features.js

@@ -15,6 +15,7 @@ var features = _.options.get(key);
 if (!features || !features.data) features = {
   data: {},
 };
+var items = {};
 
 exports.reset = function (version) {
   if (features.version !== version) {
@@ -25,22 +26,48 @@ exports.reset = function (version) {
   }
 };
 
+function getContext(el, value) {
+  function onFeatureClick(_e) {
+    features.data[value] = 1;
+    _.options.set(key, features);
+    el.classList.remove('feature');
+    el.removeEventListener('click', onFeatureClick, false);
+  }
+  function clear() {
+    el.removeEventListener('click', onFeatureClick, false);
+  }
+  function reset() {
+    clear();
+    if (!features.version || features.data[value]) return;
+    el.classList.add('feature');
+    el.addEventListener('click', onFeatureClick, false);
+  }
+  return {
+    el: el,
+    reset: reset,
+    clear: clear,
+  };
+}
+
 Vue.directive('feature', {
   bind: function (el, binding) {
-    function onFeatureClick(_e) {
-      features.data[value] = 1;
-      _.options.set(key, features);
-      el.classList.remove('feature');
-      el.removeEventListener('click', onFeatureClick, false);
-    }
-    function reset() {
-      if (!features.version || features.data[value]) return;
-      el.classList.add('feature');
-      el.removeEventListener('click', onFeatureClick, false);
-      el.addEventListener('click', onFeatureClick, false);
-    }
     var value = binding.value;
-    reset();
-    hooks && hooks.hook(reset);
+    var item = getContext(el, value);
+    var list = items[value] = items[value] || [];
+    list.push(item);
+    item.reset();
+    hooks && hooks.hook(item.reset);
+  },
+  unbind: function (el, binding) {
+    var list = items[binding.value];
+    if (list) {
+      var index = list.findIndex(function (item) {
+        return item.el === el;
+      });
+      if (~index) {
+        list[index].clear();
+        list.splice(index, 1);
+      }
+    }
   },
 });

+ 15 - 15
src/options/views/editor.js

@@ -44,26 +44,26 @@ function getHandler(key) {
 
 function initCodeMirror() {
   addCSS([
-    {href: '/lib/CodeMirror/lib/codemirror.css'},
-    {href: '/lib/CodeMirror/theme/eclipse.css'},
-    {href: '/mylib/CodeMirror/fold.css'},
+    {href: '/public/lib/CodeMirror/lib/codemirror.css'},
+    {href: '/public/lib/CodeMirror/theme/eclipse.css'},
+    {href: '/public/mylib/CodeMirror/fold.css'},
   ]);
   return addScripts(
-    {src: '/lib/CodeMirror/lib/codemirror.js'}
+    {src: '/public/lib/CodeMirror/lib/codemirror.js'}
   )
   .then(function () {
     return addScripts([
-      {src: '/lib/CodeMirror/mode/javascript/javascript.js'},
-      {src: '/lib/CodeMirror/addon/comment/continuecomment.js'},
-      {src: '/lib/CodeMirror/addon/edit/matchbrackets.js'},
-      {src: '/lib/CodeMirror/addon/edit/closebrackets.js'},
-      {src: '/lib/CodeMirror/addon/fold/foldcode.js'},
-      {src: '/lib/CodeMirror/addon/fold/foldgutter.js'},
-      {src: '/lib/CodeMirror/addon/fold/brace-fold.js'},
-      {src: '/lib/CodeMirror/addon/fold/comment-fold.js'},
-      {src: '/lib/CodeMirror/addon/search/match-highlighter.js'},
-      {src: '/lib/CodeMirror/addon/search/searchcursor.js'},
-      {src: '/lib/CodeMirror/addon/selection/active-line.js'},
+      {src: '/public/lib/CodeMirror/mode/javascript/javascript.js'},
+      {src: '/public/lib/CodeMirror/addon/comment/continuecomment.js'},
+      {src: '/public/lib/CodeMirror/addon/edit/matchbrackets.js'},
+      {src: '/public/lib/CodeMirror/addon/edit/closebrackets.js'},
+      {src: '/public/lib/CodeMirror/addon/fold/foldcode.js'},
+      {src: '/public/lib/CodeMirror/addon/fold/foldgutter.js'},
+      {src: '/public/lib/CodeMirror/addon/fold/brace-fold.js'},
+      {src: '/public/lib/CodeMirror/addon/fold/comment-fold.js'},
+      {src: '/public/lib/CodeMirror/addon/search/match-highlighter.js'},
+      {src: '/public/lib/CodeMirror/addon/search/searchcursor.js'},
+      {src: '/public/lib/CodeMirror/addon/selection/active-line.js'},
     ]);
   })
   .then(function () {

+ 1 - 1
src/options/views/main.html

@@ -1,6 +1,6 @@
 <div class="main">
   <aside>
-    <img src="/images/icon128.png">
+    <img src="/public/images/icon128.png">
     <h1 v-text="i18n('extName')"></h1>
     <hr>
     <div class=sidemenu>

+ 10 - 9
src/options/views/script.html

@@ -1,18 +1,19 @@
 <div class="script" :class="{disabled:!script.enabled}" draggable="true">
-  <img class=script-icon :src="safeIcon">
-  <div class="script-version pull-right" v-text="script.meta.version?'v'+script.meta.version:''"></div>
-  <div class="script-author ellipsis pull-right" :title="script.meta.author" v-if="author">
-    <span v-text="i18n('labelAuthor')"></span>
-    <a :href="'mailto:'+author.email" v-if="author.email" v-text="author.name"></a>
-    <span v-if="!author.email" v-text="author.name"></a>
-  </div>
-  <div class=script-info>
+  <img class="script-icon" :src="safeIcon">
+  <div class="script-info flex">
     <a class="script-name ellipsis" target=_blank :href="homepageURL"
       v-text="script.custom.name||getLocaleString('name')"></a>
-    <a class="script-support" v-show="script.meta.supportURL"
+    <a class="script-support" v-if="script.meta.supportURL"
       target=_blank :href="script.meta.supportURL">
       <svg class="icon"><use xlink:href="#question"/></svg>
     </a>
+    <div class="flex-auto"></div>
+    <div class="script-author ellipsis" :title="script.meta.author" v-if="author">
+      <span v-text="i18n('labelAuthor')"></span>
+      <a :href="'mailto:'+author.email" v-if="author.email" v-text="author.name"></a>
+      <span v-if="!author.email" v-text="author.name"></span>
+    </div>
+    <div class="script-version" v-text="script.meta.version?'v'+script.meta.version:''"></div>
   </div>
   <p class="script-desc ellipsis" v-text="script.custom.description||getLocaleString('description')"></p>
   <div class=buttons>

+ 1 - 1
src/options/views/script.js

@@ -3,7 +3,7 @@ var cache = require('../../cache');
 var _ = require('../../common');
 var store = utils.store;
 
-var DEFAULT_ICON = '/images/icon48.png';
+var DEFAULT_ICON = '/public/images/icon48.png';
 
 module.exports = {
   props: ['script'],

+ 2 - 2
src/options/views/tab-about.html

@@ -8,7 +8,7 @@
     <label v-text="i18n('labelRelated')"></label>
     <a href="https://violentmonkey.github.io" target="_blank" v-text="i18n('extName')"></a> |
     <a href="https://violentmonkey.github.io/donate/" target="_blank" v-text="i18n('labelDonate')"></a> |
-    <a href="https://github.com/violentmonkey/violentmonkey/issues" target=_blank v-text="i18n('labelFeedback')"></a>
+    <a href="https://github.com/violentmonkey/violentmonkey/issues" target="_blank" v-text="i18n('labelFeedback')"></a>
   </div>
   <div class="mb-2">
     <label v-text="i18n('labelAuthor')"></label>
@@ -21,7 +21,7 @@
   <div class="mb-2">
     <label v-text="i18n('labelCurrentLang')"></label>
     <span id="currentLang" v-text="language"></span> |
-    <a href="https://violentmonkey.github.io/localization/" target=_blank>
+    <a href="https://violentmonkey.github.io/localization/" target="_blank">
       Help with translation
     </a>
   </div>

+ 2 - 2
src/options/views/tab-about.js

@@ -1,6 +1,6 @@
-var cache = require('../../cache');
+var cache = require('src/cache');
 var data = {
-  version: chrome.runtime.getManifest().version,
+  version: browser.runtime.getManifest().version,
   language: navigator.language,
 };
 

+ 2 - 2
src/options/views/tab-installed.js

@@ -42,8 +42,8 @@ module.exports = {
     installFromURL: function () {
       var url = prompt(_.i18n('hintInputURL'));
       if (url && ~url.indexOf('://')) {
-        chrome.tabs.create({
-          url: chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(url),
+        browser.tabs.create({
+          url: browser.runtime.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(url),
         });
       }
     },

+ 1 - 1
src/options/views/tab-settings/vm-sync/index.js

@@ -38,7 +38,7 @@ module.exports = {
       return services;
     },
     service: function () {
-      var current = this.syncConfig.current;
+      var current = this.syncConfig.current || '';
       var service = this.syncServices.find(function (item) {
         return item.name === current;
       });

+ 5 - 4
src/popup/app.js

@@ -3,7 +3,7 @@ _.initOptions();
 var App = require('./views/app');
 var utils = require('./utils');
 
-var app = new Vue({
+new Vue({
   el: '#app',
   render: function (h) {
     return h(App);
@@ -12,7 +12,7 @@ var app = new Vue({
 
 function init() {
   var currentTab = utils.store.currentTab;
-  chrome.tabs.sendMessage(currentTab.id, {cmd: 'GetPopup'});
+  browser.tabs.sendMessage(currentTab.id, {cmd: 'GetPopup'});
   if (currentTab && /^https?:\/\//i.test(currentTab.url)) {
     var matches = currentTab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
     var domain = matches[1];
@@ -42,12 +42,13 @@ var handlers = {
     _.options.update(data);
   },
 };
-chrome.runtime.onMessage.addListener(function (req, src, callback) {
+browser.runtime.onMessage.addListener(function (req, src, callback) {
   var func = handlers[req.cmd];
   if (func) func(req.data, src, callback);
 });
 
-chrome.tabs.query({currentWindow: true, active: true}, function (tabs) {
+browser.tabs.query({currentWindow: true, active: true})
+.then(function (tabs) {
   utils.store.currentTab = {
     id: tabs[0].id,
     url: tabs[0].url,

+ 3 - 2
src/popup/index.html

@@ -4,8 +4,9 @@
   <meta charset="utf-8">
   <title>Popup Menu - Violentmonkey</title>
   <link rel="stylesheet" href="style.css">
-  <script src="/lib/vue.min.js"></script>
-  <script src="/lib/define.js"></script>
+  <script src="/public/lib/vue.min.js"></script>
+  <script src="/public/lib/define.js"></script>
+  <script src="/public/mylib/browser.js"></script>
   <script src="/common.js"></script>
   <script src="/cache.js"></script>
 </head>

+ 1 - 1
src/popup/views/app/index.html

@@ -1,6 +1,6 @@
 <div id="app">
   <div class="logo" :class="{disabled:!options.isApplied}">
-    <img src="/images/icon128.png">
+    <img src="/public/images/icon128.png">
   </div>
   <div class="menu-item" :class="{disabled:!options.isApplied}" @click="onToggle">
     <icon :name="getSymbolCheck(options.isApplied)"></icon>

+ 14 - 11
src/popup/views/app/index.js

@@ -66,17 +66,20 @@ module.exports = {
       _.options.set('isApplied', !this.options.isApplied);
     },
     onManage: function () {
-      var url = chrome.extension.getURL(chrome.runtime.getManifest().options_page);
-      chrome.tabs.query({
+      var url = browser.runtime.getURL(browser.runtime.getManifest().options_page);
+      // Firefox: browser.tabs.query cannot filter tabs by URLs with custom
+      // schemes like `moz-extension:`
+      browser.tabs.query({
         currentWindow: true,
-        url: url,
-      }, function (tabs) {
+        // url: url,
+      })
+      .then(function (tabs) {
         var tab = tabs.find(function (tab) {
-          var hash = tab.url.match(/#(\w+)/);
-          return !hash || hash[1] !== 'confirm';
+          var parts = tab.url.split('#');
+          return parts[0] === url && (!parts[1] || parts[1].startsWith('confirm/'));
         });
-        if (tab) chrome.tabs.update(tab.id, {active: true});
-        else chrome.tabs.create({url: url});
+        if (tab) browser.tabs.update(tab.id, {active: true});
+        else browser.tabs.create({url: url});
       });
     },
     onFindScripts: function (item) {
@@ -87,12 +90,12 @@ module.exports = {
         var matches = this.store.currentTab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
         domain = matches[1];
       }
-      chrome.tabs.create({
+      browser.tabs.create({
         url: 'https://greasyfork.org/scripts/search?q=' + encodeURIComponent(domain),
       });
     },
     onCommand: function (item) {
-      chrome.tabs.sendMessage(this.store.currentTab.id, {
+      browser.tabs.sendMessage(this.store.currentTab.id, {
         cmd: 'Command',
         data: item.name,
       });
@@ -108,7 +111,7 @@ module.exports = {
       })
       .then(function () {
         item.data.enabled = !item.data.enabled;
-        _.options.get('autoReload') && chrome.tabs.reload(_this.store.currentTab.id);
+        _.options.get('autoReload') && browser.tabs.reload(_this.store.currentTab.id);
       });
     },
   },

+ 98 - 0
src/public/mylib/browser.js

@@ -0,0 +1,98 @@
+/* global chrome */
+!function (win) {
+  function wrapAsync(func) {
+    return function () {
+      var args = [];
+      for (var i = 0; i < arguments.length; i ++) args.push(arguments[i]);
+      return new Promise(function (resolve, reject) {
+        args.push(function (res) {
+          var err = chrome.runtime.lastError;
+          if (err) {
+            console.error(args);
+            reject(err);
+          } else {
+            resolve(res);
+          }
+        });
+        func.apply(null, args);
+      });
+    };
+  }
+  function wrapAPIs(source, meta) {
+    var target = {};
+    Object.keys(source).forEach(function (key) {
+      var metaVal = meta && meta[key];
+      if (metaVal) {
+        var value = source[key];
+        if (typeof metaVal === 'function') {
+          target[key] = metaVal(value);
+        } else if (typeof metaVal === 'object' && typeof value === 'object') {
+          target[key] = wrapAPIs(value, metaVal);
+        } else {
+          target[key] = value;
+        }
+      }
+    });
+    return target;
+  }
+  var meta = {
+    browserAction: true,
+    i18n: true,
+    notifications: {
+      onClicked: true,
+      onClosed: true,
+      create: wrapAsync,
+    },
+    runtime: {
+      getManifest: true,
+      getURL: true,
+      onMessage: function (onMessage) {
+        function wrapListener(listener) {
+          return function onMessage(message, sender, sendResponse) {
+            var result = listener(message, sender);
+            if (result && typeof result.then === 'function') {
+              result.then(function (data) {
+                sendResponse({data: data});
+              }, function (err) {
+                console.error(err);
+                sendResponse({error: err});
+              });
+              return true;
+            } else {
+              sendResponse({data: result});
+            }
+          };
+        }
+        return {
+          addListener: function (listener) {
+            return onMessage.addListener(wrapListener(listener));
+          },
+        };
+      },
+      sendMessage: function (sendMessage) {
+        var promisifiedSendMessage = wrapAsync(sendMessage);
+        return function (data) {
+          return promisifiedSendMessage(data)
+          .then(function (res) {
+            if (res && res.error) throw res.error;
+            return res && res.data;
+          });
+        };
+      },
+    },
+    tabs: {
+      onUpdated: true,
+      create: wrapAsync,
+      get: wrapAsync,
+      query: wrapAsync,
+      reload: wrapAsync,
+      remove: wrapAsync,
+      sendMessage: wrapAsync,
+      update: wrapAsync,
+    },
+    webRequest: true,
+  };
+  if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
+    win.browser = wrapAPIs(chrome, meta);
+  }
+}(this);