소스 검색

refactor: add injected-web entry

Gerald 8 년 전
부모
커밋
f2fe0a15ac

+ 3 - 2
package.json

@@ -3,7 +3,7 @@
   "version": "2.6.4",
   "scripts": {
     "dev": "gulp watch",
-    "prebuild": "gulp clean",
+    "prebuild": "npm run lint && gulp clean",
     "build": "gulp build",
     "analyze": "webpack --profile --json --config scripts/webpack.conf.js | webpack-bundle-size-analyzer",
     "analyze:json": "webpack --profile --json --config scripts/webpack.conf.js > stats.json",
@@ -51,7 +51,8 @@
     "vue-style-loader": "^3.0.1",
     "vue-template-compiler": "^2.3.4",
     "webpack": "^2.6.1",
-    "webpack-bundle-size-analyzer": "^2.7.0"
+    "webpack-bundle-size-analyzer": "^2.7.0",
+    "wrapper-webpack-plugin": "^0.1.11"
   },
   "author": "Gerald <[email protected]>",
   "repository": {

+ 3 - 1
scripts/webpack.base.conf.js

@@ -1,5 +1,6 @@
 const path = require('path');
 const webpack = require('webpack');
+const BabiliWebpackPlugin = require('babili-webpack-plugin');
 const vueLoaderConfig = require('./vue-loader.conf');
 const { IS_DEV, styleRule } = require('./utils');
 const DIST = 'dist';
@@ -65,5 +66,6 @@ module.exports = {
   devtool: IS_DEV ? '#inline-source-map' : false,
   plugins: [
     definePlugin,
-  ],
+    !IS_DEV && new BabiliWebpackPlugin(),
+  ].filter(Boolean),
 };

+ 29 - 10
scripts/webpack.conf.js

@@ -2,7 +2,7 @@ const webpack = require('webpack');
 const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
 const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
-const BabiliWebpackPlugin = require('babili-webpack-plugin');
+const WrapperWebpackPlugin = require('wrapper-webpack-plugin');
 const base = require('./webpack.base.conf');
 const { IS_DEV, merge } = require('./utils');
 
@@ -13,7 +13,10 @@ const entry = {
   injected: 'src/injected/index.js',
 };
 
