Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds run command -> install and launch extension #90

Merged
merged 1 commit into from
Mar 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ log.file=./artifacts/flow.log
# dependencies. Ignoring these speeds up the standalone flow command by 2x.
#
# DO NOT EDIT: ignored-dependencies: this list is auto-generated by a grunt task
# 9877100539d1b841afc8ba07fc895ed11ce2f9d7ed224780cd9ff3bd30887568
# 181c34cd94fd821760c6050e5e2c75cdc97a8a7bbcfd48b92c462edc58c63734
.*/node_modules/.bin.*
.*/node_modules/Base64.*
.*/node_modules/JSV.*
Expand Down Expand Up @@ -282,7 +282,6 @@ log.file=./artifacts/flow.log
.*/node_modules/find-up.*
.*/node_modules/find-versions.*
.*/node_modules/findup-sync.*
.*/node_modules/firefox-profile.*
.*/node_modules/first-chunk-stream.*
.*/node_modules/flat-cache.*
.*/node_modules/flow-bin.*
Expand All @@ -297,7 +296,6 @@ log.file=./artifacts/flow.log
.*/node_modules/fs-promise.*
.*/node_modules/fs-readdir-recursive.*
.*/node_modules/fsevents.*
.*/node_modules/fx-runner.*
.*/node_modules/gaze.*
.*/node_modules/generate-function.*
.*/node_modules/generate-object-property.*
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
"babel-polyfill": "6.6.1",
"es6-error": "2.0.2",
"es6-promisify": "3.0.0",
"firefox-profile": "0.3.11",
"fx-runner": "1.0.1",
"stream-to-promise": "1.1.0",
"tmp": "0.0.28",
"yargs": "4.2.0",
Expand Down
4 changes: 3 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path';
import {readFileSync} from 'fs';

import build from './cmd/build';
import run from './cmd/run';
import {Program} from './program';


Expand Down Expand Up @@ -39,7 +40,8 @@ export function main() {
});

program
.command('build', 'Create a web extension package from source', build);
.command('build', 'Create a web extension package from source', build)
.command('run', 'Run the web extension', run);

program.run();
}
35 changes: 35 additions & 0 deletions src/cmd/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* @flow */
import buildExtension from './build';
import {ProgramOptions} from '../program';
import * as defaultFirefox from '../firefox';
import {withTempDir} from '../util/temp-dir';
import getValidatedManifest from '../util/manifest';


export default function run(
{sourceDir}: ProgramOptions,
{firefox=defaultFirefox}: Object = {}): Promise {

console.log(`Running web extension from ${sourceDir}`);

return getValidatedManifest(sourceDir)
.then((manifestData) => withTempDir(
(tmpDir) =>
Promise.all([
buildExtension({sourceDir, buildDir: tmpDir.path()},
{manifestData}),
firefox.createProfile(),
])
.then((result) => {
let [buildResult, profile] = result;
return firefox.installExtension(
{
manifestData,
extensionPath: buildResult.extensionPath,
profile,
})
.then(() => profile);
})
.then((profile) => firefox.run(profile))
));
}
133 changes: 133 additions & 0 deletions src/firefox/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* @flow */
import nodeFs from 'fs';
import path from 'path';
import defaultFxRunner from 'fx-runner/lib/run';
import FirefoxProfile from 'firefox-profile';
import streamToPromise from 'stream-to-promise';

import * as fs from '../util/promised-fs';
import {onlyErrorsWithCode, WebExtError} from '../errors';
import {getPrefs as defaultPrefGetter} from './preferences';


export const defaultFirefoxEnv = {
XPCOM_DEBUG_BREAK: 'stack',
NS_TRACE_MALLOC_DISABLE_STACKS: '1',
};

