Enhance: promisify chrome into browser, drop promisify (#866)

* promisify `chrome` into `browser`

* comment

* comment

* comment

* Add: a naive browser polyfill

* Fix: polyfill doesn't detect content script env correctly

Co-authored-by: eight04 <eight04@gmail.com>
This commit is contained in:
tophf 2020-08-14 15:16:01 +03:00 committed by GitHub
parent 3d94c641b3
commit 54b1f218e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 153 additions and 162 deletions

View File

@ -1,7 +1,7 @@
/* global download prefs openURL FIREFOX CHROME
URLS ignoreChromeError usercssHelper
styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab createTab activateTab isTabReplaceable getActiveTab
findExistingTab activateTab isTabReplaceable getActiveTab
tabManager */
'use strict';
@ -336,7 +336,7 @@ function openManage({options = false, search} = {}) {
if (isTabReplaceable(tab, url)) {
return activateTab(tab, {url});
}
return createTab({url});
return browser.tabs.create({url});
});
});
}

View File

@ -1,4 +1,4 @@
/* global msg queryTabs ignoreChromeError URLS */
/* global msg ignoreChromeError URLS */
/* exported contentScripts */
'use strict';
@ -55,7 +55,7 @@ const contentScripts = (() => {
}
function injectToAllTabs() {
return queryTabs({}).then(tabs => {
return browser.tabs.query({}).then(tabs => {
for (const tab of tabs) {
// skip unloaded/discarded/chrome tabs
if (!tab.width || tab.discarded || !URLS.supported(tab.url)) continue;

View File

@ -1,46 +1,29 @@
/* global promisify */
/* global chromeLocal */
/* exported createChromeStorageDB */
'use strict';
function createChromeStorageDB() {
const get = promisify(chrome.storage.local.get.bind(chrome.storage.local));
const set = promisify(chrome.storage.local.set.bind(chrome.storage.local));
const remove = promisify(chrome.storage.local.remove.bind(chrome.storage.local));
let INC;
const PREFIX = 'style-';
const METHODS = {
// FIXME: we don't use this method at all. Should we remove this?
get: id => get(PREFIX + id)
.then(result => result[PREFIX + id]),
put: obj => Promise.resolve()
.then(() => {
if (!obj.id) {
return prepareInc()
.then(() => {
get: id => chromeLocal.getValue(PREFIX + id),
put: obj =>
// FIXME: should we clone the object?
obj.id = INC++;
});
}
})
.then(() => set({[PREFIX + obj.id]: obj}))
Promise.resolve(!obj.id && prepareInc().then(() => Object.assign(obj, {id: INC++})))
.then(() => chromeLocal.setValue(PREFIX + obj.id, obj))
.then(() => obj.id),
putMany: items => prepareInc()
.then(() => {
for (const item of items) {
if (!item.id) {
item.id = INC++;
}
}
return set(items.reduce((obj, curr) => {
obj[PREFIX + curr.id] = curr;
return obj;
}, {}));
})
.then(() =>
chromeLocal.set(items.reduce((data, item) => {
if (!item.id) item.id = INC++;
data[PREFIX + item.id] = item;
return data;
}, {})))
.then(() => items.map(i => i.id)),
delete: id => remove(PREFIX + id),
getAll: () => get(null)
delete: id => chromeLocal.remove(PREFIX + id),
getAll: () => chromeLocal.get()
.then(result => {
const output = [];
for (const key in result) {
@ -69,7 +52,7 @@ function createChromeStorageDB() {
function prepareInc() {
if (INC) return Promise.resolve();
return get(null).then(result => {
return chromeLocal.get().then(result => {
INC = 1;
for (const key in result) {
if (key.startsWith(PREFIX)) {

View File

@ -1,4 +1,4 @@
/* global promisify CHROME URLS */
/* global CHROME URLS */
/* exported navigatorUtil */
'use strict';
@ -6,7 +6,6 @@ const navigatorUtil = (() => {
const handler = {
urlChange: null
};
const tabGet = promisify(chrome.tabs.get.bind(chrome.tabs));
return extendNative({onUrlChange});
function onUrlChange(fn) {
@ -48,7 +47,7 @@ const navigatorUtil = (() => {
) {
return Promise.resolve();
}
return tabGet(data.tabId)
return browser.tabs.get(data.tabId)
.then(tab => {
if (tab.url === 'chrome://newtab/') {
data.url = tab.url;

View File

@ -1,9 +1,11 @@
/* global chromeLocal promisify FIREFOX */
/* global chromeLocal promisifyChrome FIREFOX */
/* exported tokenManager */
'use strict';
const tokenManager = (() => {
const launchWebAuthFlow = promisify(chrome.identity.launchWebAuthFlow.bind(chrome.identity));
promisifyChrome({
identity: ['launchWebAuthFlow'],
});
const AUTH = {
dropbox: {
flow: 'token',
@ -158,7 +160,7 @@ const tokenManager = (() => {
Object.assign(query, provider.authQuery);
}
const url = `${provider.authURL}?${stringifyQuery(query)}`;
return launchWebAuthFlow({
return browser.identity.launchWebAuthFlow({
url,
interactive
})

View File

@ -1,8 +1,7 @@
/* global API_METHODS usercss styleManager deepCopy openURL download URLS getTab */
/* exports usercssHelper */
/* global API_METHODS usercss styleManager deepCopy openURL download URLS */
/* exported usercssHelper */
'use strict';
// eslint-disable-next-line no-unused-vars
const usercssHelper = (() => {
const installCodeCache = {};
const clearInstallCode = url => delete installCodeCache[url];
@ -46,7 +45,7 @@ const usercssHelper = (() => {
openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (inTab) {
getTab(tabId).then(tab =>
browser.tabs.get(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,

View File

@ -64,7 +64,6 @@
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
<script src="js/polyfill.js"></script>
<script src="js/promisify.js"></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>

View File

@ -1,5 +1,5 @@
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch
createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch
closeCurrentTab messageBox debounce workerUtil
initBeautifyButton ignoreChromeError
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
@ -226,7 +226,7 @@ function preinit() {
}).observe(document, {subtree: true, childList: true});
if (chrome.windows) {
queryTabs({currentWindow: true}).then(tabs => {
browser.tabs.query({currentWindow: true}).then(tabs => {
const windowId = tabs[0].windowId;
if (prefs.get('openEditInWindow')) {
if (

View File

@ -1,4 +1,4 @@
/* global CodeMirror focusAccessibility colorMimicry editor
/* global CodeMirror focusAccessibility colorMimicry editor chromeLocal
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */
'use strict';
@ -915,7 +915,7 @@ onDOMready().then(() => {
function readStorage() {
chrome.storage.local.get('editor', ({editor = {}}) => {
chromeLocal.getValue('editor').then((editor = {}) => {
state.find = editor.find || '';
state.replace = editor.replace || '';
state.icase = editor.icase || state.icase;
@ -924,14 +924,12 @@ onDOMready().then(() => {
function writeStorage() {
chrome.storage.local.get('editor', ({editor}) =>
chrome.storage.local.set({
editor: Object.assign(editor || {}, {
chromeLocal.getValue('editor').then((editor = {}) =>
chromeLocal.setValue('editor', Object.assign(editor, {
find: state.find,
replace: state.replace,
icase: state.icase,
})
}));
})));
}

View File

@ -1,4 +1,4 @@
/* global showHelp $ $create tryRegExp queryTabs URLS t template openURL */
/* global showHelp $ $create tryRegExp URLS t template openURL */
/* exported regExpTester */
'use strict';
@ -66,7 +66,7 @@ const regExpTester = (() => {
return rxData;
});
const getMatchInfo = m => m && {text: m[0], pos: m.index};
queryTabs({}).then(tabs => {
browser.tabs.query({}).then(tabs => {
const supported = tabs.map(tab => tab.url)
.filter(url => URLS.supported(url));
const unique = [...new Set(supported).values()];

View File

@ -10,7 +10,6 @@
<link href="install-usercss/install-usercss.css" rel="stylesheet">
<script src="js/polyfill.js"></script>
<script src="js/promisify.js"></script>
<script src="js/msg.js"></script>
<script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>

View File

@ -394,13 +394,9 @@
}
});
port.onDisconnect.addListener(() => {
chrome.tabs.get(tabId, tab => {
if (chrome.runtime.lastError) {
closeCurrentTab();
} else if (tab.url === initialUrl) {
location.reload();
}
});
browser.tabs.get(tabId)
.then(tab => tab.url === initialUrl && location.reload())
.catch(closeCurrentTab);
});
return ({timer = true} = {}) => new Promise((resolve, reject) => {
const id = performance.now();

View File

@ -1,7 +1,7 @@
/* exported getTab getActiveTab onTabReady stringAsRegExp openURL ignoreChromeError
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */
/* global promisify */
/* global promisifyChrome */
'use strict';
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(\d+)|$/)[1]);
@ -93,33 +93,20 @@ if (IS_BG) {
// Object.defineProperty(window, 'localStorage', {value: {}});
// Object.defineProperty(window, 'sessionStorage', {value: {}});
const createTab = promisify(chrome.tabs.create.bind(chrome.tabs));
const queryTabs = promisify(chrome.tabs.query.bind(chrome.tabs));
const updateTab = promisify(chrome.tabs.update.bind(chrome.tabs));
const moveTabs = promisify(chrome.tabs.move.bind(chrome.tabs));
// Android doesn't have chrome.windows
const updateWindow = chrome.windows && promisify(chrome.windows.update.bind(chrome.windows));
const createWindow = chrome.windows && promisify(chrome.windows.create.bind(chrome.windows));
promisifyChrome({
tabs: ['create', 'get', 'getCurrent', 'move', 'query', 'update'],
windows: ['create', 'update'], // Android doesn't have chrome.windows
});
// FF57+ supports openerTabId, but not in Android
// (detecting FF57 by the feature it added, not navigator.ua which may be spoofed in about:config)
const openerTabIdSupported = (!FIREFOX || window.AbortController) && chrome.windows != null;
function getTab(id) {
return new Promise(resolve =>
chrome.tabs.get(id, tab =>
!chrome.runtime.lastError && resolve(tab)));
}
function getOwnTab() {
return new Promise(resolve =>
chrome.tabs.getCurrent(tab => resolve(tab)));
return browser.tabs.getCurrent();
}
function getActiveTab() {
return queryTabs({currentWindow: true, active: true})
return browser.tabs.query({currentWindow: true, active: true})
.then(tabs => tabs[0]);
}
@ -140,7 +127,7 @@ function urlToMatchPattern(url, ignoreSearch) {
function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch = false}) {
url = new URL(url);
return queryTabs({url: urlToMatchPattern(url, ignoreSearch), currentWindow})
return browser.tabs.query({url: urlToMatchPattern(url, ignoreSearch), currentWindow})
// FIXME: is tab.url always normalized?
.then(tabs => tabs.find(matchTab));
@ -191,8 +178,8 @@ function openURL({
url: url !== tab.url && url.includes('#') ? url : undefined,
});
}
if (newWindow && createWindow) {
return createWindow(Object.assign({url}, windowPosition))
if (newWindow && browser.windows) {
return browser.windows.create(Object.assign({url}, windowPosition))
.then(wnd => wnd.tabs[0]);
}
return getActiveTab().then((activeTab = {url: ''}) =>
@ -205,7 +192,7 @@ function openURL({
if (id != null && !openerTab.incognito && openerTabIdSupported) {
options.openerTabId = id;
}
return createTab(options);
return browser.tabs.create(options);
}
}
@ -232,9 +219,9 @@ function activateTab(tab, {url, index, openerTabId} = {}) {
options.openerTabId = openerTabId;
}
return Promise.all([
updateTab(tab.id, options),
updateWindow && updateWindow(tab.windowId, {focused: true}),
index != null && moveTabs(tab.id, {index})
browser.tabs.update(tab.id, options),
browser.windows && browser.windows.update(tab.windowId, {focused: true}),
index != null && browser.tabs.move(tab.id, {index})
])
.then(() => tab);
}

View File

@ -1,12 +1,12 @@
/* global promisify deepCopy */
/* global promisifyChrome deepCopy */
// deepCopy is only used if the script is executed in extension pages.
'use strict';
self.msg = self.INJECTED === 1 ? self.msg : (() => {
const runtimeSend = promisify(chrome.runtime.sendMessage.bind(chrome.runtime));
const tabSend = chrome.tabs && promisify(chrome.tabs.sendMessage.bind(chrome.tabs));
const tabQuery = chrome.tabs && promisify(chrome.tabs.query.bind(chrome.tabs));
promisifyChrome({
runtime: ['sendMessage'],
tabs: ['sendMessage', 'query'],
});
const isBg = chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window;
if (isBg) {
window._msg = {
@ -49,19 +49,21 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
}
}
if (chrome.runtime.getBackgroundPage) {
return promisify(chrome.runtime.getBackgroundPage.bind(chrome.runtime))()
.catch(() => null);
promisifyChrome({
runtime: ['getBackgroundPage'],
});
return browser.runtime.getBackgroundPage().catch(() => null);
}
return Promise.resolve(null);
}
function send(data, target = 'extension') {
const message = {data, target};
return runtimeSend(message).then(unwrapData);
return browser.runtime.sendMessage(message).then(unwrapData);
}
function sendTab(tabId, data, options, target = 'tab') {
return tabSend(tabId, {data, target}, options)
return browser.tabs.sendMessage(tabId, {data, target}, options)
.then(unwrapData);
}
@ -99,7 +101,7 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
}
function broadcastTab(data, filter, options, ignoreExtension = false, target = 'tab') {
return tabQuery({})
return browser.tabs.query({})
// TODO: send to activated tabs first?
.then(tabs => {
const requests = [];
@ -123,7 +125,7 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
const message = {data: dataObj, target};
if (tab && tab.id) {
requests.push(
tabSend(tab.id, message, options)
browser.tabs.sendMessage(tab.id, message, options)
.then(unwrapData)
.catch(ignoreError)
);

View File

@ -3,6 +3,8 @@
// eslint-disable-next-line no-unused-expressions
self.INJECTED !== 1 && (() => {
// this part runs in workers, content scripts, our extension pages
if (!Object.entries) {
Object.entries = obj => Object.keys(obj).map(k => [k, obj[k]]);
}
@ -10,9 +12,38 @@ self.INJECTED !== 1 && (() => {
Object.values = obj => Object.keys(obj).map(k => obj[k]);
}
// the above was shared by content scripts and workers,
// the rest is only needed for our extension pages
if (!self.chrome || !self.chrome.tabs) return;
// don't use self.chrome. It is undefined in Firefox
if (typeof chrome !== 'object') return;
// the rest is for content scripts and our extension pages
self.browser = polyfillBrowser();
/* Promisifies the specified `chrome` methods into `browser`.
The definitions is an object like this: {
'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.`
windows: ['create', 'update'], // items and sub-objects will only be created if present in `chrome`
} */
self.promisifyChrome = definitions => {
for (const [scopeName, methods] of Object.entries(definitions)) {
const path = scopeName.split('.');
const src = path.reduce((obj, p) => obj && obj[p], chrome);
if (!src) continue;
const dst = path.reduce((obj, p) => obj[p] || (obj[p] = {}), browser);
for (const name of methods) {
const fn = src[name];
if (!fn || dst[name] && !dst[name].isTrap) continue;
dst[name] = (...args) => new Promise((resolve, reject) =>
fn.call(src, ...args, (...results) =>
chrome.runtime.lastError ?
reject(chrome.runtime.lastError) :
resolve(results.length <= 1 ? results[0] : results)));
// a couple of callbacks have 2 parameters (we don't use those methods, but just in case)
}
}
};
if (!chrome.tabs) return;
// the rest is for our extension pages
if (typeof document === 'object') {
const ELEMENT_METH = {
@ -75,4 +106,27 @@ self.INJECTED !== 1 && (() => {
} catch (err) {
Object.defineProperty(self, 'sessionStorage', {value: {}});
}
function polyfillBrowser() {
if (typeof browser === 'object' && browser.runtime) {
return browser;
}
return createTrap(chrome, null);
function createTrap(base, parent) {
const target = typeof base === 'function' ? () => {} : {};
target.isTrap = true;
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) return target[prop];
if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) {
target[prop] = createTrap(base[prop], base);
return target[prop];
}
return base[prop];
},
apply: (target, thisArg, args) => base.apply(parent, args)
});
}
}
})();

View File

@ -1,4 +1,4 @@
/* global promisify */
/* global promisifyChrome */
'use strict';
self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
@ -107,10 +107,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
specific: new Map(),
};
const syncSet = promisify(chrome.storage.sync.set.bind(chrome.storage.sync));
const syncGet = promisify(chrome.storage.sync.get.bind(chrome.storage.sync));
promisifyChrome({
'storage.sync': ['get', 'set'],
});
const initializing = syncGet('settings')
const initializing = browser.storage.sync.get('settings')
.then(result => {
if (result.settings) {
setAll(result.settings, true);
@ -237,7 +238,7 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
timer = null;
syncSet({settings: values})
browser.storage.sync.set({settings: values})
.then(resolve, reject);
});
});

View File

@ -1,22 +0,0 @@
'use strict';
/*
Convert chrome APIs into promises. Example:
const storageSyncGet = promisify(chrome.storage.sync.get.bind(chrome.storage.sync));
storageSyncGet(['key']).then(result => {...});
*/
self.promisify = self.INJECTED === 1 ? self.promisify : fn =>
(...args) =>
new Promise((resolve, reject) => {
fn(...args, (...result) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve(
result.length === 0 ? undefined :
result.length === 1 ? result[0] : result
);
});
});

View File

@ -1,7 +1,12 @@
/* global loadScript tryJSONparse */
/* global loadScript tryJSONparse promisifyChrome */
/* exported chromeLocal chromeSync */
'use strict';
promisifyChrome({
'storage.local': ['get', 'remove', 'set'],
'storage.sync': ['get', 'remove', 'set'],
});
const [chromeLocal, chromeSync] = (() => {
return [
createWrapper('local'),
@ -9,11 +14,11 @@ const [chromeLocal, chromeSync] = (() => {
];
function createWrapper(name) {
const storage = chrome.storage[name];
const storage = browser.storage[name];
const wrapper = {
get: data => new Promise(resolve => storage.get(data, resolve)),
set: data => new Promise(resolve => storage.set(data, () => resolve(data))),
remove: data => new Promise(resolve => storage.remove(data, resolve)),
get: storage.get.bind(storage),
set: data => storage.set(data).then(() => data),
remove: storage.remove.bind(storage),
/**
* @param {String} key

View File

@ -147,7 +147,6 @@
</template>
<script src="js/polyfill.js"></script>
<script src="js/promisify.js"></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>

View File

@ -26,7 +26,6 @@
"background": {
"scripts": [
"js/polyfill.js",
"js/promisify.js",
"js/messaging.js",
"js/msg.js",
"js/storage-util.js",
@ -77,7 +76,6 @@
"match_about_blank": true,
"js": [
"js/polyfill.js",
"js/promisify.js",
"js/msg.js",
"js/prefs.js",
"content/style-injector.js",

View File

@ -21,7 +21,6 @@
<script src="js/polyfill.js"></script>
<script src="js/dom.js"></script>
<script src="js/promisify.js"></script>
<script src="js/messaging.js"></script>
<script src="js/msg.js"></script>
<script src="js/localization.js"></script>

View File

@ -179,7 +179,6 @@
<script src="manage/config-dialog.js"></script>
<script src="js/polyfill.js"></script>
<script src="js/promisify.js"></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/localization.js"></script>

View File

@ -1,6 +1,6 @@
/* global tabURL handleEvent $ $$ prefs template FIREFOX chromeLocal debounce
$create t API tWordBreak formatDate tryCatch tryJSONparse LZString
ignoreChromeError download */
promisifyChrome download */
'use strict';
window.addEventListener('showStyles:done', function _() {
@ -88,6 +88,9 @@ window.addEventListener('showStyles:done', function _() {
return;
function init() {
promisifyChrome({
'storage.local': ['getBytesInUse'], // FF doesn't implement it
});
setTimeout(() => document.body.classList.add(BODY_CLASS));
$('#find-styles-inline-group').classList.add('hidden');
@ -711,7 +714,7 @@ window.addEventListener('showStyles:done', function _() {
return chromeLocal.loadLZStringScript().then(() =>
tryJSONparse(LZString.decompressFromUTF16(item.payload)));
} else if (item) {
chrome.storage.local.remove(key);
chromeLocal.remove(key);
}
});
}
@ -742,16 +745,8 @@ window.addEventListener('showStyles:done', function _() {
function cleanupCache() {
chromeLocal.remove(CACHE_CLEANUP_NEEDED);
if (chrome.storage.local.getBytesInUse) {
chrome.storage.local.getBytesInUse(null, size => {
if (size > CACHE_SIZE) {
chrome.storage.local.get(null, cleanupCacheInternal);
}
ignoreChromeError();
});
} else {
chrome.storage.local.get(null, cleanupCacheInternal);
}
Promise.resolve(!browser.storage.local.getBytesInUse ? 1e99 : browser.storage.local.getBytesInUse())
.then(size => size > CACHE_SIZE && chromeLocal.get().then(cleanupCacheInternal));
}
function cleanupCacheInternal(storage) {
@ -764,9 +759,8 @@ window.addEventListener('showStyles:done', function _() {
sortedByTime.slice(0, sortedByTime.length / 2);
const toRemove = expired.length ? expired : sortedByTime;
if (toRemove.length) {
chrome.storage.local.remove(toRemove.map(item => item.key), ignoreChromeError);
chromeLocal.remove(toRemove.map(item => item.key));
}
ignoreChromeError();
}
//endregion