-module.exports = merge(base, {
+const targets = [];
+module.exports = targets;
+
+targets.push(merge(base, {
   entry,
   plugins: [
     new webpack.optimize.CommonsChunkPlugin({
@@ -47,14 +50,30 @@ module.exports = merge(base, {
       chunksSortMode: 'dependency'
     }),
     // new FriendlyErrorsPlugin(),
-    ... IS_DEV ? [
-    ] : [
-      // extract css into its own file
-      new ExtractTextPlugin('[name].css'),
-      new BabiliWebpackPlugin(),
-    ],
-  ],
+    !IS_DEV && new ExtractTextPlugin('[name].css'),
+  ].filter(Boolean),
   externals: {
     localStorage: 'localStorage',
   },
-});
+}));
+
+targets.push(merge(base, {
+  entry: {
+    'injected-web': 'src/injected/web',
+  },
+  output: {
+    libraryTarget: 'commonjs2',
+  },
+  plugins: [
+    new WrapperWebpackPlugin({
+      header: `\
+var VM_initializeWeb = function () {
+  var module = { exports: {} };
+`,
+      footer: `
+  var exports = module.exports;
+  return exports.__esModule ? exports['default'] : exports;
+};`,
+    }),
+  ],
+}));

+ 0 - 108
src/injected/bridge.js

@@ -1,108 +0,0 @@
-/**
- * All functions to be injected into web page must be independent.
- * They must be assigned to `bridge` so that they can be serialized.
- */
-import { getUniqId, postData, noop } from './utils';
-
-function post(data) {
-  const bridge = this;
-  bridge.postData(bridge.destId, data);
-}
-
-function prepare(src, dest) {
-  const bridge = this;
-  const { helpers } = bridge;
-  const arrayProto = Array.prototype;
-  const bindThis = func => (thisObj, ...args) => func.apply(thisObj, args);
-  helpers.forEach = bindThis(arrayProto.forEach);
-  helpers.map = bindThis(arrayProto.map);
-  helpers.indexOf = bindThis(arrayProto.indexOf);
-  helpers.includes = arrayProto.includes
-  ? bindThis(arrayProto.includes)
-  : (arr, item) => helpers.indexOf(arr, item) >= 0;
-  helpers.toString = bindThis(Object.prototype.toString);
-
-  const { vmid } = bridge;
-  const srcId = vmid + src;
-  bridge.destId = vmid + dest;
-  document.addEventListener(srcId, e => {
-    const data = JSON.parse(e.detail);
-    bridge.handle(data);
-  }, false);
-}
-
-function encodeBody(body) {
-  const helpers = this;
-  const cls = helpers.getType(body);
-  let result;
-  if (cls === 'formdata') {
-    // FormData#keys is supported in Chrome >= 50
-    if (!body.keys) return {};
-    const promises = [];
-    const iterator = body.keys();
-    while (1) { // eslint-disable-line no-constant-condition
-      const item = iterator.next();
-      if (item.done) break;
-      const key = item.value;
-      const promise = Promise.all(body.getAll(key).map(value => helpers.encodeBody(value)))
-      .then(values => ({ key, values }));
-      promises.push(promise);
-    }
-    result = Promise.all(promises)
-    .then(items => items.reduce((res, item) => {
-      res[item.key] = item.values;
-      return res;
-    }, {}))
-    .then(value => ({ cls, value }));
-  } else if (helpers.includes(['blob', 'file'], cls)) {
-    const bufsize = 8192;
-    result = new Promise(resolve => {
-      const reader = new FileReader();
-      reader.onload = () => {
-        let value = '';
-        const array = new Uint8Array(reader.result);
-        for (let i = 0; i < array.length; i += bufsize) {
-          value += String.fromCharCode.apply(null, array.subarray(i, i + bufsize));
-        }
-        resolve({
-          cls,
-          value,
-          type: body.type,
-          name: body.name,
-          lastModified: body.lastModified,
-        });
-      };
-      reader.readAsArrayBuffer(body);
-    });
-  } else if (body) {
-    result = {
-      cls,
-      value: JSON.stringify(body),
-    };
-  }
-  return Promise.resolve(result);
-}
-
-function initialize(src, dest) {
-  this.prepare(src, dest);
-}
-
-export default {
-  postData,
-  post,
-  getUniqId,
-  prepare,
-  initialize,
-  helpers: {
-    noop,
-    encodeBody,
-    getType(obj) {
-      const helpers = this;
-      const type = typeof obj;
-      if (type !== 'object') return type;
-      const typeString = helpers.toString(obj); // [object TYPENAME]
-      return typeString.slice(8, -1).toLowerCase();
-    },
-  },
-  vmid: `VM_${getUniqId()}`,
-};

+ 0 - 86
src/injected/content.js

@@ -1,86 +0,0 @@
-import base from './bridge';
-import { getRequestId, httpRequest, abortRequest } from './requests';
-import { inject, postData, sendMessage } from './utils';
-import { onNotificationCreate } from './notification';
-import { tabOpen, tabClose } from './tabs';
-
-const ids = [];
-const menus = [];
-
-const bridge = Object.assign({}, base, {
-  getPopup,
-  ids,
-  menus,
-  handle: handleContent,
-});
-
-export default bridge;
-
-function getPopup() {
-  // XXX: only scripts run in top level window are counted
-  if (top === window) {
-    sendMessage({
-      cmd: 'SetPopup',
-      data: { ids, menus },
-    });
-  }
-}
-
-function injectScript(data) {
-  const [id, wrapperKeys, code] = data;
-  const func = (scriptId, cb, post, destId) => {
-    Object.defineProperty(window, `VM_${scriptId}`, {
-      value: cb,
-      configurable: true,
-    });
-    post(destId, { cmd: 'Injected', data: scriptId });
-  };
-  const args = [
-    JSON.stringify(id),
-    `function(${wrapperKeys.join(',')}){${code}}`,
-    postData.toString(),
-    JSON.stringify(bridge.destId),
-  ];
-  inject(`!${func.toString()}(${args.join(',')})`);
-}
-
-function handleContent(req) {
-  if (!req) {
-    console.error('[Violentmonkey] Invalid data! There might be unsupported data format.');
-    return;
-  }
-  const handlers = {
-    GetRequestId: getRequestId,
-    HttpRequest: httpRequest,
-    AbortRequest: abortRequest,
-    Inject: injectScript,
-    TabOpen: tabOpen,
-    TabClose: tabClose,
-    SetValue(data) {
-      sendMessage({ cmd: 'SetValue', data });
-    },
-    RegisterMenu(data) {
-      if (window.top === window) menus.push(data);
-      getPopup();
-    },
-    AddStyle(css) {
-      if (document.head) {
-        const style = document.createElement('style');
-        style.textContent = css;
-        document.head.appendChild(style);
-      }
-    },
-    Notification: onNotificationCreate,
-    SetClipboard(data) {
-      sendMessage({ cmd: 'SetClipboard', data });
-    },
-    CheckScript({ name, namespace, callback }) {
-      sendMessage({ cmd: 'CheckScript', data: { name, namespace } })
-      .then(result => {
-        bridge.post({ cmd: 'ScriptChecked', data: { callback, result } });
-      });
-    },
-  };
-  const handle = handlers[req.cmd];
-  if (handle) handle(req.data);
-}

+ 1 - 0
src/injected/content/bridge.js

@@ -0,0 +1 @@
+export default {};

+ 131 - 0
src/injected/content/index.js

@@ -0,0 +1,131 @@
+import { bindEvents, sendMessage, postData, inject } from '../utils';
+import bridge from './bridge';
+import { tabOpen, tabClose, tabClosed } from './tabs';
+import { onNotificationCreate, onNotificationClick, onNotificationClose } from './notifications';
+import { getRequestId, httpRequest, abortRequest, httpRequested } from './requests';
+
+const ids = [];
+const menus = [];
+
+const badge = {
+  number: 0,
+  ready: false,
+  willSet: false,
+};
+
+function getBadge() {
+  badge.willSet = true;
+  setBadge();
+}
+
+function setBadge() {
+  if (badge.ready && badge.willSet) {
+    // XXX: only scripts run in top level window are counted
+    if (top === window) sendMessage({ cmd: 'SetBadge', data: badge.number });
+  }
+}
+
+const bgHandlers = {
+  Command(data) {
+    bridge.post({ cmd: 'Command', data });
+  },
+  GetPopup: getPopup,
+  GetBadge: getBadge,
+  HttpRequested: httpRequested,
+  TabClosed: tabClosed,
+  UpdateValues(data) {
+    bridge.post({ cmd: 'UpdateValues', data });
+  },
+  NotificationClick: onNotificationClick,
+  NotificationClose: onNotificationClose,
+};
+
+export default function initialize(contentId, webId) {
+  bridge.post = bindEvents(contentId, webId, onHandle);
+  bridge.destId = webId;
+
+  browser.runtime.onMessage.addListener((req, src) => {
+    const handle = bgHandlers[req.cmd];
+    if (handle) handle(req.data, src);
+  });
+
+  sendMessage({ cmd: 'GetInjected', data: location.href })
+  .then(data => {
+    if (data.scripts) {
+      data.scripts.forEach(script => {
+        ids.push(script.id);
+        if (script.enabled) badge.number += 1;
+      });
+    }
+    bridge.post({ cmd: 'LoadScripts', data });
+    badge.ready = true;
+    getPopup();
+    setBadge();
+  });
+}
+
+const handlers = {
+  GetRequestId: getRequestId,
+  HttpRequest: httpRequest,
+  AbortRequest: abortRequest,
+  Inject: injectScript,
+  TabOpen: tabOpen,
+  TabClose: tabClose,
+  SetValue(data) {
+    sendMessage({ cmd: 'SetValue', data });
+  },
+  RegisterMenu(data) {
+    if (window.top === window) menus.push(data);
+    getPopup();
+  },
+  AddStyle(css) {
+    if (document.head) {
+      const style = document.createElement('style');
+      style.textContent = css;
+      document.head.appendChild(style);
+    }
+  },
+  Notification: onNotificationCreate,
+  SetClipboard(data) {
+    sendMessage({ cmd: 'SetClipboard', data });
+  },
+  CheckScript({ name, namespace, callback }) {
+    sendMessage({ cmd: 'CheckScript', data: { name, namespace } })
+    .then(result => {
+      bridge.post({ cmd: 'ScriptChecked', data: { callback, result } });
+    });
+  },
+};
+
+function onHandle(req) {
+  const handle = handlers[req.cmd];
+  if (handle) handle(req.data);
+}
+
+function getPopup() {
+  // XXX: only scripts run in top level window are counted
+  if (top === window) {
+    sendMessage({
+      cmd: 'SetPopup',
+      data: { ids, menus },
+    });
+  }
+}
+
+function injectScript(data) {
+  const [id, wrapperKeys, code] = data;
+  const func = (scriptId, cb, post, destId) => {
+    Object.defineProperty(window, `VM_${scriptId}`, {
+      value: cb,
+      configurable: true,
+    });
+    post(destId, { cmd: 'Injected', data: scriptId });
+  };
+  const args = [
+    JSON.stringify(id),
+    `function(${wrapperKeys.join(',')}){${code}}`,
+    postData.toString(),
+    JSON.stringify(bridge.destId),
+  ];
+  inject(`!${func.toString()}(${args.join(',')})`);
+}

+ 4 - 2
src/injected/notification.js → src/injected/content/notifications.js

@@ -1,5 +1,5 @@
-import { sendMessage } from './utils';
-import bridge from './content';
+import { sendMessage } from '../utils';
+import bridge from './bridge';
 
 const notifications = {};
 
@@ -7,10 +7,12 @@ export function onNotificationCreate(options) {
   sendMessage({ cmd: 'Notification', data: options })
   .then(nid => { notifications[nid] = options.id; });
 }
+
 export function onNotificationClick(nid) {
   const id = notifications[nid];
   if (id) bridge.post({ cmd: 'NotificationClicked', data: id });
 }
+
 export function onNotificationClose(nid) {
   const id = notifications[nid];
   if (id) {

+ 2 - 2
src/injected/requests.js → src/injected/content/requests.js

@@ -1,5 +1,5 @@
-import { sendMessage } from './utils';
-import bridge from './content';
+import { sendMessage } from '../utils';
+import bridge from './bridge';
 
 const requests = {};
 

+ 12 - 0
src/injected/content/tabs.js

@@ -0,0 +1,12 @@
+import { sendMessage } from '../utils';
+import bridge from './bridge';
+
+export function tabOpen(data) {
+  sendMessage({ cmd: 'TabOpen', data });
+}
+export function tabClose(id) {
+  sendMessage({ cmd: 'TabClose', data: { id } });
+}
+export function tabClosed(id) {
+  bridge.post({ cmd: 'TabClosed', data: id });
+}

+ 4 - 66
src/injected/index.js

@@ -1,84 +1,22 @@
 import 'src/common/browser';
-import { inject, encodeObject, getUniqId, sendMessage } from './utils';
-import { onNotificationClick, onNotificationClose } from './notification';
-import { httpRequested } from './requests';
-import { tabClosed } from './tabs';
-import bridge from './content';
-import webBridgeObj from './web';
+import { inject, getUniqId, sendMessage } from './utils';
+import initialize from './content';
 
 (function main() {
   // Avoid running repeatedly due to new `document.documentElement`
   if (window.VM) return;
   window.VM = 1;
 
-  const badge = {
-    number: 0,
-    ready: false,
-    willSet: false,
-  };
-  function getBadge() {
-    badge.willSet = true;
-    setBadge();
-  }
-  function setBadge() {
-    if (badge.ready && badge.willSet) {
-      // XXX: only scripts run in top level window are counted
-      if (top === window) sendMessage({ cmd: 'SetBadge', data: badge.number });
-    }
-  }
-
-  // Messages
-  browser.runtime.onMessage.addListener((req, src) => {
-    const handlers = {
-      Command(data) {
-        bridge.post({ cmd: 'Command', data });
-      },
-      GetPopup: bridge.getPopup,
-      GetBadge: getBadge,
-      HttpRequested: httpRequested,
-      TabClosed: tabClosed,
-      UpdateValues(data) {
-        bridge.post({ cmd: 'UpdateValues', data });
-      },
-      NotificationClick: onNotificationClick,
-      NotificationClose: onNotificationClose,
-    };
-    const handle = handlers[req.cmd];
-    if (handle) handle(req.data, src);
-  });
-
-  function initWeb(webBridge, webId, contentId, props) {
-    webBridge.initialize(webId, contentId, props);
-    document.addEventListener('DOMContentLoaded', () => {
-      webBridge.state = 1;
-      webBridge.load();
-    }, false);
-    webBridge.checkLoad();
-  }
   function initBridge() {
     const contentId = getUniqId();
     const webId = getUniqId();
     const args = [
-      encodeObject(webBridgeObj),
       JSON.stringify(webId),
       JSON.stringify(contentId),
       JSON.stringify(Object.getOwnPropertyNames(window)),
     ];
-    inject(`(${initWeb.toString()})(${args.join(',')})`);
-    bridge.initialize(contentId, webId);
-    sendMessage({ cmd: 'GetInjected', data: location.href })
-    .then(data => {
-      if (data.scripts) {
-        data.scripts.forEach(script => {
-          bridge.ids.push(script.id);
-          if (script.enabled) badge.number += 1;
-        });
-      }
-      bridge.post({ cmd: 'LoadScripts', data });
-      badge.ready = true;
-      bridge.getPopup();
-      setBadge();
-    });
+    inject(`(${window.VM_initializeWeb.toString()}())(${args.join(',')})`);
+    initialize(contentId, webId);
   }
   initBridge();
 

+ 0 - 31
src/injected/tabs.js

@@ -1,31 +0,0 @@
-import { sendMessage } from './utils';
-import bridge from './content';
-
-const tabs = {};
-const tabIdMap = {};
-
-export function tabOpen({ data, key }) {
-  sendMessage({ cmd: 'TabOpen', data })
-  .then(({ id }) => {
-    tabs[key] = { id };
-    tabIdMap[id] = key;
-  });
-}
-export function tabClose(key) {
-  let data;
-  if (key) {
-    const item = tabs[key];
-    data = item && { id: item.id };
-  } else {
-    data = {};
-  }
-  if (data) sendMessage({ cmd: 'TabClose', data });
-}
-export function tabClosed(id) {
-  const key = tabIdMap[id];
-  if (key) {
-    delete tabIdMap[id];
-    delete tabs[key];
-    bridge.post({ cmd: 'TabClosed', data: key });
-  }
-}

+ 29 - 21
src/injected/utils.js

@@ -1,4 +1,4 @@
-export { sendMessage, noop } from 'src/common';
+export { sendMessage, noop, request } from 'src/common';
 
 export function postData(destId, data) {
   // Firefox issue: data must be stringified to avoid cross-origin problem
@@ -18,27 +18,35 @@ export function inject(code) {
   }
 }
 
-export function encodeObject(obj) {
-  if (Array.isArray(obj)) {
-    return obj.map(encodeObject).join(',');
-  }
-  if (typeof obj === 'function') {
-    let str = obj.toString();
-    const prefix = str.slice(0, str.indexOf('{'));
-    if (prefix.indexOf('=>') < 0 && prefix.indexOf('function ') < 0) {
-      // method definition
-      str = `function ${str}`;
-    }
-    return str;
-  }
-  if (obj && typeof obj === 'object') {
-    const pairs = Object.keys(obj)
-    .map(key => `${JSON.stringify(key)}:${encodeObject(obj[key])}`);
-    return `{${pairs.join(',')}}`;
-  }
-  return JSON.stringify(obj);
-}
+// export function encodeObject(obj) {
+//   if (Array.isArray(obj)) {
+//     return obj.map(encodeObject).join(',');
+//   }
+//   if (typeof obj === 'function') {
+//     let str = obj.toString();
+//     const prefix = str.slice(0, str.indexOf('{'));
+//     if (prefix.indexOf('=>') < 0 && prefix.indexOf('function ') < 0) {
+//       // method definition
+//       str = `function ${str}`;
+//     }
+//     return str;
+//   }
+//   if (obj && typeof obj === 'object') {
+//     const pairs = Object.keys(obj)
+//     .map(key => `${JSON.stringify(key)}:${encodeObject(obj[key])}`);
+//     return `{${pairs.join(',')}}`;
+//   }
+//   return JSON.stringify(obj);
+// }
 
 export function getUniqId() {
   return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
 }
+
+export function bindEvents(srcId, destId, handle) {
+  document.addEventListener(srcId, e => {
+    const data = JSON.parse(e.detail);
+    handle(data);
+  }, false);
+  return data => { postData(destId, data); };
+}

+ 8 - 0
src/injected/web/bridge.js

@@ -0,0 +1,8 @@
+import { noop } from './helpers';
+import { getUniqId } from '../utils';
+
+export default {
+  id: `VM_${getUniqId()}`,
+  load: noop,
+  checkLoad: noop,
+};

+ 116 - 0
src/injected/web/helpers.js

@@ -0,0 +1,116 @@
+const arrayProto = Array.prototype;
+
+const bindThis = func => (thisObj, ...args) => func.apply(thisObj, args);
+
+export const forEach = bindThis(arrayProto.forEach);
+
+export const map = bindThis(arrayProto.map);
+
+export const indexOf = bindThis(arrayProto.indexOf);
+
+export const includes = arrayProto.includes
+  ? bindThis(arrayProto.includes)
+  : (arr, item) => indexOf(arr, item) >= 0;
+
+export const toString = bindThis(Object.prototype.toString);
+
+export const assign = Object.assign || ((obj, ...args) => {
+  forEach(args, arg => {
+    if (arg) {
+      forEach(Object.keys(arg), key => {
+        obj[key] = arg[key];
+      });
+    }
+  });
+  return obj;
+});
+
+export function encodeBody(body) {
+  const cls = getType(body);
+  let result;
+  if (cls === 'formdata') {
+    // FormData#keys is supported in Chrome >= 50
+    if (!body.keys) return {};
+    const promises = [];
+    const iterator = body.keys();
+    while (1) { // eslint-disable-line no-constant-condition
+      const item = iterator.next();
+      if (item.done) break;
+      const key = item.value;
+      const promise = Promise.all(body.getAll(key).map(value => encodeBody(value)))
+      .then(values => ({ key, values }));
+      promises.push(promise);
+    }
+    result = Promise.all(promises)
+    .then(items => items.reduce((res, item) => {
+      res[item.key] = item.values;
+      return res;
+    }, {}))
+    .then(value => ({ cls, value }));
+  } else if (includes(['blob', 'file'], cls)) {
+    const bufsize = 8192;
+    result = new Promise(resolve => {
+      const reader = new FileReader();
+      reader.onload = () => {
+        let value = '';
+        const array = new Uint8Array(reader.result);
+        for (let i = 0; i < array.length; i += bufsize) {
+          value += String.fromCharCode.apply(null, array.subarray(i, i + bufsize));
+        }
+        resolve({
+          cls,
+          value,
+          type: body.type,
+          name: body.name,
+          lastModified: body.lastModified,
+        });
+      };
+      reader.readAsArrayBuffer(body);
+    });
+  } else if (body) {
+    result = {
+      cls,
+      value: JSON.stringify(body),
+    };
+  }
+  return Promise.resolve(result);
+}
+
+function getType(obj) {
+  const type = typeof obj;
+  if (type !== 'object') return type;
+  const typeString = toString(obj); // [object TYPENAME]
+  return typeString.slice(8, -1).toLowerCase();
+}
+
+export function noop() {}
+
+/**
+ * http://www.webtoolkit.info/javascript-utf8.html
+ */
+export function utf8decode(utftext) {
+  /* eslint-disable no-bitwise */
+  let string = '';
+  let i = 0;
+  let c1 = 0;
+  let c2 = 0;
+  let c3 = 0;
+  while (i < utftext.length) {
+    c1 = utftext.charCodeAt(i);
+    if (c1 < 128) {
+      string += String.fromCharCode(c1);
+      i += 1;
+    } else if (c1 > 191 && c1 < 224) {
+      c2 = utftext.charCodeAt(i + 1);
+      string += String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
+      i += 2;
+    } else {
+      c2 = utftext.charCodeAt(i + 1);
+      c3 = utftext.charCodeAt(i + 2);
+      string += String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+      i += 3;
+    }
+  }
+  return string;
+  /* eslint-enable no-bitwise */
+}

+ 140 - 350
src/injected/web.js → src/injected/web/index.js

@@ -1,130 +1,95 @@
-/**
- * All functions to be injected into web page must be independent.
- * They must be assigned to `bridge` so that they can be serialized.
- */
-import base from './bridge';
+import { getUniqId, bindEvents } from '../utils';
+import { includes, forEach, map, utf8decode } from './helpers';
+import bridge from './bridge';
+import { onRequestCreate, onRequestStart, onRequestCallback } from './requests';
+import {
+  onNotificationCreate,
+  onNotificationClicked,
+  onNotificationClosed,
+} from './notifications';
+import { onTabCreate, onTabClosed } from './tabs';
 
-export default Object.assign({}, base, {
-  utf8decode,
-  getRequest,
-  getTab,
-  wrapGM,
-  getWrapper,
-  onLoadScripts,
-  exposeVM,
-  runCode,
-  initialize,
-  state: 0,
-  handle: handleWeb,
-});
+let state = 0;
 
-/**
- * http://www.webtoolkit.info/javascript-utf8.html
- */
-function utf8decode(utftext) {
-  /* eslint-disable no-bitwise */
-  let string = '';
-  let i = 0;
-  let c1 = 0;
-  let c2 = 0;
-  let c3 = 0;
-  while (i < utftext.length) {
-    c1 = utftext.charCodeAt(i);
-    if (c1 < 128) {
-      string += String.fromCharCode(c1);
-      i += 1;
-    } else if (c1 > 191 && c1 < 224) {
-      c2 = utftext.charCodeAt(i + 1);
-      string += String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
-      i += 2;
-    } else {
-      c2 = utftext.charCodeAt(i + 1);
-      c3 = utftext.charCodeAt(i + 2);
-      string += String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
-      i += 3;
-    }
-  }
-  return string;
-  /* eslint-enable no-bitwise */
+export default function initialize(webId, contentId, props) {
+  bridge.props = props;
+  bridge.post = bindEvents(webId, contentId, onHandle);
+  document.addEventListener('DOMContentLoaded', () => {
+    state = 1;
+    bridge.load();
+  }, false);
+  bridge.checkLoad();
 }
 
-function exposeVM() {
-  const bridge = this;
-  const Violentmonkey = {};
-  const checking = {};
-  let key = 0;
-  bridge.onScriptChecked = ({ callback, result }) => {
-    const cb = checking[callback];
-    if (cb) {
-      cb(result);
-      delete checking[callback];
-    }
-  };
-  Object.defineProperty(Violentmonkey, 'getVersion', {
-    value() {
-      return Promise.resolve({
-        version: bridge.version,
-      });
-    },
-  });
-  Object.defineProperty(Violentmonkey, 'isInstalled', {
-    value(name, namespace) {
-      return new Promise(resolve => {
-        key += 1;
-        const callback = checking[key];
-        checking[callback] = resolve;
-        bridge.post({
-          cmd: 'CheckScript',
-          data: {
-            name,
-            namespace,
-            callback,
-          },
-        });
-      });
-    },
-  });
-  Object.defineProperty(window.external, 'Violentmonkey', {
-    value: Violentmonkey,
-  });
+const commands = {};
+const ainject = {};
+const values = {};
+
+const handlers = {
+  LoadScripts: onLoadScripts,
+  Command(data) {
+    const func = commands[data];
+    if (func) func();
+  },
+  GotRequestId: onRequestStart,
+  HttpRequested: onRequestCallback,
+  TabClosed: onTabClosed,
+  UpdateValues(data) {
+    if (values[data.uri]) values[data.uri] = data.values;
+  },
+  NotificationClicked: onNotificationClicked,
+  NotificationClosed: onNotificationClosed,
+  // advanced inject
+  Injected(id) {
+    const item = ainject[id];
+    const func = window[`VM_${id}`];
+    delete window[`VM_${id}`];
+    delete ainject[id];
+    if (item && func) runCode(item[0], func, item[1], item[2]);
+  },
+  ScriptChecked(data) {
+    if (bridge.onScriptChecked) bridge.onScriptChecked(data);
+  },
+};
+
+function onHandle(obj) {
+  const handle = handlers[obj.cmd];
+  if (handle) handle(obj.data);
 }
 
 function onLoadScripts(data) {
-  const bridge = this;
   const start = [];
   const idle = [];
   const end = [];
-  bridge.command = {};
-  bridge.notif = {};
-  bridge.ainject = {};
   bridge.version = data.version;
-  const { helpers } = bridge;
-  if (helpers.includes([
+  if (includes([
     'greasyfork.org',
   ], location.host)) {
-    bridge.exposeVM();
+    exposeVM();
   }
-  bridge.values = {};
   // reset load and checkLoad
   bridge.load = () => {
     run(end);
     setTimeout(run, 0, idle);
   };
   bridge.checkLoad = () => {
-    if (!bridge.state && helpers.includes(['interactive', 'complete'], document.readyState)) bridge.state = 1;
-    if (bridge.state) bridge.load();
+    if (!state && includes(['interactive', 'complete'], document.readyState)) {
+      state = 1;
+    }
+    if (state) bridge.load();
   };
   const listMap = {
     'document-start': start,
     'document-idle': idle,
     'document-end': end,
   };
-  helpers.forEach(data.scripts, script => {
-    bridge.values[script.uri] = data.values[script.uri] || {};
+  forEach(data.scripts, script => {
+    values[script.uri] = data.values[script.uri] || {};
     if (script && script.enabled) {
       const list = listMap[
-        script.custom.runAt || script.custom['run-at']
-        || script.meta.runAt || script.meta['run-at']
+        // XXX: use camelCase since v2.6.3
+        script.custom.runAt || script.custom['run-at'] ||
+        script.meta.runAt || script.meta['run-at']
       ] || end;
       list.push(script);
     }
@@ -133,12 +98,12 @@ function onLoadScripts(data) {
   bridge.checkLoad();
   function buildCode(script) {
     const requireKeys = script.meta.require || [];
-    const wrapper = bridge.wrapGM(script, data.cache);
+    const wrapper = wrapGM(script, data.cache);
     // Must use Object.getOwnPropertyNames to list unenumerable properties
     const wrapperKeys = Object.getOwnPropertyNames(wrapper);
-    const wrapperInit = helpers.map(wrapperKeys, name => `this["${name}"]=${name}`).join(';');
+    const wrapperInit = map(wrapperKeys, name => `this["${name}"]=${name}`).join(';');
     const codeSlices = [`${wrapperInit};with(this)!function(){`];
-    helpers.forEach(requireKeys, key => {
+    forEach(requireKeys, key => {
       const requireCode = data.require[key];
       if (requireCode) {
         codeSlices.push(requireCode);
@@ -151,10 +116,10 @@ function onLoadScripts(data) {
     codeSlices.push('}.call(this);');
     const code = codeSlices.join('\n');
     const name = script.custom.name || script.meta.name || script.id;
-    const args = helpers.map(wrapperKeys, key => wrapper[key]);
+    const args = map(wrapperKeys, key => wrapper[key]);
     const thisObj = wrapper.window || wrapper;
-    const id = bridge.getUniqId();
-    bridge.ainject[id] = [name, args, thisObj];
+    const id = getUniqId();
+    ainject[id] = [name, args, thisObj];
     bridge.post({ cmd: 'Inject', data: [id, wrapperKeys, code] });
   }
   function run(list) {
@@ -162,207 +127,9 @@ function onLoadScripts(data) {
   }
 }
 
-function handleWeb(obj) {
-  const bridge = this;
-  const handlers = {
-    LoadScripts(data) {
-      bridge.onLoadScripts(data);
-    },
-    Command(data) {
-      const func = bridge.command[data];
-      if (func) func();
-    },
-    GotRequestId(id) {
-      const req = bridge.requests.queue.shift();
-      req.start(req, id);
-    },
-    HttpRequested(res) {
-      const req = bridge.requests.map[res.id];
-      if (req) req.callback(req, res);
-    },
-    TabClosed(key) {
-      const item = bridge.tabs[key];
-      if (item) {
-        item.closed = true;
-        const { onclose } = item;
-        if (onclose) onclose();
-        delete bridge.tabs[key];
-      }
-    },
-    UpdateValues(data) {
-      const { values } = bridge;
-      if (values && values[data.uri]) values[data.uri] = data.values;
-    },
-    NotificationClicked(id) {
-      const options = bridge.notif[id];
-      if (options) {
-        const { onclick } = options;
-        if (onclick) onclick();
-      }
-    },
-    NotificationClosed(id) {
-      const options = bridge.notif[id];
-      if (options) {
-        delete bridge.notif[id];
-        const { ondone } = options;
-        if (ondone) ondone();
-      }
-    },
-    // advanced inject
-    Injected(id) {
-      const item = bridge.ainject[id];
-      const func = window[`VM_${id}`];
-      delete window[`VM_${id}`];
-      delete bridge.ainject[id];
-      if (item && func) bridge.runCode(item[0], func, item[1], item[2]);
-    },
-    ScriptChecked(data) {
-      if (bridge.onScriptChecked) bridge.onScriptChecked(data);
-    },
-  };
-  const handle = handlers[obj.cmd];
-  if (handle) handle(obj.data);
-}
-
-function runCode(name, func, args, thisObj) {
-  if (process.env.DEBUG) {
-    console.log(`Run script: ${name}`); // eslint-disable-line no-console
-  }
-  try {
-    func.apply(thisObj, args);
-  } catch (e) {
-    let msg = `Error running script: ${name}\n${e}`;
-    if (e.message) msg = `${msg}\n${e.message}`;
-    console.error(msg);
-  }
-}
-
-function getRequest(arg) {
-  const bridge = this;
-  const { helpers } = bridge;
-  init();
-  return bridge.getRequest(arg);
-  function init() {
-    bridge.requests = {
-      map: {},
-      queue: [],
-    };
-    bridge.getRequest = details => {
-      const req = {
-        details,
-        callback,
-        start,
-        req: {
-          abort: reqAbort,
-        },
-      };
-      details.url = getFullUrl(details.url);
-      bridge.requests.queue.push(req);
-      bridge.post({ cmd: 'GetRequestId' });
-      return req.req;
-    };
-  }
-  function reqAbort() {
-    bridge.post({ cmd: 'AbortRequest', data: this.id });
-  }
-  function parseData(req, details) {
-    if (req.resType) {
-      // blob or arraybuffer
-      let data = req.data.response.split(',');
-      const mimetype = data[0].match(/^data:(.*?);base64$/);
-      if (!mimetype) {
-        // invalid
-        req.data.response = null;
-      } else {
-        data = window.atob(data[1]);
-        const arr = new window.Uint8Array(data.length);
-        for (let i = 0; i < data.length; i += 1) arr[i] = data.charCodeAt(i);
-        if (details.responseType === 'blob') {
-          // blob
-          return new Blob([arr], { type: mimetype });
-        }
-        // arraybuffer
-        return arr.buffer;
-      }
-    } else if (details.responseType === 'json') {
-      // json
-      return JSON.parse(req.data.response);
-    } else {
-      // text
-      return req.data.response;
-    }
-  }
-  // request object functions
-  function callback(req, res) {
-    const cb = req.details[`on${res.type}`];
-    if (cb) {
-      if (res.data.response) {
-        if (!req.data) req.data = [parseData(res, req.details)];
-        res.data.response = req.data[0];
-      }
-      res.data.context = req.details.context;
-      cb(res.data);
-    }
-    if (res.type === 'loadend') delete bridge.requests.map[req.id];
-  }
-  function start(req, id) {
-    const { details } = req;
-    const payload = {
-      id,
-      method: details.method,
-      url: details.url,
-      user: details.user,
-      password: details.password,
-      headers: details.headers,
-      overrideMimeType: details.overrideMimeType,
-    };
-    req.id = id;
-    bridge.requests.map[id] = req;
-    if (helpers.includes(['arraybuffer', 'blob'], details.responseType)) {
-      payload.responseType = 'blob';
-    }
-    helpers.encodeBody(details.data)
-    .then(body => {
-      payload.data = body;
-      bridge.post({
-        cmd: 'HttpRequest',
-        data: payload,
-      });
-    });
-  }
-  function getFullUrl(url) {
-    const a = document.createElement('a');
-    a.setAttribute('href', url);
-    return a.href;
-  }
-}
-
-function getTab(detail) {
-  const bridge = this;
-  init();
-  return bridge.getTab(detail);
-  function init() {
-    bridge.tabs = {};
-    bridge.getTab = data => {
-      const key = bridge.getUniqId();
-      const item = {
-        close() {
-          bridge.post({ cmd: 'TabClose', data: key });
-        },
-        onclose: null,
-        closed: false,
-      };
-      bridge.tabs[key] = item;
-      bridge.post({ cmd: 'TabOpen', data: { key, data } });
-      return item;
-    };
-  }
-}
-
 function wrapGM(script, cache) {
   // Add GM functions
   // Reference: http://wiki.greasespot.net/Greasemonkey_Manual:API
-  const bridge = this;
   const gm = {};
   const grant = script.meta.grant || [];
   const urls = {};
@@ -370,12 +137,11 @@ function wrapGM(script, cache) {
     // @grant none
     grant.pop();
   } else {
-    gm.window = bridge.getWrapper();
+    gm.window = getWrapper();
   }
-  const { helpers } = bridge;
-  if (!helpers.includes(grant, 'unsafeWindow')) grant.push('unsafeWindow');
-  if (!helpers.includes(grant, 'GM_info')) grant.push('GM_info');
-  if (helpers.includes(grant, 'window.close')) gm.window.close = () => { bridge.post({ cmd: 'TabClose' }); };
+  if (!includes(grant, 'unsafeWindow')) grant.push('unsafeWindow');
+  if (!includes(grant, 'GM_info')) grant.push('GM_info');
+  if (includes(grant, 'window.close')) gm.window.close = () => { bridge.post({ cmd: 'TabClose' }); };
   const resources = script.meta.resources || {};
   const dataEncoders = {
     o: val => JSON.stringify(val),
@@ -418,25 +184,25 @@ function wrapGM(script, cache) {
     },
     GM_deleteValue: {
       value(key) {
-        const values = getValues();
-        delete values[key];
+        const value = getValues();
+        delete value[key];
         saveValues();
       },
     },
     GM_getValue: {
       value(key, def) {
-        const values = getValues();
-        const raw = values[key];
+        const value = getValues();
+        const raw = value[key];
         if (raw) {
           const type = raw[0];
           const handle = dataDecoders[type] || dataDecoders[''];
-          let value = raw.slice(1);
+          let val = raw.slice(1);
           try {
-            value = handle(value);
+            val = handle(val);
           } catch (e) {
             console.warn(e);
           }
-          return value;
+          return val;
         }
         return def;
       },
@@ -450,10 +216,9 @@ function wrapGM(script, cache) {
       value(key, val) {
         const type = (typeof val)[0];
         const handle = dataEncoders[type] || dataEncoders[''];
-        let value = val;
-        value = type + handle(value);
-        const values = getValues();
-        values[key] = value;
+        const raw = type + handle(val);
+        const value = getValues();
+        value[key] = raw;
         saveValues();
       },
     },
@@ -508,19 +273,17 @@ function wrapGM(script, cache) {
           active: !options,
         };
         data.url = url;
-        return bridge.getTab(data);
+        return onTabCreate(data);
       },
     },
     GM_registerMenuCommand: {
       value(cap, func, acc) {
-        bridge.command[cap] = func;
+        commands[cap] = func;
         bridge.post({ cmd: 'RegisterMenu', data: [cap, acc] });
       },
     },
     GM_xmlhttpRequest: {
-      value(details) {
-        return bridge.getRequest(details);
-      },
+      value: onRequestCreate,
     },
     GM_notification: {
       value(text, title, image, onclick) {
@@ -533,18 +296,7 @@ function wrapGM(script, cache) {
         if (!options.text) {
           throw new Error('GM_notification: `text` is required!');
         }
-        const id = (bridge.notif[''] || 0) + 1;
-        bridge.notif[''] = id;
-        bridge.notif[id] = options;
-        bridge.post({
-          cmd: 'Notification',
-          data: {
-            id,
-            text: options.text,
-            title: options.title,
-            image: options.image,
-          },
-        });
+        onNotificationCreate(options);
       },
     },
     GM_setClipboard: {
@@ -556,13 +308,13 @@ function wrapGM(script, cache) {
       },
     },
   };
-  helpers.forEach(grant, name => {
+  forEach(grant, name => {
     const prop = gmFunctions[name];
     if (prop) addProperty(name, prop, gm);
   });
   return gm;
   function getValues() {
-    return bridge.values[script.uri];
+    return values[script.uri];
   }
   function propertyToString() {
     return '[Violentmonkey property]';
@@ -590,16 +342,14 @@ function wrapGM(script, cache) {
 function getWrapper() {
   // http://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects
   // http://developer.mozilla.org/docs/Web/API/Window
-  const bridge = this;
   const wrapper = {};
-  const { helpers } = bridge;
-  helpers.forEach([
+  forEach([
     // `eval` should be called directly so that it is run in current scope
     'eval',
   ], name => {
     wrapper[name] = window[name];
   });
-  helpers.forEach([
+  forEach([
     // 'uneval',
     'isFinite',
     'isNaN',
@@ -677,7 +427,7 @@ function getWrapper() {
     });
   }
   // Wrap properties
-  helpers.forEach(bridge.props, name => {
+  forEach(bridge.props, name => {
     if (name in wrapper) return;
     if (name.slice(0, 2) === 'on') defineReactedProperty(name);
     else defineProtectedProperty(name);
@@ -685,11 +435,51 @@ function getWrapper() {
   return wrapper;
 }
 
-function initialize(src, dest, props) {
-  const bridge = this;
-  bridge.prepare(src, dest);
-  bridge.props = props;
-  const { noop } = bridge.helpers;
-  bridge.load = noop;
-  bridge.checkLoad = noop;
+function runCode(name, func, args, thisObj) {
+  if (process.env.DEBUG) {
+    console.log(`Run script: ${name}`); // eslint-disable-line no-console
+  }
+  try {
+    func.apply(thisObj, args);
+  } catch (e) {
+    let msg = `Error running script: ${name}\n${e}`;
+    if (e.message) msg = `${msg}\n${e.message}`;
+    console.error(msg);
+  }
+}
+
+function exposeVM() {
+  const Violentmonkey = {};
+  const checking = {};
+  let key = 0;
+  bridge.onScriptChecked = ({ callback, result }) => {
+    const cb = checking[callback];
+    if (cb) {
+      cb(result);
+      delete checking[callback];
+    }
+  };
+  Object.defineProperty(Violentmonkey, 'getVersion', {
+    value: () => Promise.resolve({
+      version: bridge.version,
+    }),
+  });
+  Object.defineProperty(Violentmonkey, 'isInstalled', {
+    value: (name, namespace) => new Promise(resolve => {
+      key += 1;
+      const callback = checking[key];
+      checking[callback] = resolve;
+      bridge.post({
+        cmd: 'CheckScript',
+        data: {
+          name,
+          namespace,
+          callback,
+        },
+      });
+    }),
+  });
+  Object.defineProperty(window.external, 'Violentmonkey', {
+    value: Violentmonkey,
+  });
 }

+ 35 - 0
src/injected/web/notifications.js

@@ -0,0 +1,35 @@
+import bridge from './bridge';
+
+let lastId = 0;
+const notifications = {};
+
+export function onNotificationCreate(options) {
+  lastId += 1;
+  notifications[lastId] = options;
+  bridge.post({
+    cmd: 'Notification',
+    data: {
+      id: lastId,
+      text: options.text,
+      title: options.title,
+      image: options.image,
+    },
+  });
+}
+
+export function onNotificationClicked(id) {
+  const options = notifications[id];
+  if (options) {
+    const { onclick } = options;
+    if (onclick) onclick();
+  }
+}
+
+export function onNotificationClosed(id) {
+  const options = notifications[id];
+  if (options) {
+    delete notifications[id];
+    const { ondone } = options;
+    if (ondone) ondone();
+  }
+}

+ 106 - 0
src/injected/web/requests.js

@@ -0,0 +1,106 @@
+import { includes, encodeBody } from './helpers';
+import bridge from './bridge';
+
+const map = {};
+const queue = [];
+
+export function onRequestCreate(details) {
+  const req = {
+    details,
+    req: {
+      abort: reqAbort,
+    },
+  };
+  details.url = getFullUrl(details.url);
+  queue.push(req);
+  bridge.post({ cmd: 'GetRequestId' });
+  return req.req;
+}
+
+export function onRequestStart(id) {
+  const req = queue.shift();
+  start(req, id);
+}
+
+export function onRequestCallback(res) {
+  const req = map[res.id];
+  if (req) callback(req, res);
+}
+
+function reqAbort() {
+  bridge.post({ cmd: 'AbortRequest', data: this.id });
+}
+
+function parseData(req, details) {
+  if (req.resType) {
+    // blob or arraybuffer
+    let data = req.data.response.split(',');
+    const mimetype = data[0].match(/^data:(.*?);base64$/);
+    if (!mimetype) {
+      // invalid
+      req.data.response = null;
+    } else {
+      data = window.atob(data[1]);
+      const arr = new window.Uint8Array(data.length);
+      for (let i = 0; i < data.length; i += 1) arr[i] = data.charCodeAt(i);
+      if (details.responseType === 'blob') {
+        // blob
+        return new Blob([arr], { type: mimetype });
+      }
+      // arraybuffer
+      return arr.buffer;
+    }
+  } else if (details.responseType === 'json') {
+    // json
+    return JSON.parse(req.data.response);
+  } else {
+    // text
+    return req.data.response;
+  }
+}
+
+// request object functions
+function callback(req, res) {
+  const cb = req.details[`on${res.type}`];
+  if (cb) {
+    if (res.data.response) {
+      if (!req.data) req.data = [parseData(res, req.details)];
+      res.data.response = req.data[0];
+    }
+    res.data.context = req.details.context;
+    cb(res.data);
+  }
+  if (res.type === 'loadend') delete map[req.id];
+}
+
+function start(req, id) {
+  const { details } = req;
+  const payload = {
+    id,
+    method: details.method,
+    url: details.url,
+    user: details.user,
+    password: details.password,
+    headers: details.headers,
+    overrideMimeType: details.overrideMimeType,
+  };
+  req.id = id;
+  map[id] = req;
+  if (includes(['arraybuffer', 'blob'], details.responseType)) {
+    payload.responseType = 'blob';
+  }
+  encodeBody(details.data)
+  .then(body => {
+    payload.data = body;
+    bridge.post({
+      cmd: 'HttpRequest',
+      data: payload,
+    });
+  });
+}
+
+function getFullUrl(url) {
+  const a = document.createElement('a');
+  a.setAttribute('href', url);
+  return a.href;
+}

+ 30 - 0
src/injected/web/tabs.js

@@ -0,0 +1,30 @@
+import bridge from './bridge';
+
+const tabs = {};
+
+export function onTabCreate(data) {
+  const item = {
+    id: null,
+    onclose: null,
+    closed: false,
+  };
+  const ready = bridge.post({ cmd: 'TabOpen', data })
+  .then(({ id }) => {
+    item.id = id;
+    tabs[id] = item;
+  });
+  item.close = () => ready.then(() => {
+    bridge.post({ cmd: 'TabClose', data: this.id });
+  });
+  return item;
+}
+
+export function onTabClosed(id) {
+  const item = tabs[id];
+  if (item) {
+    item.closed = true;
+    const { onclose } = item;
+    if (onclose) onclose();
+    delete tabs[id];
+  }
+}

+ 1 - 0
src/manifest.json

@@ -27,6 +27,7 @@
     {
       "js": [
         "browser.js",
+        "injected-web.js",
         "injected.js"
       ],
       "matches": [