export function run(
profile: FirefoxProfile, {fxRunner=defaultFxRunner}: Object = {}): Promise {

console.log(`Running Firefox with profile at ${profile.path()}`);
return fxRunner(
{
'binary': null,
'binary-args': null,
'no-remote': true,
'foreground': true,
'profile': profile.path(),
'env': {
...process.env,
...defaultFirefoxEnv,
},
'verbose': true,
})
.then((results) => {
return new Promise((resolve) => {
let firefox = results.process;

console.log(`Executing Firefox binary: ${results.binary}`);
console.log(`Executing Firefox with args: ${results.args.join(' ')}`);

firefox.on('error', (error) => {
// TODO: show a nice error when it can't find Firefox.
// if (/No such file/.test(err) || err.code === 'ENOENT') {
console.log(`Firefox error: ${error}`);
throw error;
});

firefox.stderr.on('data', (data) => {
console.error(`stderr: ${data.toString().trim()}`);
});

firefox.stdout.on('data', function(data) {
console.log(`stdout: ${data.toString().trim()}`);
});

firefox.on('close', () => {
console.log('Firefox closed');
resolve();
});
});
});
}


export function createProfile(
app: string = 'firefox',
{getPrefs=defaultPrefGetter}: Object = {}): Promise {

return new Promise((resolve) => {
// The profile is created in a self-destructing temp dir.
// TODO: add option to copy a profile.
// https://github.com/mozilla/web-ext/issues/69
let profile = new FirefoxProfile();

// Set default preferences.
// TODO: support custom preferences.
// https://github.com/mozilla/web-ext/issues/88
let prefs = getPrefs(app);
Object.keys(prefs).forEach((pref) => {
profile.setPreference(pref, prefs[pref]);
});
profile.updatePreferences();

resolve(profile);
});
}


class InstallationConfig {
manifestData: Object;
profile: FirefoxProfile;
extensionPath: string;
}

export function installExtension(
{manifestData, profile, extensionPath}: InstallationConfig): Promise {

// This more or less follows
// https://github.com/saadtazi/firefox-profile-js/blob/master/lib/firefox_profile.js#L531
// (which is broken for web extensions).
// TODO: maybe uplift a patch that supports web extensions instead?

return new Promise(
(resolve) => {
if (!profile.extensionsDir) {
throw new WebExtError('profile.extensionsDir was unexpectedly empty');
}
resolve(fs.stat(profile.extensionsDir));
})
.catch(onlyErrorsWithCode('ENOENT', () => {
console.log(`Creating extensions directory: ${profile.extensionsDir}`);
return fs.mkdir(profile.extensionsDir);
}))
.then(() => {
let readStream = nodeFs.createReadStream(extensionPath);
let id = manifestData.applications.gecko.id;

// TODO: also support copying a directory of code to this
// destination. That is, to name the directory ${id}.
// https://github.com/mozilla/web-ext/issues/70
let destPath = path.join(profile.extensionsDir, `${id}.xpi`);
let writeStream = nodeFs.createWriteStream(destPath);

console.log(`Copying ${extensionPath} to ${destPath}`);
readStream.pipe(writeStream);

return Promise.all([
streamToPromise(readStream),
streamToPromise(writeStream),
]);
});
}
90 changes: 90 additions & 0 deletions src/firefox/preferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* @flow */
import {WebExtError} from '../errors';


export function getPrefs(app: string = 'firefox'): Object {
let appPrefs = prefs[app];
if (!appPrefs) {
throw new WebExtError(`Unsupported application: ${app}`);
}
return {
...prefs.common,
...appPrefs,
};
}


var prefs = {};

