285 lines
9.4 KiB
JavaScript
285 lines
9.4 KiB
JavaScript
'use strict';
|
|
|
|
if (self.INJECTED !== 1) {
|
|
/* Chrome reinjects content script when documentElement is replaced so we ignore it
|
|
by checking against a literal `1`, not just `if (truthy)`, because <html id="INJECTED">
|
|
is exposed per HTML spec as a global `window.INJECTED` */
|
|
Promise.resolve().then(() => (self.INJECTED = 1));
|
|
|
|
const isContentScript = !chrome.tabs;
|
|
|
|
//#region Stuff for content scripts and our extension pages
|
|
|
|
if (!((window.browser || {}).runtime || {}).sendMessage) {
|
|
/* Auto-promisifier with a fallback to direct call on signature error.
|
|
The fallback isn't used now since we call all synchronous methods via `chrome` */
|
|
const directEvents = ['addListener', 'removeListener', 'hasListener', 'hasListeners'];
|
|
// generated by tools/chrome-api-no-cb.js
|
|
const directMethods = {
|
|
alarms: ['create'],
|
|
extension: ['getBackgroundPage', 'getExtensionTabs', 'getURL', 'getViews', 'setUpdateUrlData'],
|
|
i18n: ['getMessage', 'getUILanguage'],
|
|
identity: ['getRedirectURL'],
|
|
runtime: ['connect', 'connectNative', 'getManifest', 'getURL', 'reload', 'restart'],
|
|
tabs: ['connect'],
|
|
};
|
|
const promisify = function (fn, ...args) {
|
|
let res;
|
|
try {
|
|
let resolve, reject;
|
|
/* Some callbacks have 2 parameters so we're resolving as an array in that case.
|
|
For example, chrome.runtime.requestUpdateCheck and chrome.webRequest.onAuthRequired */
|
|
args.push((...results) =>
|
|
chrome.runtime.lastError ?
|
|
reject(new Error(chrome.runtime.lastError.message)) :
|
|
resolve(results.length <= 1 ? results[0] : results));
|
|
fn.apply(this, args);
|
|
res = new Promise((...rr) => ([resolve, reject] = rr));
|
|
} catch (err) {
|
|
if (!err.message.includes('No matching signature')) {
|
|
throw err;
|
|
}
|
|
args.pop();
|
|
res = fn.apply(this, args);
|
|
}
|
|
return res;
|
|
};
|
|
const proxify = (src, srcName, target, key) => {
|
|
let res = src[key];
|
|
if (res && typeof res === 'object') {
|
|
res = createProxy(res, key); // eslint-disable-line no-use-before-define
|
|
} else if (typeof res === 'function') {
|
|
res = (directMethods[srcName] || directEvents).includes(key)
|
|
? res.bind(src)
|
|
: promisify.bind(src, res);
|
|
}
|
|
target[key] = res;
|
|
return res;
|
|
};
|
|
const createProxy = (src, srcName) =>
|
|
new Proxy({}, {
|
|
get(target, key) {
|
|
return target[key] || proxify(src, srcName, target, key);
|
|
},
|
|
});
|
|
window.browser = createProxy(chrome);
|
|
}
|
|
|
|
//#endregion
|
|
//#region AMD loader for content scripts
|
|
|
|
if (isContentScript && typeof define !== 'function') {
|
|
/**
|
|
* WARNING!
|
|
* All deps needed to run the current define() must be already resolved.
|
|
*/
|
|
const modules = {};
|
|
const addJs = name =>
|
|
name.endsWith('.js') ? name : name + '.js';
|
|
const require = self.require = name =>
|
|
(name = addJs(name)) in modules ? modules[name] : {};
|
|
const define = self.define = fn => {
|
|
const name = addJs(define.currentModule || `${fn}`.slice(0, 1000));
|
|
if (!(name in modules)) modules[name] = fn(require);
|
|
define.currentModule = null;
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region AMD loader for our extension pages
|
|
|
|
if (!isContentScript && typeof define !== 'function') {
|
|
/**
|
|
* Notes:
|
|
* 'js!path' and 'path.css' resolve on load automatically.
|
|
* 'resolved!path' will skip this path during dependency auto-detection so use only if
|
|
you need to use a synchronous require() call in the future when this path is
|
|
guaranteed to be resolved elsewhere.
|
|
* js/css of the critical rendering path should still be specified in html.
|
|
*/
|
|
const REQ_SYM = Symbol(Math.random());
|
|
const modules = {};
|
|
const depsQueue = {};
|
|
|
|
const loadElement = ({mode, url}) => {
|
|
const isCss = url.endsWith('.css');
|
|
const tag = isCss ? 'link' : 'script';
|
|
const el = document.createElement(tag);
|
|
if (mode !== 'js' && !isCss) {
|
|
el.src = url;
|
|
return el;
|
|
}
|
|
const args = [url, [], () => el];
|
|
const attr = isCss ? 'href' : 'src';
|
|
const fileName = url.split('/').pop();
|
|
for (const el of document.head.querySelectorAll(`${tag}[${attr}$="${fileName}"]`)) {
|
|
if (location.origin + url === (el.href || el.src || '')) {
|
|
self.define(...args);
|
|
return;
|
|
}
|
|
}
|
|
el.addEventListener('load', self.define.bind(null, ...args), {once: true});
|
|
el[attr] = url;
|
|
if (isCss) el.rel = 'stylesheet';
|
|
return el;
|
|
};
|
|
|
|
const isEmpty = obj => {
|
|
for (const k in obj) return false;
|
|
return true;
|
|
};
|
|
|
|
const parseDepName = (name, base) => {
|
|
let mode;
|
|
let url = name;
|
|
if (name) {
|
|
mode = name.startsWith('js!') ? 'js' : '';
|
|
url = mode ? name.slice(mode.length + 1) : name;
|
|
if (!url.startsWith('/')) {
|
|
url = new URL(url, location.origin + base).pathname;
|
|
}
|
|
if (!url.endsWith('.js') && !url.endsWith('.css')) {
|
|
url += '.js';
|
|
}
|
|
}
|
|
return {mode, url};
|
|
};
|
|
|
|
const require = self.require = function (name) {
|
|
return Array.isArray(name) ?
|
|
self.define.apply([REQ_SYM, this], arguments) :
|
|
modules[parseDepName(name, this).url] || {};
|
|
};
|
|
|
|
const define = self.define = async function (...args) {
|
|
const isReq = this && this[0] === REQ_SYM;
|
|
let base = isReq && this[1];
|
|
let i = 0;
|
|
let name = typeof args[i] === 'string' && args[i++];
|
|
let deps = Array.isArray(args[i]) && args[i++];
|
|
const init = args[i];
|
|
const hasInit = i < args.length;
|
|
if (isReq) {
|
|
name = '';
|
|
define.currentModule = base;
|
|
} else {
|
|
define.currentModule = base = name =
|
|
typeof name === 'string' && name ||
|
|
((document.currentScript || {}).src || '').replace(location.origin, '') ||
|
|
parseDepName(define.currentModule, '').url ||
|
|
'';
|
|
}
|
|
if (name in modules) {
|
|
return modules[name];
|
|
}
|
|
const isInitFn = typeof init === 'function';
|
|
if (!deps) {
|
|
deps = isInitFn ? ['require', 'exports', 'module'] : [];
|
|
let code = isInitFn ? `${init}` : '';
|
|
if (code.includes('require')) {
|
|
code = code.replace(/\/\*([^*]|\*(?!\/))*(\*\/|$)|\/\/[^'"`\r\n]*?require\s*\(.*/g, '');
|
|
const rx = /[^.\w]require\s*\(\s*(['"])(?!resolved!)([^'"]+)\1\s*\)/g;
|
|
for (let m; (m = rx.exec(code));) {
|
|
deps.push(m[2]);
|
|
}
|
|
}
|
|
}
|
|
const exports = {};
|
|
const internals = isInitFn && {
|
|
exports,
|
|
module: {exports},
|
|
require: require.bind(base),
|
|
};
|
|
const needs = {};
|
|
const toLoad = [];
|
|
const ctx = {deps, needs, name, urls: []};
|
|
deps.forEach((depName, i) => {
|
|
let int, mode, url;
|
|
if ((int = internals[depName]) ||
|
|
({mode, url} = parseDepName(depName, base)).url in modules) {
|
|
deps[i] = int || modules[url];
|
|
} else if (needs[url] == null) {
|
|
needs[url] = i;
|
|
ctx.urls.push(url);
|
|
const queue = depsQueue[url];
|
|
if (!queue) toLoad.push({mode, url});
|
|
(queue || (depsQueue[url] = [])).push(ctx);
|
|
}
|
|
});
|
|
let resolvedElsewhere;
|
|
if (!isEmpty(needs)) {
|
|
if (toLoad.length) document.head.append(...toLoad.map(loadElement).filter(Boolean));
|
|
if (name && !depsQueue[name]) depsQueue[name] = [ctx];
|
|
if (!isEmpty(needs)) await new Promise(resolve => (ctx.resolve = resolve));
|
|
resolvedElsewhere = name in modules;
|
|
}
|
|
const result =
|
|
resolvedElsewhere ? modules[name] :
|
|
isInitFn ? init(...deps) :
|
|
hasInit ? init :
|
|
deps[0];
|
|
if (name && !resolvedElsewhere) {
|
|
modules[name] = result;
|
|
const toClear = [name];
|
|
while (toClear.length) {
|
|
const tc = toClear.pop();
|
|
const contexts = depsQueue[tc] || [];
|
|
for (let i = contexts.length, qCtx; i && (qCtx = contexts[--i]);) {
|
|
const {needs} = qCtx;
|
|
qCtx.deps[needs[tc]] = result;
|
|
delete needs[tc];
|
|
delete needs[qCtx.name];
|
|
if (isEmpty(needs)) {
|
|
toClear.push(...qCtx.urls);
|
|
contexts.splice(i, 1);
|
|
if (qCtx.resolve) qCtx.resolve();
|
|
}
|
|
}
|
|
if (!contexts.length) delete depsQueue[tc];
|
|
}
|
|
}
|
|
define.currentModule = null;
|
|
return result;
|
|
};
|
|
|
|
define.amd = {modules, depsQueue};
|
|
}
|
|
|
|
//#endregion
|
|
//#region Stuff for our extension pages
|
|
|
|
if (!isContentScript) {
|
|
if (!(new URLSearchParams({foo: 1})).get('foo')) {
|
|
// TODO: remove when minimum_chrome_version >= 61
|
|
window.URLSearchParams = class extends URLSearchParams {
|
|
constructor(init) {
|
|
if (init && typeof init === 'object') {
|
|
super();
|
|
for (const [key, val] of Object.entries(init)) {
|
|
this.set(key, val);
|
|
}
|
|
} else {
|
|
super(...arguments);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
|
|
define.currentModule = '/js/polyfill';
|
|
define(() => ({
|
|
isEmptyObj(obj) {
|
|
if (obj) {
|
|
for (const k in obj) {
|
|
if (Object.prototype.hasOwnProperty.call(obj, k)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
}));
|
|
}
|