-
Notifications
You must be signed in to change notification settings - Fork 343
/
program.js
427 lines (388 loc) · 12.6 KB
/
program.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
/* @flow */
import path from 'path';
import {readFileSync} from 'fs';
import camelCase from 'camelcase';
import git from 'git-rev-sync';
import yargs from 'yargs';
import defaultCommands from './cmd';
import {UsageError} from './errors';
import {createLogger, consoleStream as defaultLogStream} from './util/logger';
import {coerceCLICustomPreference} from './firefox/preferences';
import {checkForUpdates as defaultUpdateChecker} from './util/updates';
const log = createLogger(__filename);
const envPrefix = 'WEB_EXT';
type ProgramOptions = {|
absolutePackageDir?: string,
|}
// TODO: add pipes to Flow type after https://github.com/facebook/flow/issues/2405 is fixed
type ExecuteOptions = {
checkForUpdates?: Function,
systemProcess?: typeof process,
logStream?: typeof defaultLogStream,
getVersion?: Function,
shouldExitProgram?: boolean,
globalEnv?: string,
}
/*
* The command line program.
*/
export class Program {
yargs: any;
commands: { [key: string]: Function };
shouldExitProgram: boolean;
options: Object;
constructor(
argv: ?Array<string>,
{
absolutePackageDir = process.cwd(),
}: ProgramOptions = {}
) {
// This allows us to override the process argv which is useful for
// testing.
// NOTE: process.argv.slice(2) removes the path to node and web-ext
// executables from the process.argv array.
argv = argv || process.argv.slice(2);
// NOTE: always initialize yargs explicitly with the package dir
// so that we are sure that it is going to load the 'boolean-negation: false'
// config (See web-ext#469 for rationale).
const yargsInstance = yargs(argv, absolutePackageDir);
this.shouldExitProgram = true;
this.yargs = yargsInstance;
this.yargs.strict();
this.commands = {};
this.options = {};
}
command(
name: string, description: string, executor: Function,
commandOptions: Object = {}
): Program {
this.options[camelCase(name)] = commandOptions;
this.yargs.command(name, description, (yargsForCmd) => {
if (!commandOptions) {
return;
}
return yargsForCmd
// Make sure the user does not add any extra commands. For example,
// this would be a mistake because lint does not accept arguments:
// web-ext lint ./src/path/to/file.js
.demandCommand(0, 0, undefined,
'This command does not take any arguments')
.strict()
.exitProcess(this.shouldExitProgram)
// Calling env() will be unnecessary after
// https://github.com/yargs/yargs/issues/486 is fixed
.env(envPrefix)
.options(commandOptions);
});
this.commands[name] = executor;
return this;
}
setGlobalOptions(options: Object): Program {
// This is a convenience for setting global options.
// An option is only global (i.e. available to all sub commands)
// with the `global` flag so this makes sure every option has it.
this.options = {...this.options, ...options};
Object.keys(options).forEach((key) => {
options[key].global = true;
if (options[key].demand === undefined) {
// By default, all options should be "demanded" otherwise
// yargs.strict() will think they are missing when declared.
options[key].demand = true;
}
});
this.yargs.options(options);
return this;
}
async execute(
absolutePackageDir: string,
{
checkForUpdates = defaultUpdateChecker, systemProcess = process,
logStream = defaultLogStream, getVersion = defaultVersionGetter,
shouldExitProgram = true, globalEnv = WEBEXT_BUILD_ENV,
}: ExecuteOptions = {}
): Promise<void> {
this.shouldExitProgram = shouldExitProgram;
this.yargs.exitProcess(this.shouldExitProgram);
const argv = this.yargs.argv;
const cmd = argv._[0];
const runCommand = this.commands[cmd];
if (argv.verbose) {
logStream.makeVerbose();
log.info('Version:', getVersion(absolutePackageDir));
}
try {
if (cmd === undefined) {
throw new UsageError('No sub-command was specified in the args');
}
if (!runCommand) {
throw new UsageError(`Unknown command: ${cmd}`);
}
if (globalEnv === 'production') {
checkForUpdates ({
version: getVersion(absolutePackageDir),
});
}
await runCommand(argv, {shouldExitProgram});
} catch (error) {
if (!(error instanceof UsageError) || argv.verbose) {
log.error(`\n${error.stack}\n`);
} else {
log.error(`\n${error}\n`);
}
if (error.code) {
log.error(`Error code: ${error.code}\n`);
}
log.debug(`Command executed: ${cmd}`);
if (this.shouldExitProgram) {
systemProcess.exit(1);
} else {
throw error;
}
}
}
}
// A global variable generated by DefinePlugin, generated in webpack.config.js
declare var WEBEXT_BUILD_ENV: string;
//A defintion of type of argument for defaultVersionGetter
type VersionGetterOptions = {
globalEnv?: string,
};
export function defaultVersionGetter(
absolutePackageDir: string,
{globalEnv = WEBEXT_BUILD_ENV}: VersionGetterOptions = {}
): string {
if (globalEnv === 'production') {
log.debug('Getting the version from package.json');
const packageData: any = readFileSync(
path.join(absolutePackageDir, 'package.json'));
return JSON.parse(packageData).version;
} else {
log.debug('Getting version from the git revision');
return `${git.branch(absolutePackageDir)}-${git.long(absolutePackageDir)}`;
}
}
// TODO: add pipes to Flow type after https://github.com/facebook/flow/issues/2405 is fixed
type MainParams = {
getVersion?: Function,
commands?: Object,
argv: Array<any>,
runOptions?: Object,
}
export function main(
absolutePackageDir: string,
{
getVersion = defaultVersionGetter, commands = defaultCommands, argv,
runOptions = {},
}: MainParams = {}
): Promise<any> {
const program = new Program(argv, {absolutePackageDir});
// yargs uses magic camel case expansion to expose options on the
// final argv object. For example, the 'artifacts-dir' option is alternatively
// available as argv.artifactsDir.
program.yargs
.usage(`Usage: $0 [options] command
Option values can also be set by declaring an environment variable prefixed
with $${envPrefix}_. For example: $${envPrefix}_SOURCE_DIR=/path is the same as
--source-dir=/path.
To view specific help for any given command, add the command name.
Example: $0 --help run.
`)
.help('help')
.alias('h', 'help')
.env(envPrefix)
.version(() => getVersion(absolutePackageDir))
.demandCommand(1, 'You must specify a command')
.strict();
program.setGlobalOptions({
'source-dir': {
alias: 's',
describe: 'Web extension source directory.',
default: process.cwd(),
requiresArg: true,
type: 'string',
coerce: path.resolve,
},
'artifacts-dir': {
alias: 'a',
describe: 'Directory where artifacts will be saved.',
default: path.join(process.cwd(), 'web-ext-artifacts'),
normalize: true,
requiresArg: true,
type: 'string',
},
'verbose': {
alias: 'v',
describe: 'Show verbose output',
type: 'boolean',
},
'ignore-files': {
alias: 'i',
describe: 'A list of glob patterns to define which files should be ' +
'ignored. (Example: --ignore-files=path/to/first.js ' +
'path/to/second.js "**/*.log")',
demand: false,
requiresArg: true,
type: 'array',
},
'no-input': {
describe: 'Disable all features that require standard input',
type: 'boolean',
},
});
program
.command(
'build',
'Create an extension package from source',
commands.build, {
'as-needed': {
describe: 'Watch for file changes and re-build as needed',
type: 'boolean',
},
'overwrite-dest': {
alias: 'o',
describe: 'Overwrite destination package if it exists.',
type: 'boolean',
},
})
.command(
'sign',
'Sign the extension so it can be installed in Firefox',
commands.sign, {
'api-key': {
describe: 'API key (JWT issuer) from addons.mozilla.org',
demand: true,
type: 'string',
},
'api-secret': {
describe: 'API secret (JWT secret) from addons.mozilla.org',
demand: true,
type: 'string',
},
'api-url-prefix': {
describe: 'Signing API URL prefix',
default: 'https://addons.mozilla.org/api/v3',
demand: true,
type: 'string',
},
'api-proxy': {
describe:
'Use a proxy to access the signing API. ' +
'Example: https://yourproxy:6000 ',
demand: false,
type: 'string',
},
'id': {
describe:
'A custom ID for the extension. This has no effect if the ' +
'extension already declares an explicit ID in its manifest.',
demand: false,
type: 'string',
},
'timeout': {
describe: 'Number of milliseconds to wait before giving up',
type: 'number',
},
})
.command('run', 'Run the extension', commands.run, {
'firefox': {
alias: ['f', 'firefox-binary'],
describe: 'Path or alias to a Firefox executable such as firefox-bin ' +
'or firefox.exe. ' +
'If not specified, the default Firefox will be used. ' +
'You can specify the following aliases in lieu of a path: ' +
'firefox, beta, nightly, firefoxdeveloperedition.',
demand: false,
type: 'string',
},
'firefox-profile': {
alias: 'p',
describe: 'Run Firefox using a copy of this profile. The profile ' +
'can be specified as a directory or a name, such as one ' +
'you would see in the Profile Manager. If not specified, ' +
'a new temporary profile will be created.',
demand: false,
type: 'string',
},
'keep-profile-changes': {
describe: 'Run Firefox directly in custom profile. Any changes to ' +
'the profile will be saved.',
demand: false,
type: 'boolean',
},
'no-reload': {
describe: 'Do not reload the extension when source files change',
demand: false,
type: 'boolean',
},
'pre-install': {
describe: 'Pre-install the extension into the profile before ' +
'startup. This is only needed to support older versions ' +
'of Firefox.',
demand: false,
type: 'boolean',
},
'pref': {
describe: 'Launch firefox with a custom preference ' +
'(example: --pref=general.useragent.locale=fr-FR). ' +
'You can repeat this option to set more than one ' +
'preference.',
demand: false,
requiresArg: true,
type: 'string',
coerce: coerceCLICustomPreference,
},
'start-url': {
alias: ['u', 'url'],
describe: 'Launch firefox at specified page',
demand: false,
requiresArg: true,
type: 'string',
},
'browser-console': {
alias: ['bc'],
describe: 'Open the DevTools Browser Console.',
demand: false,
type: 'boolean',
},
})
.command('lint', 'Validate the extension source', commands.lint, {
'output': {
alias: 'o',
describe: 'The type of output to generate',
type: 'string',
default: 'text',
choices: ['json', 'text'],
},
'metadata': {
describe: 'Output only metadata as JSON',
type: 'boolean',
default: false,
},
'warnings-as-errors': {
describe: 'Treat warnings as errors by exiting non-zero for warnings',
alias: 'w',
type: 'boolean',
default: false,
},
'pretty': {
describe: 'Prettify JSON output',
type: 'boolean',
default: false,
},
'self-hosted': {
describe:
'Your extension will be self-hosted. This disables messages ' +
'related to hosting on addons.mozilla.org.',
type: 'boolean',
default: false,
},
'boring': {
describe: 'Disables colorful shell output',
type: 'boolean',
default: false,
},
})
.command('docs', 'Open the web-ext documentation in a browser',
commands.docs, {});
return program.execute(absolutePackageDir, runOptions);
}