prefs.common = {
// Allow debug output via dump to be printed to the system console
'browser.dom.window.dump.enabled': true,
// Warn about possibly incorrect code.
'javascript.options.strict': true,
'javascript.options.showInConsole': true,

// Allow remote connections to the debugger.
'devtools.debugger.remote-enabled' : true,

// Turn off platform logging because it is a lot of info.
'extensions.logging.enabled': false,

// Disable extension updates and notifications.
'extensions.checkCompatibility.nightly' : false,
'extensions.update.enabled' : false,
'extensions.update.notifyUser' : false,

// From:
// http://hg.mozilla.org/mozilla-central/file/1dd81c324ac7/build/automation.py.in//l372
// Only load extensions from the application and user profile.
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
'extensions.enabledScopes' : 5,
// Disable metadata caching for installed add-ons by default.
'extensions.getAddons.cache.enabled' : false,
// Disable intalling any distribution add-ons.
'extensions.installDistroAddons' : false,
// Allow installing extensions dropped into the profile folder.
'extensions.autoDisableScopes' : 10,

// Disable app update.
'app.update.enabled' : false,

// Point update checks to a nonexistent local URL for fast failures.
'extensions.update.url': 'http://localhost/extensions-dummy/updateURL',
'extensions.blocklist.url':
'http://localhost/extensions-dummy/blocklistURL',

// Make sure opening about:addons won't hit the network.
'extensions.webservice.discoverURL' :
'http://localhost/extensions-dummy/discoveryURL',

// Allow unsigned add-ons.
'xpinstall.signatures.required' : false,
};

// Prefs specific to Firefox for Android.
prefs.fennec = {
'browser.console.showInPanel': true,
'browser.firstrun.show.uidiscovery': false,
};

// Prefs specific to Firefox for desktop.
prefs.firefox = {
'browser.startup.homepage' : 'about:blank',
'startup.homepage_welcome_url' : 'about:blank',
'startup.homepage_welcome_url.additional' : '',
'devtools.errorconsole.enabled' : true,
'devtools.chrome.enabled' : true,

// From:
// http://hg.mozilla.org/mozilla-central/file/1dd81c324ac7/build/automation.py.in//l388
// Make url-classifier updates so rare that they won't affect tests.
'urlclassifier.updateinterval' : 172800,
// Point the url-classifier to a nonexistent local URL for fast failures.
'browser.safebrowsing.provider.0.gethashURL' :
'http://localhost/safebrowsing-dummy/gethash',
'browser.safebrowsing.provider.0.keyURL' :
'http://localhost/safebrowsing-dummy/newkey',
'browser.safebrowsing.provider.0.updateURL' :
'http://localhost/safebrowsing-dummy/update',
};
1 change: 1 addition & 0 deletions src/util/promised-fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import {promisify} from './es6-modules';

export const stat = promisify(fs.stat);
export const mkdir = promisify(fs.mkdir);
export const readdir = promisify(fs.readdir);
export const readFile = promisify(fs.readFile);
export const writeFile = promisify(fs.writeFile);
Binary file added tests/fixtures/minimal_extension-1.0.xpi
Binary file not shown.
60 changes: 60 additions & 0 deletions tests/helpers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'path';
import sinon from 'sinon';
import {promisify} from '../src/util/es6-modules';
import yauzl from 'yauzl';

Expand Down Expand Up @@ -72,3 +73,62 @@ export function makeSureItFails() {
throw new Error('This test unexpectedly succeeded without an error');
};
}


/*
* Return a fake version of an object for testing.
*
* The fake object will contain stub implementations of
* all original methods. Each method will be wrapped in
* a sinon.spy() for inspection.
*
* You can optionally provide implementations for one or
* more methods.
*
* Unlike similar sinon helpers, this *does not* touch the
* original object so there is no need to tear down any
* patches afterwards.
*
* Usage:
*
* let fakeProcess = fake(process, {
* cwd: () => '/some/directory',
* });
*
* // Use the object in real code:
* fakeProcess.cwd();
*
* // Make assertions about methods that
* // were on the original object:
* assert.equal(fakeProcess.exit.called, true);
*
*/
export function fake(original, methods={}) {
var stub = {};

// Provide stubs for all original members:
Object.keys(original).forEach((key) => {
if (typeof original[key] === 'function') {
stub[key] = () => {
console.warn(
`Running stubbed function ${key} (default implementation)`);
};
}
});

// Provide custom implementations, if necessary.
Object.keys(methods).forEach((key) => {
if (!original[key]) {
throw new Error(
`Cannot define method "${key}"; it does not exist on the original`);
}
stub[key] = methods[key];
});

// Wrap all implementations in spies.
Object.keys(stub).forEach((key) => {
stub[key] = sinon.spy(stub[key]);
});

return stub;
}
Loading