This drop-in module does one simple thing: it allows you to keep the extension tabs of your WebExtension open when you reload it during development.
Pleas read the README for more information.
This drop-in module does one simple thing: it allows you to keep the extension tabs of your WebExtension open when you reload it during development.
Pleas read the README for more information.
This drop-in module does one simple thing: it allows you to keep the extension tabs of your WebExtension open when you reload it during development.
It is not possible to catch the 'unload'
event of the background page (in Firefox 65), so this module (only) hooks into the browser.runtime.reload()
method, which means that it will only handle reloads initiated by calls to that method, and not reloads caused by clicking Reload
on about:debugging#addons
or updating/re-installing the extension. I'd suggest you put a button that invokes that function on your extension pages (which you want to have open anyway) or add a context menu option (see below).
I was reminded of this problem and decided to use it as a quick exercise. Is it Worth the Time is already giving me a clear no, so here is the solution I came up with in about two hours:
This probably requires the 'tabs'
permission, and to remove the temporary tabs from the history, it (optionally) requires the 'sessions'
permission (which you might want to only add to development builds).
This module is available via NPM, so do npm install keep-tabs-open
in a terminal in your project to install.
Now include the file node_modules/keep-tabs-open/index.js
in your extension (web-ext
might mess with this) and load it as a background script (however you do that with your other scripts).
If you use an AMD loader (like require.js
) the script will define an anonymous module, otherwise it exposes itself as a keepExtTabsOpen
global function.
Either way, you need to call that function with extension specific options (all optional) to activate it:
iconUrl
: favicon of the temporary reload tab.title
: HTML title of the temporary reload tab.message
: HTML message to display on the temporary reload tab.browser
/chrome
: APIs to use and patch. Defaults to the respective globals. Doesn't need Promise capability, prefers to use browser
.E.g.:
background/index.js
:
const manifest = browser.runtime.getManifest();
require([ 'node_modules/keep-tabs-open/index', ], keepExtTabsOpen => {
keepExtTabsOpen({ browser: global.browser, iconUrl: '/icon.png',
title: 'Reloading: '+ manifest.name, message: `
<style> :root { background: #424F5A; filter: invert(1) hue-rotate(180deg); font-family: Segoe UI, Tahoma, sans-serif; } </style>
<h1>Reloading ${manifest.name}</h1><p>This tab should close in a few seconds ...</p>
`,
}).catch(console.error);
});
If you have the 'menus'
or 'contextMenus'
permission, you can also add this to make reloading the extension more convenient.
const browser = window.browser || window.chrome;
const Menus = browser.menus || browser.contextMenus; if (Menus) {
Menus.create({ contexts: [ 'browser_action', ], id: 'extension:restart', title: 'Restart Extension', });
Menus.onClicked.addListener(({ menuItemId, }) => { menuItemId === 'extension:restart' && browser.runtime.reload(); });
}
(function(global) { 'use strict'; const factory = function keepExtTabsOpen(exports) { return async options => { // 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/. | |
// TODO: test uncommitted changes! | |
const { browser = global.browser, chrome = global.chrome, iconUrl, title, message, } = options, api = browser || chrome; | |
/** | |
* save open tabs (when reloading) | |
*/ | |
const doReload = api.runtime.reload; | |
chrome && Object.defineProperty(chrome.runtime, 'reload', { value: reload, }); | |
browser && Object.defineProperty(browser.runtime, 'reload', { value: reload, }); | |
async function reload() { | |
const notice = URL.createObjectURL(new global.Blob([ ` | |
<!DOCTYPE html> | |
<html><head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta name="viewport" content="initial-scale=1, maximum-scale=1.0, user-scalable=no"> | |
<title>${title}</title><link rel="icon" href="${ iconUrl && (await fetchB64(iconUrl)) }"> | |
</head><body> | |
${ message || '<h1>Extension is reloading ...</h1>' } | |
</body></html> | |
`.trim(), ], { type: 'text/html', })) + suffix; | |
const tabs = (await call(api.tabs, 'query', { url: api.runtime.getURL('*'), discarded: false, })); | |
(await Promise.all(tabs.filter(_=>_.active).map(({ windowId, index, }) => call(api.tabs, 'create', { windowId, index: index + 1, url: notice, })))); | |
// (await Promise.all(tabs.map(tab => call(api.tabs, 'discard', tab.id)))); | |
tabs.map(tab => api.tabs.discard(tab.id)); // firefox does not allow a callback here | |
setTimeout(doReload, 300); | |
} | |
/** | |
* restore previous tabs | |
*/ | |
const prefix = 'blob:'+ api.runtime.getURL(''), suffix = '#reloading'; | |
const tabs = (await call(api.tabs, 'query', { url: api.runtime.getURL('*'), })); // BUG[FF65]: extension tabs that are being undiscarded would be missed by this query -.- | |
(await Promise.all(tabs.filter(_=>_.discarded).map(tab => call(api.tabs, 'reload', tab.id)))); | |
(await new Promise(wake => setTimeout(wake, 1000))); | |
for (const { id, windowId, index, } of tabs) { // sequential to avoid race conditions with the sessions API | |
const active = (await call(api.tabs, 'query', { windowId, index: index + 1, }))[0]; | |
if (active && active.url && active.url.startsWith(prefix) && active.url.endsWith(suffix)) { | |
(await call(api.tabs, 'update', id, { active: true, })); | |
(await call(api.tabs, 'remove', active.id)); | |
const session = api.sessions && api.sessions.getRecentlyClosed && api.sessions.forgetClosedTab | |
&& (await call(api.sessions, 'getRecentlyClosed', { maxResults: 1, }))[0]; | |
session && session.tab && api.sessions.forgetClosedTab(session.tab.windowId, session.tab.sessionId); | |
} | |
} | |
/** | |
* utils | |
*/ | |
function fetchB64(url) { return global.fetch(url).then(_=>_.blob()).then(blob => new Promise((y, n) => { const r = new global.FileReader; r.onerror = n; r.onload = () => y(r.result); r.readAsDataURL(blob); })); } | |
function call(api, method, ...args) { return new Promise((y, n) => api[method](...args, v => api.runtime.lastError ? n(api.runtime.lastError) : y(v))); } | |
}; }; if (typeof define === 'function' /* global define */ && define.amd) { define([ 'exports', ], factory); } else { const exp = { }, result = factory(exp) || exp; if (typeof exports === 'object' && typeof module === 'object') { /* eslint-disable */ module.exports = result; /* eslint-enable */ } else { global[factory.name] = result; } } })(this); |
{ | |
"name": "keep-tabs-open", | |
"version": "1.0.2", | |
"description": "npm + AMD module that prevents Firefox from closing WebExtension tabs during development", | |
"keywords": [ "Firefox", "WebExtension", "tabs", "close", "development" ], | |
"author": "NiklasGollenstede", | |
"license": "MPL-2.0", | |
"repo": "gist:63a6099d97e82ffe0cc064d4d4d82b62", | |
"homepage": "https://gist.github.com/NiklasGollenstede/63a6099d97e82ffe0cc064d4d4d82b62#file-readme-md" | |
} |