stylus/js/polyfill.js
2020-12-17 23:23:17 +03:00

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;
},
}));
}