Skip to content

Commit 61c5087

Browse files
committed
feat: run watches source, reloads addon in Firefox
1 parent 47fb0ac commit 61c5087

File tree

8 files changed

+513
-78
lines changed

8 files changed

+513
-78
lines changed

src/cmd/run.js

Lines changed: 170 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,186 @@
11
/* @flow */
22
import buildExtension from './build';
33
import * as defaultFirefox from '../firefox';
4+
import defaultFirefoxConnector from '../firefox/remote';
5+
import {onlyErrorsWithCode} from '../errors';
46
import {withTempDir} from '../util/temp-dir';
57
import {createLogger} from '../util/logger';
68
import getValidatedManifest from '../util/manifest';
9+
import defaultSourceWatcher from '../watcher';
710

811
const log = createLogger(__filename);
912

1013

14+
export function defaultWatcherCreator(
15+
{profile, client, sourceDir, artifactsDir, createRunner,
16+
onSourceChange=defaultSourceWatcher}: Object): Object {
17+
return onSourceChange({
18+
sourceDir, artifactsDir, onChange: () => createRunner(
19+
(runner) => runner.buildExtension()
20+
.then((buildResult) => runner.install(buildResult, {profile}))
21+
.then(() => {
22+
log.debug('Attempting to reload extension');
23+
const addonId = runner.manifestData.applications.gecko.id;
24+
log.debug(`Reloading add-on ID ${addonId}`);
25+
return client.reloadAddon(addonId);
26+
})
27+
.catch((error) => {
28+
log.error(error.stack);
29+
throw error;
30+
})
31+
),
32+
});
33+
}
34+
35+
36+
export function defaultReloadStrategy(
37+
{firefox, profile, sourceDir, artifactsDir, createRunner}: Object,
38+
{connectToFirefox=defaultFirefoxConnector,
39+
maxRetries=25, retryInterval=120,
40+
createWatcher=defaultWatcherCreator}: Object = {}): Promise {
41+
var watcher;
42+
var client;
43+
var retries = 0;
44+
45+
firefox.on('close', () => {
46+
if (client) {
47+
client.disconnect();
48+
}
49+
if (watcher) {
50+
watcher.close();
51+
}
52+
});
53+
54+
function establishConnection() {
55+
return new Promise((resolve, reject) => {
56+
connectToFirefox()
57+
.then((connectedClient) => {
58+
log.debug('Connected to the Firefox debugger');
59+
client = connectedClient;
60+
watcher = createWatcher({
61+
profile, client, sourceDir, artifactsDir, createRunner,
62+
});
63+
resolve();
64+
})
65+
.catch(onlyErrorsWithCode('ECONNREFUSED', (error) => {
66+
if (retries >= maxRetries) {
67+
log.debug('Connect to Firefox debugger: too many retries');
68+
throw error;
69+
} else {
70+
setTimeout(() => {
71+
retries ++;
72+
log.debug(
73+
`Retrying Firefox (${retries}); connection error: ${error}`);
74+
resolve(establishConnection());
75+
}, retryInterval);
76+
}
77+
}))
78+
.catch((error) => {
79+
log.error(error.stack);
80+
reject(error);
81+
});
82+
});
83+
}
84+
85+
return establishConnection();
86+
}
87+
88+
1189
export default function run(
12-
{sourceDir, firefoxBinary, firefoxProfile}: Object,
13-
{firefox=defaultFirefox}: Object = {}): Promise {
90+
{sourceDir, artifactsDir, firefoxBinary, firefoxProfile, noReload}: Object,
91+
{firefox=defaultFirefox, reloadStrategy=defaultReloadStrategy}
92+
: Object = {}): Promise {
1493

1594
log.info(`Running web extension from ${sourceDir}`);
1695

17-
return getValidatedManifest(sourceDir)
18-
.then((manifestData) => withTempDir(
19-
(tmpDir) =>
20-
Promise.all([
21-
buildExtension({sourceDir, artifactsDir: tmpDir.path()},
22-
{manifestData}),
23-
new Promise((resolve) => {
24-
if (firefoxProfile) {
25-
log.debug(`Copying Firefox profile from ${firefoxProfile}`);
26-
resolve(firefox.copyProfile(firefoxProfile));
27-
} else {
28-
log.debug('Creating new Firefox profile');
29-
resolve(firefox.createProfile());
30-
}
31-
}),
32-
])
33-
.then((result) => {
34-
let [buildResult, profile] = result;
35-
return firefox.installExtension(
36-
{
37-
manifestData,
38-
extensionPath: buildResult.extensionPath,
39-
profile,
40-
})
41-
.then(() => profile);
96+
function createRunner(callback) {
97+
return getValidatedManifest(sourceDir)
98+
.then((manifestData) => withTempDir(
99+
(tmpDir) => {
100+
const runner = new ExtensionRunner({
101+
sourceDir,
102+
firefox,
103+
firefoxBinary,
104+
tmpDirPath: tmpDir.path(),
105+
manifestData,
106+
firefoxProfile,
107+
});
108+
return callback(runner);
109+
}
110+
));
111+
}
112+
113+
return createRunner(
114+
(runner) => runner.buildExtension()
115+
.then((buildResult) => runner.install(buildResult))
116+
.then((profile) => runner.run(profile).then((firefox) => {
117+
return {firefox, profile};
118+
}))
119+
.then(({firefox, profile}) => {
120+
if (noReload) {
121+
log.debug('Extension auto-reloading has been disabled');
122+
} else {
123+
log.debug('Reloading extension when the source changes');
124+
reloadStrategy(
125+
{firefox, profile, sourceDir, artifactsDir, createRunner});
126+
}
127+
return firefox;
128+
})
129+
);
130+
}
131+
132+
133+
export class ExtensionRunner {
134+
sourceDir: string;
135+
tmpDirPath: string;
136+
manifestData: Object;
137+
firefoxProfile: Object;
138+
firefox: Object;
139+
firefoxBinary: string;
140+
141+
constructor({firefox, sourceDir, tmpDirPath, manifestData,
142+
firefoxProfile, firefoxBinary}: Object) {
143+
this.sourceDir = sourceDir;
144+
this.tmpDirPath = tmpDirPath;
145+
this.manifestData = manifestData;
146+
this.firefoxProfile = firefoxProfile;
147+
this.firefox = firefox;
148+
this.firefoxBinary = firefoxBinary;
149+
}
150+
151+
buildExtension(): Promise {
152+
const {sourceDir, tmpDirPath, manifestData} = this;
153+
return buildExtension({sourceDir, artifactsDir: tmpDirPath},
154+
{manifestData});
155+
}
156+
157+
getProfile(): Promise {
158+
const {firefox, firefoxProfile} = this;
159+
return new Promise((resolve) => {
160+
if (firefoxProfile) {
161+
log.debug(`Copying Firefox profile from ${firefoxProfile}`);
162+
resolve(firefox.copyProfile(firefoxProfile));
163+
} else {
164+
log.debug('Creating new Firefox profile');
165+
resolve(firefox.createProfile());
166+
}
167+
});
168+
}
169+
170+
install(buildResult: Object, {profile}: Object = {}): Promise {
171+
const {firefox, manifestData} = this;
172+
return Promise.resolve(profile ? profile : this.getProfile())
173+
.then((profile) => firefox.installExtension(
174+
{
175+
manifestData,
176+
extensionPath: buildResult.extensionPath,
177+
profile,
42178
})
43-
.then((profile) => firefox.run(profile, {firefoxBinary}))
44-
));
179+
.then(() => profile));
180+
}
181+
182+
run(profile: Object): Promise {
183+
const {firefox, firefoxBinary} = this;
184+
return firefox.run(profile, {firefoxBinary});
185+
}
45186
}

src/firefox/index.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ export const defaultFirefoxEnv = {
2626
*/
2727
export function run(
2828
profile: FirefoxProfile,
29-
{fxRunner=defaultFxRunner, firefoxBinary}: Object = {}): Promise {
29+
{fxRunner=defaultFxRunner, firefoxBinary, binaryArgs}
30+
: Object = {}): Promise {
3031

3132
log.info(`Running Firefox with profile at ${profile.path()}`);
3233
return fxRunner(
3334
{
3435
// if this is falsey, fxRunner tries to find the default one.
3536
'binary': firefoxBinary,
36-
'binary-args': null,
37-
'no-remote': true,
37+
'binary-args': binaryArgs,
38+
'no-remote': false,
39+
'listen': '6000',
3840
'foreground': true,
3941
'profile': profile.path(),
4042
'env': {
@@ -48,7 +50,7 @@ export function run(
4850
let firefox = results.process;
4951

5052
log.debug(`Executing Firefox binary: ${results.binary}`);
51-
log.debug(`Executing Firefox with args: ${results.args.join(' ')}`);
53+
log.debug(`Firefox args: ${results.args.join(' ')}`);
5254

5355
firefox.on('error', (error) => {
5456
// TODO: show a nice error when it can't find Firefox.
@@ -67,8 +69,9 @@ export function run(
6769

6870
firefox.on('close', () => {
6971
log.debug('Firefox closed');
70-
resolve();
7172
});
73+
74+
resolve(firefox);
7275
});
7376
});
7477
}

src/firefox/preferences.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ prefs.common = {
2525

2626
// Allow remote connections to the debugger.
2727
'devtools.debugger.remote-enabled' : true,
28+
// Disable the prompt for allowing connections.
29+
'devtools.debugger.prompt-connection' : false,
2830

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

src/program.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class Program {
2828
}
2929

3030
command(name: string, description: string, executor: Function,
31-
commandOptions: ?Object): Program {
31+
commandOptions: Object = {}): Program {
3232
this.yargs.command(name, description, (yargs) => {
3333
if (!commandOptions) {
3434
return;
@@ -184,6 +184,10 @@ Example: $0 --help run.
184184
demand: false,
185185
type: 'string',
186186
},
187+
'no-reload': {
188+
describe: 'Do not reload the extension as the source changes',
189+
type: 'boolean',
190+
},
187191
});
188192

189193
return program.run(runOptions);

src/watcher.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Watchpack from 'watchpack';
33
import debounce from 'debounce';
44

55
import {createLogger} from './util/logger';
6+
import {FileFilter} from './cmd/build';
67

78
const log = createLogger(__filename);
89

@@ -21,16 +22,19 @@ export default function onSourceChange(
2122
log.debug(`Watching for file changes in ${sourceDir}`);
2223
watcher.watch([], [sourceDir], Date.now());
2324

24-
// TODO: support windows See:
25-
// http://stackoverflow.com/questions/10021373/what-is-the-windows-equivalent-of-process-onsigint-in-node-js
25+
// TODO: support interrupting the watcher on Windows.
26+
// https://github.com/mozilla/web-ext/issues/225
2627
process.on('SIGINT', () => watcher.close());
2728
return watcher;
2829
}
2930

3031

3132
export function proxyFileChanges(
32-
{artifactsDir, onChange, filePath, shouldWatchFile=() => true}
33-
: Object) {
33+
{artifactsDir, onChange, filePath, shouldWatchFile}: Object) {
34+
if (!shouldWatchFile) {
35+
const fileFilter = new FileFilter();
36+
shouldWatchFile = (...args) => fileFilter.wantFile(...args);
37+
}
3438
if (filePath.indexOf(artifactsDir) === 0 || !shouldWatchFile(filePath)) {
3539
log.debug(`Ignoring change to: ${filePath}`);
3640
} else {

0 commit comments

Comments
 (0)