web-ext
Version:
A command line tool to help build, run, and test web extensions
299 lines (278 loc) • 8.84 kB
JavaScript
import readline from 'readline';
import { WebExtError } from '../errors.js';
import { createLogger } from '../util/logger.js';
import { createFileFilter as defaultFileFilterCreator } from '../util/file-filter.js';
import { isTTY, setRawMode } from '../util/stdin.js';
import defaultSourceWatcher from '../watcher.js';
const log = createLogger(import.meta.url);
export async function createExtensionRunner(config) {
switch (config.target) {
case 'firefox-desktop':
{
const {
FirefoxDesktopExtensionRunner
} = await import('./firefox-desktop.js');
return new FirefoxDesktopExtensionRunner(config.params);
}
case 'firefox-android':
{
const {
FirefoxAndroidExtensionRunner
} = await import('./firefox-android.js');
return new FirefoxAndroidExtensionRunner(config.params);
}
case 'chromium':
{
const {
ChromiumExtensionRunner
} = await import('./chromium.js');
return new ChromiumExtensionRunner(config.params);
}
default:
throw new WebExtError(`Unknown target: "${config.target}"`);
}
}
/**
* Implements an IExtensionRunner which allow the caller to
* manage multiple extension runners at the same time (e.g. by running
* a Firefox Desktop instance alongside to a Firefox for Android instance).
*/
export class MultiExtensionRunner {
extensionRunners;
desktopNotifications;
constructor(params) {
this.extensionRunners = params.runners;
this.desktopNotifications = params.desktopNotifications;
}
// Method exported from the IExtensionRunner interface.
/**
* Returns the runner name.
*/
getName() {
return 'Multi Extension Runner';
}
/**
* Call the `run` method on all the managed extension runners,
* and awaits that all the runners has been successfully started.
*/
async run() {
const promises = [];
for (const runner of this.extensionRunners) {
promises.push(runner.run());
}
await Promise.all(promises);
}
/**
* Reloads all the extensions on all the managed extension runners,
* collect any reload error, and resolves to an array composed by
* a ExtensionRunnerReloadResult object per managed runner.
*
* Any detected reload error is also logged on the terminal and shows as a
* desktop notification.
*/
async reloadAllExtensions() {
log.debug('Reloading all reloadable add-ons');
const promises = [];
for (const runner of this.extensionRunners) {
const reloadPromise = runner.reloadAllExtensions().then(() => {
return {
runnerName: runner.getName()
};
}, error => {
return {
runnerName: runner.getName(),
reloadError: error
};
});
promises.push(reloadPromise);
}
return await Promise.all(promises).then(results => {
this.handleReloadResults(results);
return results;
});
}
/**
* Reloads a single extension on all the managed extension runners,
* collect any reload error and resolves to an array composed by
* a ExtensionRunnerReloadResult object per managed runner.
*
* Any detected reload error is also logged on the terminal and shows as a
* desktop notification.
*/
async reloadExtensionBySourceDir(sourceDir) {
log.debug(`Reloading add-on at ${sourceDir}`);
const promises = [];
for (const runner of this.extensionRunners) {
const reloadPromise = runner.reloadExtensionBySourceDir(sourceDir).then(() => {
return {
runnerName: runner.getName(),
sourceDir
};
}, error => {
return {
runnerName: runner.getName(),
reloadError: error,
sourceDir
};
});
promises.push(reloadPromise);
}
return await Promise.all(promises).then(results => {
this.handleReloadResults(results);
return results;
});
}
/**
* Register a callback to be called when all the managed runners has been exited.
*/
registerCleanup(cleanupCallback) {
const promises = [];
// Create a promise for every extension runner managed by this instance,
// the promise will be resolved when the particular runner calls its
// registered cleanup callbacks.
for (const runner of this.extensionRunners) {
promises.push(new Promise(resolve => {
runner.registerCleanup(resolve);
}));
}
// Wait for all the created promises to be resolved or rejected
// (once each one of the runners has cleaned up) and then call
// the cleanup callback registered to this runner.
Promise.all(promises).then(cleanupCallback, cleanupCallback);
}
/**
* Exits all the managed runner has been exited.
*/
async exit() {
const promises = [];
for (const runner of this.extensionRunners) {
promises.push(runner.exit());
}
await Promise.all(promises);
}
// Private helper methods.
handleReloadResults(results) {
for (const {
runnerName,
reloadError,
sourceDir
} of results) {
if (reloadError instanceof Error) {
let message = 'Error occurred while reloading';
if (sourceDir) {
message += ` "${sourceDir}" `;
}
message += `on "${runnerName}" - ${reloadError.message}`;
log.error(`\n${message}`);
log.debug(reloadError.stack);
this.desktopNotifications({
title: 'web-ext run: extension reload error',
message
});
}
}
}
}
// defaultWatcherCreator types and implementation.
export function defaultWatcherCreator({
reloadExtension,
sourceDir,
watchFile,
watchIgnored,
artifactsDir,
ignoreFiles,
onSourceChange = defaultSourceWatcher,
createFileFilter = defaultFileFilterCreator
}) {
const fileFilter = createFileFilter({
sourceDir,
artifactsDir,
ignoreFiles
});
return onSourceChange({
sourceDir,
watchFile,
watchIgnored,
artifactsDir,
onChange: () => reloadExtension(sourceDir),
shouldWatchFile: file => fileFilter.wantFile(file)
});
}
// defaultReloadStrategy types and implementation.
export function defaultReloadStrategy({
artifactsDir,
extensionRunner,
ignoreFiles,
noInput = false,
sourceDir,
watchFile,
watchIgnored
}, {
createWatcher = defaultWatcherCreator,
stdin = process.stdin,
kill = process.kill
} = {}) {
const allowInput = !noInput;
if (!allowInput) {
log.debug('Input has been disabled because of noInput==true');
}
const watcher = createWatcher({
reloadExtension: watchedSourceDir => {
extensionRunner.reloadExtensionBySourceDir(watchedSourceDir);
},
sourceDir,
watchFile,
watchIgnored,
artifactsDir,
ignoreFiles
});
extensionRunner.registerCleanup(() => {
watcher.close();
if (allowInput) {
if (isTTY(stdin)) {
setRawMode(stdin, false);
}
stdin.pause();
}
});
if (allowInput && isTTY(stdin)) {
readline.emitKeypressEvents(stdin);
setRawMode(stdin, true);
const keypressUsageInfo = 'Press R to reload (and Ctrl-C to quit)';
// NOTE: this `Promise.resolve().then(...)` is basically used to spawn a "co-routine"
// that is executed before the callback attached to the Promise returned by this function
// (and it allows the `run` function to not be stuck in the while loop).
Promise.resolve().then(async function () {
log.info(keypressUsageInfo);
let userExit = false;
while (!userExit) {
const keyPressed = await new Promise(resolve => {
stdin.once('keypress', (str, key) => resolve(key));
});
if (keyPressed.ctrl && keyPressed.name === 'c') {
userExit = true;
} else if (keyPressed.name === 'z') {
// Prepare to suspend.
// NOTE: Switch the raw mode off before suspending (needed to make the keypress event
// to work correctly when the nodejs process is resumed).
setRawMode(stdin, false);
log.info('\nweb-ext has been suspended on user request');
kill(process.pid, 'SIGTSTP');
// Prepare to resume.
log.info(`\nweb-ext has been resumed. ${keypressUsageInfo}`);
// Switch the raw mode on on resume.
setRawMode(stdin, true);
} else if (keyPressed.name === 'r') {
log.debug('Reloading installed extensions on user request');
await extensionRunner.reloadAllExtensions().catch(err => {
log.warn(`\nError reloading extension: ${err}`);
log.debug(`Reloading extension error stack: ${err.stack}`);
});
}
}
log.info('\nExiting web-ext on user request');
extensionRunner.exit();
});
}
}
//# sourceMappingURL=index.js.map