From c74d1af46d6adaabac9c75cc661ccc802d9457bb Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Thu, 6 Oct 2016 18:20:37 -0700 Subject: [PATCH] Initial checkin. --- .eslintrc | 457 +++++++++++++++++++++++++++++++++++++++ .gitignore | 2 + Gruntfile.js | 87 ++++++++ README.md | 161 ++++++++++++++ api-metadata.json | 512 ++++++++++++++++++++++++++++++++++++++++++++ browser-polyfill.js | 338 +++++++++++++++++++++++++++++ package.json | 21 ++ 7 files changed, 1578 insertions(+) create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 Gruntfile.js create mode 100644 README.md create mode 100644 api-metadata.json create mode 100644 browser-polyfill.js create mode 100644 package.json diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..239f70e --- /dev/null +++ b/.eslintrc @@ -0,0 +1,457 @@ +{ + "parserOptions": { + "ecmaVersion": 6, + }, + + "env": { + "browser": true, + "es6": true, + "webextensions": true, + }, + + "globals": { + }, + + "rules": { + "valid-jsdoc": [2, { + "prefer": { + "return": "returns", + }, + "preferType": { + "Boolean": "boolean", + "Number": "number", + "String": "string", + "bool": "boolean", + }, + "requireParamDescription": false, + "requireReturn": false, + "requireReturnDescription": false, + }], + + // Require spacing around => + "arrow-spacing": 2, + + // Always require spacing around a single line block + "block-spacing": 1, + + // Forbid spaces inside the square brackets of array literals. + "array-bracket-spacing": [2, "never"], + + // Forbid spaces inside the curly brackets of object literals. + "object-curly-spacing": [2, "never"], + + // No space padding in parentheses + "space-in-parens": [2, "never"], + + // Enforce one true brace style (opening brace on the same line) and avoid + // start and end braces on the same line. + "brace-style": [2, "1tbs", {"allowSingleLine": true}], + + // No space before always a space after a comma + "comma-spacing": [2, {"before": false, "after": true}], + + // Commas at the end of the line not the start + "comma-style": 2, + + // Don't require spaces around computed properties + "computed-property-spacing": [1, "never"], + + // Functions are not required to consistently return something or nothing + "consistent-return": 0, + + // Require braces around blocks that start a new line + "curly": [2, "all"], + + // Always require a trailing EOL + "eol-last": 2, + + // Require function* name() + "generator-star-spacing": [2, {"before": false, "after": true}], + + // Two space indent + "indent": [2, 2, {"SwitchCase": 1}], + + // Space after colon not before in property declarations + "key-spacing": [2, {"beforeColon": false, "afterColon": true, "mode": "minimum"}], + + // Require spaces before and after finally, catch, etc. + "keyword-spacing": 2, + + // Unix linebreaks + "linebreak-style": [2, "unix"], + + // Always require parenthesis for new calls + "new-parens": 2, + + // Use [] instead of Array() + "no-array-constructor": 2, + + // No duplicate arguments in function declarations + "no-dupe-args": 2, + + // No duplicate keys in object declarations + "no-dupe-keys": 2, + + // No duplicate cases in switch statements + "no-duplicate-case": 2, + + // Disallow empty statements. This will report an error for: + // try { something(); } catch (e) {} + // but will not report it for: + // try { something(); } catch (e) { /* Silencing the error because ...*/ } + // which is a valid use case. + "no-empty": 2, + + // No empty character classes in regex + "no-empty-character-class": 2, + + // Disallow empty destructuring + "no-empty-pattern": 2, + + // No assiging to exception variable + "no-ex-assign": 2, + + // No using !! where casting to boolean is already happening + "no-extra-boolean-cast": 1, + + // No double semicolon + "no-extra-semi": 2, + + // No overwriting defined functions + "no-func-assign": 2, + + // No invalid regular expresions + "no-invalid-regexp": 2, + + // No odd whitespace characters + "no-irregular-whitespace": 2, + + // No single if block inside an else block + "no-lonely-if": 1, + + // No mixing spaces and tabs in indent + "no-mixed-spaces-and-tabs": [2, "smart-tabs"], + + // Disallow use of multiple spaces (sometimes used to align const values, + // array or object items, etc.). It's hard to maintain and doesn't add that + // much benefit. + "no-multi-spaces": 1, + + // No reassigning native JS objects + "no-native-reassign": 2, + + // No (!foo in bar) + "no-negated-in-lhs": 2, + + // Nested ternary statements are confusing + "no-nested-ternary": 2, + + // Use {} instead of new Object() + "no-new-object": 2, + + // No Math() or JSON() + "no-obj-calls": 2, + + // No octal literals + "no-octal": 2, + + // No redeclaring variables + "no-redeclare": 2, + + // No unnecessary comparisons + "no-self-compare": 2, + + // No spaces between function name and parentheses + "no-spaced-func": 1, + + // No trailing whitespace + "no-trailing-spaces": 2, + + // Error on newline where a semicolon is needed + "no-unexpected-multiline": 2, + + // No unreachable statements + "no-unreachable": 2, + + // No expressions where a statement is expected + "no-unused-expressions": 2, + + // No declaring variables that are never used + "no-unused-vars": [2, {"args": "none", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}], + + // No using variables before defined + "no-use-before-define": 2, + + // No using with + "no-with": 2, + + // Always require semicolon at end of statement + "semi": [2, "always"], + + // Require space before blocks + "space-before-blocks": 2, + + // Never use spaces before function parentheses + "space-before-function-paren": [2, {"anonymous": "never", "named": "never"}], + + // Require spaces around operators, except for a|0. + "space-infix-ops": [2, {"int32Hint": true}], + + // ++ and -- should not need spacing + "space-unary-ops": [1, {"nonwords": false, "words": true, "overrides": {"typeof": false}}], + + // No comparisons to NaN + "use-isnan": 2, + + // Only check typeof against valid results + "valid-typeof": 2, + + // Disallow using variables outside the blocks they are defined (especially + // since only let and const are used, see "no-var"). + "block-scoped-var": 2, + + // Allow trailing commas for easy list extension. Having them does not + // impair readability, but also not required either. + "comma-dangle": [2, "always-multiline"], + + // Warn about cyclomatic complexity in functions. + "complexity": 1, + + // Don't require a default case in switch statements. Avoid being forced to + // add a bogus default when you know all possible cases are handled. + "default-case": 0, + + // Enforce dots on the next line with property name. + "dot-location": [2, "property"], + + // Encourage the use of dot notation whenever possible. + "dot-notation": 2, + + // Allow using == instead of ===, in the interest of landing something since + // the devtools codebase is split on convention here. + "eqeqeq": 0, + + // Don't require function expressions to have a name. + // This makes the code more verbose and hard to read. Our engine already + // does a fantastic job assigning a name to the function, which includes + // the enclosing function name, and worst case you have a line number that + // you can just look up. + "func-names": 0, + + // Allow use of function declarations and expressions. + "func-style": 0, + + // Don't enforce the maximum depth that blocks can be nested. The complexity + // rule is a better rule to check this. + "max-depth": 0, + + // Maximum length of a line. + // Disabled because we exceed this in too many places. + "max-len": [0, 80], + + // Maximum depth callbacks can be nested. + "max-nested-callbacks": [2, 4], + + // Don't limit the number of parameters that can be used in a function. + "max-params": 0, + + // Don't limit the maximum number of statement allowed in a function. We + // already have the complexity rule that's a better measurement. + "max-statements": 0, + + // Don't require a capital letter for constructors, only check if all new + // operators are followed by a capital letter. Don't warn when capitalized + // functions are used without the new operator. + "new-cap": [0, {"capIsNew": false}], + + // Allow use of bitwise operators. + "no-bitwise": 0, + + // Disallow use of arguments.caller or arguments.callee. + "no-caller": 2, + + // Disallow the catch clause parameter name being the same as a variable in + // the outer scope, to avoid confusion. + "no-catch-shadow": 0, + + // Disallow assignment in conditional expressions. + "no-cond-assign": 2, + + // Allow using constant expressions in conditions like while (true) + "no-constant-condition": 0, + + // Allow use of the continue statement. + "no-continue": 0, + + // Disallow control characters in regular expressions. + "no-control-regex": 2, + + // Disallow use of debugger. + "no-debugger": 2, + + // Disallow deletion of variables (deleting properties is fine). + "no-delete-var": 2, + + // Allow division operators explicitly at beginning of regular expression. + "no-div-regex": 0, + + // Disallow use of eval(). We have other APIs to evaluate code in content. + "no-eval": 2, + + // Disallow adding to native types + "no-extend-native": 2, + + // Disallow unnecessary function binding. + "no-extra-bind": 2, + + // Allow unnecessary parentheses, as they may make the code more readable. + "no-extra-parens": 0, + + // Disallow fallthrough of case statements, except if there is a comment. + "no-fallthrough": 2, + + // Allow the use of leading or trailing decimal points in numeric literals. + "no-floating-decimal": 0, + + // Allow comments inline after code. + "no-inline-comments": 0, + + // Disallow use of labels for anything other then loops and switches. + "no-labels": [2, { "allowLoop": true }], + + // Disallow use of multiline strings (use template strings instead). + "no-multi-str": 1, + + // Disallow multiple empty lines. + "no-multiple-empty-lines": [1, {"max": 2}], + + // Allow reassignment of function parameters. + "no-param-reassign": 0, + + // Allow string concatenation with __dirname and __filename (not a node env). + "no-path-concat": 0, + + // Allow use of unary operators, ++ and --. + "no-plusplus": 0, + + // Allow using process.env (not a node environment). + "no-process-env": 0, + + // Allow using process.exit (not a node environment). + "no-process-exit": 0, + + // Disallow usage of __proto__ property. + "no-proto": 2, + + // Disallow multiple spaces in a regular expression literal. + "no-regex-spaces": 2, + + // Allow reserved words being used as object literal keys. + "no-reserved-keys": 0, + + // Don't restrict usage of specified node modules (not a node environment). + "no-restricted-modules": 0, + + // Disallow use of assignment in return statement. It is preferable for a + // single line of code to have only one easily predictable effect. + "no-return-assign": 2, + + // Don't warn about declaration of variables already declared in the outer scope. + "no-shadow": 0, + + // Disallow shadowing of names such as arguments. + "no-shadow-restricted-names": 2, + + // Allow use of synchronous methods (not a node environment). + "no-sync": 0, + + // Allow the use of ternary operators. + "no-ternary": 0, + + // Disallow throwing literals (eg. throw "error" instead of + // throw new Error("error")). + "no-throw-literal": 2, + + // Disallow use of undeclared variables unless mentioned in a /* global */ + // block. Note that globals from head.js are automatically imported in tests + // by the import-headjs-globals rule form the mozilla eslint plugin. + "no-undef": 2, + + // Allow dangling underscores in identifiers (for privates). + "no-underscore-dangle": 0, + + // Allow use of undefined variable. + "no-undefined": 0, + + // Disallow the use of Boolean literals in conditional expressions. + "no-unneeded-ternary": 2, + + // We use var-only-at-top-level instead of no-var as we allow top level + // vars. + "no-var": 0, + + // Allow using TODO/FIXME comments. + "no-warning-comments": 0, + + // Don't require method and property shorthand syntax for object literals. + // We use this in the code a lot, but not consistently, and this seems more + // like something to check at code review time. + "object-shorthand": 0, + + // Allow more than one variable declaration per function. + "one-var": 0, + + // Disallow padding within blocks. + "padded-blocks": [1, "never"], + + // Don't require quotes around object literal property names. + "quote-props": 0, + + // Double quotes should be used. + "quotes": [1, "double", {"avoidEscape": true, "allowTemplateLiterals": true}], + + // Require use of the second argument for parseInt(). + "radix": 2, + + // Enforce spacing after semicolons. + "semi-spacing": [2, {"before": false, "after": true}], + + // Don't require to sort variables within the same declaration block. + // Anyway, one-var is disabled. + "sort-vars": 0, + + // Require a space immediately following the // in a line comment. + "spaced-comment": [2, "always"], + + // Require "use strict" to be defined globally in the script. + "strict": [2, "global"], + + // Allow vars to be declared anywhere in the scope. + "vars-on-top": 0, + + // Don't require immediate function invocation to be wrapped in parentheses. + "wrap-iife": 0, + + // Don't require regex literals to be wrapped in parentheses (which + // supposedly prevent them from being mistaken for division operators). + "wrap-regex": 0, + + // Disallow Yoda conditions (where literal value comes first). + "yoda": 2, + + // disallow use of eval()-like methods + "no-implied-eval": 2, + + // Disallow function or variable declarations in nested blocks + "no-inner-declarations": 2, + + // Disallow usage of __iterator__ property + "no-iterator": 2, + + // Disallow labels that share a name with a variable + "no-label-var": 2, + + // Disallow creating new instances of String, Number, and Boolean + "no-new-wrappers": 2, + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3659f1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/* +dist/* diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..7f2465d --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,87 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* eslint-env commonjs */ + +const LICENSE = `/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */`; + +module.exports = function(grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON("package.json"), + + eslint: { + src: ["browser-polyfill.js", "Gruntfile.js"], + }, + + replace: { + dist: { + options: { + patterns: [ + { + match: /\{\/\* include\("(.*?)"\) \*\/\}/, + replacement: (match, filename) => { + return grunt.file.read(filename) + .replace(/\n$/, "") + .replace(/^[^{]/gm, " $&"); + }, + }, + { + json: { + "package_name": "<%= pkg.name %>", + "version": "<%= pkg.version %>", + "timestamp": "<%= grunt.template.today() %>", + }, + }, + ], + }, + files: [ + { + expand: true, + flatten: true, + src: ["browser-polyfill.js"], + dest: "dist/", + }, + ], + }, + }, + + "closure-compiler": { + dist: { + files: { + "dist/browser-polyfill.min.js": ["dist/browser-polyfill.js"], + }, + options: { + // Closure currently supports only whitespace and comment stripping + // when both the input and output languages are ES6. + compilation_level: "WHITESPACE_ONLY", + language_in: "ECMASCRIPT6_STRICT", + language_out: "ECMASCRIPT6", + output_wrapper: `${LICENSE}\n%output%`, + }, + }, + }, + + // This currently does not support ES6 classes. + uglify: { + options: { + banner: LICENSE, + compress: true, + }, + + dist: { + files: { + "dist/browser-polyfill.min.js": ["dist/browser-polyfill.js"], + }, + }, + }, + }); + + grunt.loadNpmTasks("gruntify-eslint"); + grunt.loadNpmTasks("grunt-replace"); + require("google-closure-compiler").grunt(grunt); + + grunt.registerTask("default", ["eslint", "replace", "closure-compiler"]); +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..0565c72 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# WebExtension `browser` API Polyfill + +This library allows extensions written for the Promise-based +WebExtension/BrowserExt API being standardized by the [W3 Browser +Extensions][w3-browserext] group to be used without modification in Google +Chrome. + +[w3-browserext]: https://www.w3.org/community/browserext/ + +## Basic Setup + +In order to use the polyfill, it must be loaded into any context where +`browser` APIs are accessed. The most common cases are background and +content scripts, which can be specified in `manifest.json`: + +```javascript +{ + // ... + + "background": { + "scripts": [ + "browser-polyfill.js", + "background.js" + ] + }, + + "content_scripts": [{ + // ... + "js": [ + "browser-polyfill.js", + "content.js" + ] + }] +} +``` + +For HTML documents, such as `browserAction` popups, or tab pages, it must be +included more explicitly: + +```html + + + + + + + + +``` + +And for dynamically-injected content scripts loaded by `tabs.executeScript`, +it must be injected by a separate `executeScript` call, unless it has +already been loaded via a `content_scripts` declaration in +`manifest.json`: + +```javascript +browser.tabs.executeScript({file: "browser-polyfill.js"}); +browser.tabs.executeScript({file: "content.js"}).then(result => { + // ... +}); +``` + + +## Using the Promise-based APIs + +The Promise-based APIs in the `browser` namespace work, for the most part, +very similarly to the callback-based APIs in Chrome's `chrome` namespace. +The major differences are: + +* Rather than receiving a callback argument, every async function returns a + `Promise` object, which resolves or rejects when the operation completes. + +* Rather than checking the `chrome.runtime.lastError` property from every + callback, code which needs to explicitly deal with errors registers a + separate Promise rejection handler. + +* Rather than receiving a `sendResponse` callback to send a response, + `onMessage` listeners simply return a Promise whose resolution value is + used as a reply. + +* Rather than nesting callbacks when a sequence of operations depend on each + other, Promise chaining is generally used instead. + +* For users of an ES7 transpiler, such as Babel, the resulting Promises are + generally used with `async` and `await`, rather than dealt with + directly. + +## Examples + +The following code will retrieve a list of URLs patterns from the `storage` +API, retrieve a list of tabs which match any of them, reload each of those +tabs, and notify the user that is has been done: + +```javascript +browser.storage.get("urls").then(({urls}) => { + return browser.tabs.query({url: urls}); +}).then(tabs => { + return Promise.all( + Array.from(tabs, tab => browser.tabs.reload(tab.id))); + ); +}).then(() => { + return browser.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "Tabs reloaded", + message: "Your tabs have been reloaded", + }); +}).catch(error => { + console.error(`An error occurred while reloading tabs: ${error.message}`); +}); +``` + +Or, using an async function: + +```javascript +async function reloadTabs() { + try { + let {urls} = await browser.storage.get("urls"); + + let tabs = await browser.tabs.query({url: urls}); + + await Promise.all( + Array.from(tabs, tab => browser.tabs.reload(tab.id))); + ); + + await browser.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "Tabs reloaded", + message: "Your tabs have been reloaded", + }); + } catch (error) { + console.error(`An error occurred while reloading tabs: ${error.message}`); + } +} +``` + +It's also possible to use Promises effectively using two-way messaging. +Communication between a background page and a tab content script, for example, +looks something like this from the background page side: + +```javascript +browser.tabs.sendMessage("get-ids").then(results => { + processResults(results); +}); +``` + +And like this from the content script: + +```javascript +browser.runtime.onMessage.addListener(msg => { + if (msg == "get-ids") { + return browser.storage.get("idPattern").then(({idPattern}) => { + return Array.from(document.querySelectorAll(idPattern), + elem => elem.textContent); + }); + } +}); +``` + +Or vice versa. diff --git a/api-metadata.json b/api-metadata.json new file mode 100644 index 0000000..b3542cb --- /dev/null +++ b/api-metadata.json @@ -0,0 +1,512 @@ +{ + "alarms": { + "clear": { + "minArgs": 0, + "maxArgs": 1 + }, + "clearAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "get": { + "minArgs": 0, + "maxArgs": 1 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "bookmarks": { + "create": { + "minArgs": 1, + "maxArgs": 1 + }, + "export": { + "minArgs": 0, + "maxArgs": 0 + }, + "get": { + "minArgs": 1, + "maxArgs": 1 + }, + "getChildren": { + "minArgs": 1, + "maxArgs": 1 + }, + "getRecent": { + "minArgs": 1, + "maxArgs": 1 + }, + "getTree": { + "minArgs": 0, + "maxArgs": 0 + }, + "getSubTree": { + "minArgs": 1, + "maxArgs": 1 + }, + "import": { + "minArgs": 0, + "maxArgs": 0 + }, + "move": { + "minArgs": 2, + "maxArgs": 2 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeTree": { + "minArgs": 1, + "maxArgs": 1 + }, + "search": { + "minArgs": 1, + "maxArgs": 1 + }, + "update": { + "minArgs": 2, + "maxArgs": 2 + } + }, + "browserAction": { + "getBadgeBackgroundColor": { + "minArgs": 1, + "maxArgs": 1 + }, + "getBadgeText": { + "minArgs": 1, + "maxArgs": 1 + }, + "getPopup": { + "minArgs": 1, + "maxArgs": 1 + }, + "getTitle": { + "minArgs": 1, + "maxArgs": 1 + }, + "setIcon": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "commands": { + "getAll": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "contextMenus": { + "update": { + "minArgs": 2, + "maxArgs": 2 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeAll": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "cookies": { + "get": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAll": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAllCookieStores": { + "minArgs": 0, + "maxArgs": 0 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "set": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "downloads": { + "download": { + "minArgs": 1, + "maxArgs": 1 + }, + "cancel": { + "minArgs": 1, + "maxArgs": 1 + }, + "erase": { + "minArgs": 1, + "maxArgs": 1 + }, + "getFileIcon": { + "minArgs": 1, + "maxArgs": 2 + }, + "open": { + "minArgs": 1, + "maxArgs": 1 + }, + "pause": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeFile": { + "minArgs": 1, + "maxArgs": 1 + }, + "resume": { + "minArgs": 1, + "maxArgs": 1 + }, + "search": { + "minArgs": 1, + "maxArgs": 1 + }, + "show": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "extension": { + "isAllowedFileSchemeAccess": { + "minArgs": 0, + "maxArgs": 0 + }, + "isAllowedIncognitoAccess": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "history": { + "addUrl": { + "minArgs": 1, + "maxArgs": 1 + }, + "getVisits": { + "minArgs": 1, + "maxArgs": 1 + }, + "deleteAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "deleteRange": { + "minArgs": 1, + "maxArgs": 1 + }, + "deleteUrl": { + "minArgs": 1, + "maxArgs": 1 + }, + "search": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "i18n": { + "detectLanguage": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAcceptLanguages": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "idle": { + "queryState": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "management": { + "get": { + "minArgs": 1, + "maxArgs": 1 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "getSelf": { + "minArgs": 0, + "maxArgs": 0 + }, + "uninstallSelf": { + "minArgs": 0, + "maxArgs": 1 + } + }, + "notifications": { + "clear": { + "minArgs": 1, + "maxArgs": 1 + }, + "create": { + "minArgs": 1, + "maxArgs": 2 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 0 + }, + "getPermissionLevel": { + "minArgs": 0, + "maxArgs": 0 + }, + "update": { + "minArgs": 2, + "maxArgs": 2 + } + }, + "pageAction": { + "getPopup": { + "minArgs": 1, + "maxArgs": 1 + }, + "getTitle": { + "minArgs": 1, + "maxArgs": 1 + }, + "hide": { + "minArgs": 0, + "maxArgs": 0 + }, + "setIcon": { + "minArgs": 1, + "maxArgs": 1 + }, + "show": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "runtime": { + "getBackgroundPage": { + "minArgs": 0, + "maxArgs": 0 + }, + "getBrowserInfo": { + "minArgs": 0, + "maxArgs": 0 + }, + "getPlatformInfo": { + "minArgs": 0, + "maxArgs": 0 + }, + "openOptionsPage": { + "minArgs": 0, + "maxArgs": 0 + }, + "requestUpdateCheck": { + "minArgs": 0, + "maxArgs": 0 + }, + "sendMessage": { + "minArgs": 1, + "maxArgs": 3 + }, + "sendNativeMessage": { + "minArgs": 2, + "maxArgs": 2 + }, + "setUninstallURL": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "storage": { + "local": { + "clear": { + "minArgs": 0, + "maxArgs": 0 + }, + "get": { + "minArgs": 0, + "maxArgs": 1 + }, + "getBytesInUse": { + "minArgs": 0, + "maxArgs": 1 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "set": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "managed": { + "get": { + "minArgs": 0, + "maxArgs": 1 + }, + "getBytesInUse": { + "minArgs": 0, + "maxArgs": 1 + } + }, + "sync": { + "clear": { + "minArgs": 0, + "maxArgs": 0 + }, + "get": { + "minArgs": 0, + "maxArgs": 1 + }, + "getBytesInUse": { + "minArgs": 0, + "maxArgs": 1 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "set": { + "minArgs": 1, + "maxArgs": 1 + } + } + }, + "tabs": { + "create": { + "minArgs": 1, + "maxArgs": 1 + }, + "captureVisibleTab": { + "minArgs": 0, + "maxArgs": 2 + }, + "detectLanguage": { + "minArgs": 0, + "maxArgs": 1 + }, + "duplicate": { + "minArgs": 1, + "maxArgs": 1 + }, + "executeScript": { + "minArgs": 1, + "maxArgs": 2 + }, + "get": { + "minArgs": 1, + "maxArgs": 1 + }, + "getCurrent": { + "minArgs": 0, + "maxArgs": 0 + }, + "getZoom": { + "minArgs": 0, + "maxArgs": 1 + }, + "getZoomSettings": { + "minArgs": 0, + "maxArgs": 1 + }, + "highlight": { + "minArgs": 1, + "maxArgs": 1 + }, + "insertCSS": { + "minArgs": 1, + "maxArgs": 2 + }, + "move": { + "minArgs": 2, + "maxArgs": 2 + }, + "reload": { + "minArgs": 0, + "maxArgs": 2 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "query": { + "minArgs": 1, + "maxArgs": 1 + }, + "removeCSS": { + "minArgs": 1, + "maxArgs": 2 + }, + "sendMessage": { + "minArgs": 2, + "maxArgs": 3 + }, + "setZoom": { + "minArgs": 1, + "maxArgs": 2 + }, + "setZoomSettings": { + "minArgs": 1, + "maxArgs": 2 + }, + "update": { + "minArgs": 1, + "maxArgs": 2 + } + }, + "webNavigation": { + "getAllFrames": { + "minArgs": 1, + "maxArgs": 1 + }, + "getFrame": { + "minArgs": 1, + "maxArgs": 1 + } + }, + "webRequest": { + "handlerBehaviorChanged": { + "minArgs": 0, + "maxArgs": 0 + } + }, + "windows": { + "create": { + "minArgs": 0, + "maxArgs": 1 + }, + "get": { + "minArgs": 1, + "maxArgs": 2 + }, + "getAll": { + "minArgs": 0, + "maxArgs": 1 + }, + "getCurrent": { + "minArgs": 0, + "maxArgs": 1 + }, + "getLastFocused": { + "minArgs": 0, + "maxArgs": 1 + }, + "remove": { + "minArgs": 1, + "maxArgs": 1 + }, + "update": { + "minArgs": 2, + "maxArgs": 2 + } + } +} diff --git a/browser-polyfill.js b/browser-polyfill.js new file mode 100644 index 0000000..df26346 --- /dev/null +++ b/browser-polyfill.js @@ -0,0 +1,338 @@ +/* @@package_name - v@@version - @@timestamp */ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +if (typeof browser === "undefined") { + // Wrapping the bulk of this polyfill in a one-time-use function is a minor + // optimization for Firefox. Since Spidermonkey does not fully parse the + // contents of a function until the first time it's called, and since it will + // never actually need to be called, this allows the polyfill to be included + // in Firefox nearly for free. + const wrapAPIs = () => { + const apiMetadata = {/* include("api-metadata.json") */}; + + /** + * A WeakMap subclass which creates and stores a value for any key which does + * not exist when accessed, but behaves exactly as an ordinary WeakMap + * otherwise. + * + * @param {function} createItem + * A function which will be called in order to create the value for any + * key which does not exist, the first time it is accessed. The + * function receives, as its only argument, the key being created. + */ + class DefaultWeakMap extends WeakMap { + constructor(createItem, items = undefined) { + super(items); + this.createItem = createItem; + } + + get(key) { + if (!this.has(key)) { + this.set(key, this.createItem(key)); + } + + return super.get(key); + } + } + + /** + * Returns true if the given object is an object with a `then` method, and can + * therefore be assumed to behave as a Promise. + * + * @param {*} value The value to test. + * @returns {boolean} True if the value is thenable. + */ + const isThenable = value => { + return value && typeof value === "object" && typeof value.then === "function"; + }; + + /** + * Creates and returns a function which, when called, will resolve or reject + * the given promise based on how it is called: + * + * - If, when called, `chrome.runtime.lastError` contains a non-null object, + * the promise is rejected with that value. + * - If the function is called with exactly one argument, the promise is + * resolved to that value. + * - Otherwise, the promise is resolved to an array containing all of the + * function's arguments. + * + * @param {object} promise + * An object containing the resolution and rejection functions of a + * promise. + * @param {function} promise.resolve + * The promise's resolution function. + * @param {function} promise.rejection + * The promise's rejection function. + * + * @returns {function} + * The generated callback function. + */ + const makeCallback = promise => { + return (...callbackArgs) => { + if (chrome.runtime.lastError) { + promise.reject(chrome.runtime.lastError); + } else if (callbackArgs.length === 1) { + promise.resolve(callbackArgs[0]); + } else { + promise.resolve(callbackArgs); + } + }; + }; + + /** + * Creates a wrapper function for a method with the given name and metadata. + * + * @param {string} name + * The name of the method which is being wrapped. + * @param {object} metadata + * Metadata about the method being wrapped. + * @param {integer} metadata.minArgs + * The minimum number of arguments which must be passed to the + * function. If called with fewer than this number of arguments, the + * wrapper will raise an exception. + * @param {integer} metadata.maxArgs + * The maximum number of arguments which may be passed to the + * function. If called with more than this number of arguments, the + * wrapper will raise an exception. + * + * @returns {function(object, ...*)} + * The generated wrapper function. + */ + const wrapAsyncFunction = (name, metadata) => { + return function asyncFunctionWrapper(target, ...args) { + if (args.length < metadata.minArgs) { + throw new Error(`Expected at least ${metadata.minArgs} arguments for ${name}(), got ${args.length}`); + } + + if (args.length > metadata.maxArgs) { + throw new Error(`Expected at most ${metadata.maxArgs} arguments for ${name}(), got ${args.length}`); + } + + return new Promise((resolve, reject) => { + target[name](...args, makeCallback({resolve, reject})); + }); + }; + }; + + /** + * Wraps an existing method of the target object, so that calls to it are + * intercepted by the given wrapper function. The wrapper function receives, + * as its first argument, the original `target` object, followed by each of + * the arguments passed to the orginal method. + * + * @param {object} target + * The original target object that the wrapped method belongs to. + * @param {function} method + * The method being wrapped. This is used as the target of the Proxy + * object which is created to wrap the method. + * @param {function} wrapper + * The wrapper function which is called in place of a direct invocation + * of the wrapped method. + * + * @returns {Proxy} + * A Proxy object for the given method, which invokes the given wrapper + * method in its place. + */ + const wrapMethod = (target, method, wrapper) => { + return new Proxy(method, { + apply(targetMethod, thisObj, args) { + return wrapper.call(thisObj, target, ...args); + }, + }); + }; + + let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); + + /** + * Wraps an object in a Proxy which intercepts and wraps certain methods + * based on the given `wrappers` and `metadata` objects. + * + * @param {object} target + * The target object to wrap. + * + * @param {object} [wrappers = {}] + * An object tree containing wrapper functions for special cases. Any + * function present in this object tree is called in place of the + * method in the same location in the `target` object tree. These + * wrapper methods are invoked as described in {@see wrapMethod}. + * + * @param {object} [metadata = {}] + * An object tree containing metadata used to automatically generate + * Promise-based wrapper functions for asynchronous. Any function in + * the `target` object tree which has a corresponding metadata object + * in the same location in the `metadata` tree is replaced with an + * automatically-generated wrapper function, as described in + * {@see wrapAsyncFunction} + * + * @returns {Proxy} + */ + const wrapObject = (target, wrappers = {}, metadata = {}) => { + let cache = Object.create(null); + + let handlers = { + has(target, prop) { + return prop in target || prop in cache; + }, + + get(target, prop, receiver) { + if (prop in cache) { + return cache[prop]; + } + + if (!(prop in target)) { + return undefined; + } + + let value = target[prop]; + + if (typeof value === "function") { + // This is a method on the underlying object. Check if we need to do + // any wrapping. + + if (typeof wrappers[prop] === "function") { + // We have a special-case wrapper for this method. + value = wrapMethod(target, target[prop], wrappers[prop]); + } else if (hasOwnProperty(metadata, prop)) { + // This is an async method that we have metadata for. Create a + // Promise wrapper for it. + let wrapper = wrapAsyncFunction(prop, metadata[prop]); + value = wrapMethod(target, target[prop], wrapper); + } else { + // This is a method that we don't know or care about. Return the + // original method, bound to the underlying object. + value = value.bind(target); + } + } else if (typeof value === "object" && value !== null && + (hasOwnProperty(wrappers, prop) || + hasOwnProperty(metadata, prop))) { + // This is an object that we need to do some wrapping for the children + // of. Create a sub-object wrapper for it with the appropriate child + // metadata. + value = wrapObject(value, wrappers[prop], metadata[prop]); + } else { + // We don't need to do any wrapping for this property, + // so just forward all access to the underlying object. + Object.defineProperty(cache, prop, { + configurable: true, + enumerable: true, + get() { + return target[prop]; + }, + set(value) { + target[prop] = value; + }, + }); + + return value; + } + + cache[prop] = value; + return value; + }, + + set(target, prop, value, receiver) { + if (prop in cache) { + cache[prop] = value; + } else { + target[prop] = value; + } + return true; + }, + + defineProperty(target, prop, desc) { + return Reflect.defineProperty(cache, prop, desc); + }, + + deleteProperty(target, prop) { + return Reflect.deleteProperty(cache, prop); + }, + }; + + return new Proxy(target, handlers); + }; + + /** + * Creates a set of wrapper functions for an event object, which handles + * wrapping of listener functions that those messages are passed. + * + * A single wrapper is created for each listener function, and stored in a + * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` + * retrieve the original wrapper, so that attempts to remove a + * previously-added listener work as expected. + * + * @param {DefaultWeakMap} wrapperMap + * A DefaultWeakMap object which will create the appropriate wrapper + * for a given listener function when one does not exist, and retrieve + * an existing one when it does. + * + * @returns {object} + */ + const wrapEvent = wrapperMap => ({ + addListener(target, listener, ...args) { + target.addListener(wrapperMap.get(listener), ...args); + }, + + hasListener(target, listener) { + return target.hasListener(wrapperMap.get(listener)); + }, + + removeListener(target, listener) { + target.removeListener(wrapperMap.get(listener)); + }, + }); + + const onMessageWrappers = new DefaultWeakMap(listener => { + if (typeof listener !== "function") { + return listener; + } + + /** + * Wraps a message listener function so that it may send responses based on + * its return value, rather than by returning a sentinel value and calling a + * callback. If the listener function returns a Promise, the response is + * sent when the promise either resolves or rejects. + * + * @param {*} message + * The message sent by the other end of the channel. + * @param {object} sender + * Details about the sender of the message. + * @param {function(*)} sendResponse + * A callback which, when called with an arbitrary argument, sends + * that value as a response. + * @returns {boolean} + * True if the wrapped listener returned a Promise, which will later + * yield a response. False otherwise. + */ + return function onMessage(message, sender, sendResponse) { + let result = listener(message, sender); + + if (isThenable(result)) { + result.then(sendResponse, error => { + console.error(error); + sendResponse(error); + }); + + return true; + } else if (result !== undefined) { + sendResponse(result); + } + }; + }); + + const staticWrappers = { + runtime: { + onMessage: wrapEvent(onMessageWrappers), + }, + }; + + return wrapObject(chrome, staticWrappers, apiMetadata); + }; + + this.browser = wrapAPIs(); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bf58da6 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "webextension-polyfill", + "version": "0.1.0", + "description": "A lightweight polyfill library for Promise-based WebExtension APIs in Chrome.", + "repository": { + "type": "git", + "url": "git+https://github.com/mozilla/webextension-polyfill.git" + }, + "author": "Mozilla", + "license": "MPL-2.0", + "bugs": { + "url": "https://github.com/mozilla/webextension-polyfill/issues" + }, + "homepage": "https://github.com/mozilla/webextension-polyfill", + "devDependencies": { + "google-closure-compiler": "^20160911.0.0", + "grunt": "^1.0.1", + "grunt-replace": "*", + "gruntify-eslint": "*" + } +}