-
Notifications
You must be signed in to change notification settings - Fork 332
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
feat: basic implementation of command line config option #1080
Changes from 11 commits
5552269
1c4ffd8
8c363e1
482fbb5
5d1db6c
67ea96f
afe0454
1e8acb9
32ec847
af2e9a9
737dd74
67d04a8
66e696c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,10 @@ import {UsageError} from './errors'; | |
import {createLogger, consoleStream as defaultLogStream} from './util/logger'; | ||
import {coerceCLICustomPreference} from './firefox/preferences'; | ||
import {checkForUpdates as defaultUpdateChecker} from './util/updates'; | ||
import { | ||
loadJSConfigFile as defaultLoadJSConfigFile, | ||
applyConfigToArgv as defaultApplyConfigToArgv, | ||
} from './config'; | ||
|
||
const log = createLogger(__filename); | ||
const envPrefix = 'WEB_EXT'; | ||
|
@@ -20,13 +24,17 @@ type ProgramOptions = {| | |
absolutePackageDir?: string, | ||
|} | ||
|
||
export type VersionGetterFn = (absolutePackageDir: string) => 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, | ||
getVersion?: VersionGetterFn, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @saintsebastian ❤️ thank you so much! this type annotation is way better! |
||
applyConfigToArgv?: typeof defaultApplyConfigToArgv, | ||
loadJSConfigFile?: typeof defaultLoadJSConfigFile, | ||
shouldExitProgram?: boolean, | ||
globalEnv?: string, | ||
} | ||
|
@@ -115,6 +123,8 @@ export class Program { | |
{ | ||
checkForUpdates = defaultUpdateChecker, systemProcess = process, | ||
logStream = defaultLogStream, getVersion = defaultVersionGetter, | ||
applyConfigToArgv = defaultApplyConfigToArgv, | ||
loadJSConfigFile = defaultLoadJSConfigFile, | ||
shouldExitProgram = true, globalEnv = WEBEXT_BUILD_ENV, | ||
}: ExecuteOptions = {} | ||
): Promise<void> { | ||
|
@@ -145,6 +155,17 @@ export class Program { | |
}); | ||
} | ||
|
||
if (argv.config) { | ||
const configFileName = path.resolve(argv.config); | ||
const configObject = loadJSConfigFile(configFileName); | ||
applyConfigToArgv({ | ||
argv, | ||
configFileName, | ||
configObject, | ||
options: this.options, | ||
}); | ||
} | ||
|
||
await runCommand(argv, {shouldExitProgram}); | ||
|
||
} catch (error) { | ||
|
@@ -194,7 +215,7 @@ export function defaultVersionGetter( | |
// TODO: add pipes to Flow type after https://github.com/facebook/flow/issues/2405 is fixed | ||
|
||
type MainParams = { | ||
getVersion?: Function, | ||
getVersion?: VersionGetterFn, | ||
commands?: Object, | ||
argv: Array<any>, | ||
runOptions?: Object, | ||
|
@@ -265,6 +286,13 @@ Example: $0 --help run. | |
describe: 'Disable all features that require standard input', | ||
type: 'boolean', | ||
}, | ||
'config': { | ||
alias: 'c', | ||
describe: 'Location of the config file', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think 'Path to the config file' would be a bit more explicit |
||
default: undefined, | ||
requiresArg: true, | ||
type: 'string', | ||
}, | ||
}); | ||
|
||
program | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,7 +110,7 @@ export type CreateBunyanLogParams = {| | |
export type CreateBunyanLogFn = (params: CreateBunyanLogParams) => Logger; | ||
|
||
export type CreateLoggerOptions = {| | ||
createBunyanLog: CreateBunyanLogFn, | ||
createBunyanLog?: CreateBunyanLogFn, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @saintsebastian is this change to the type annotation needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was this type change necessary? |
||
|}; | ||
|
||
export function createLogger( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ import { | |
loadJSConfigFile, | ||
} from '../../src/config'; | ||
import {withTempDir} from '../../src/util/temp-dir'; | ||
import {UsageError, WebExtError} from '../../src/errors'; | ||
import {UsageError} from '../../src/errors'; | ||
|
||
type MakeArgvParams = {| | ||
userCmd?: Array<string>, | ||
|
@@ -657,8 +657,8 @@ describe('config', () => { | |
}; | ||
assert.throws(() => { | ||
applyConf({...params, configObject}); | ||
}, WebExtError, | ||
'WebExtError: Option: apiUrl was defined without a type.'); | ||
}, UsageError, | ||
'UsageError: Option: apiUrl was defined without a type.'); | ||
}); | ||
|
||
it('throws an error when the type of one of them is in config' + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @saintsebastian This test seems basically the same of the above test, I see that you have only changed the expected error here, but given that you looked into this test recently I'm wondering if you could check if I'm right and this test case is redundant. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test looks like a duplicate of https://github.com/saintsebastian/web-ext/blob/737dd7495f090e5334b9ccd7ee8ad9a809ef4031/tests/unit/test.config.js#L640 You can delete it. It looks like the intention of the second test was to see if the code still works when one of the options has a type and the other does not but I don't think that's a useful test. |
||
|
@@ -688,8 +688,8 @@ describe('config', () => { | |
}; | ||
assert.throws(() => { | ||
applyConf({...params, configObject}); | ||
}, WebExtError, | ||
'WebExtError: Option: apiUrl was defined without a type.'); | ||
}, UsageError, | ||
'UsageError: Option: apiUrl was defined without a type.'); | ||
}); | ||
|
||
it('throws an error when type of unrelated sub option is invalid', () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,15 +9,20 @@ import {assert} from 'chai'; | |
|
||
import {defaultVersionGetter, main, Program} from '../../src/program'; | ||
import commands from '../../src/cmd'; | ||
import {onlyInstancesOf, UsageError} from '../../src/errors'; | ||
import { | ||
onlyInstancesOf, | ||
UsageError, | ||
} from '../../src/errors'; | ||
import { | ||
createFakeProcess, | ||
fake, | ||
makeSureItFails, | ||
ErrorWithCode, | ||
} from './helpers'; | ||
import {ConsoleStream} from '../../src/util/logger'; | ||
|
||
import { | ||
consoleStream, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @saintsebastian nit, an inline comment here could be useful here (just to confirm to the next developer that reads this test file that we are importing both consoleStream and ConsoleStream on purpose, one to be able to inspect the messages logged by the logger instance created internally in the tested module and the other to create new fake console streams) |
||
ConsoleStream, | ||
} from '../../src/util/logger'; | ||
|
||
describe('program.Program', () => { | ||
|
||
|
@@ -26,7 +31,7 @@ describe('program.Program', () => { | |
const absolutePackageDir = path.join(__dirname, '..', '..'); | ||
return program.execute( | ||
absolutePackageDir, { | ||
getVersion: () => spy(), | ||
getVersion: () => 'not-a-real-version', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. argh... This is why it is better to use a more specific type signature instead of Do you mind to file a follow up issue related to a more specific type signature for A reasonable flow type could be just |
||
checkForUpdates: spy(), | ||
systemProcess: fakeProcess, | ||
shouldExitProgram: false, | ||
|
@@ -430,6 +435,205 @@ describe('program.main', () => { | |
assert.strictEqual(options.shouldExitProgram, false); | ||
}); | ||
}); | ||
|
||
it('applies options from the specified config file', () => { | ||
const fakeCommands = fake(commands, { | ||
lint: () => Promise.resolve(), | ||
}); | ||
const fakePath = 'path/to/web-ext-config.js'; | ||
const resolvedFakePath = path.resolve(fakePath); | ||
const fakeLoadJSConfigFile = sinon.spy(() => {}); | ||
const fakeApplyConfigToArgv = sinon.spy(() => {}); | ||
|
||
return execProgram( | ||
['lint', '--config', fakePath], | ||
{ | ||
commands: fakeCommands, | ||
runOptions: { | ||
applyConfigToArgv: fakeApplyConfigToArgv, | ||
loadJSConfigFile: fakeLoadJSConfigFile, | ||
}, | ||
}) | ||
.then(() => { | ||
const options = fakeCommands.lint.firstCall.args[0]; | ||
assert.strictEqual(options.config, fakePath); | ||
sinon.assert.calledOnce(fakeLoadJSConfigFile); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to see the |
||
sinon.assert.calledWith(fakeLoadJSConfigFile, | ||
sinon.match(resolvedFakePath)); | ||
sinon.assert.calledOnce(fakeApplyConfigToArgv); | ||
sinon.assert.calledWith(fakeApplyConfigToArgv, sinon.match({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be better to check all parameters for the call to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume |
||
configFileName: resolvedFakePath, | ||
})); | ||
}); | ||
}); | ||
|
||
it('throws a UsageError when the config file can\'t be loaded', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you need this test because the behavior of handling thrown errors is already well defined. I can see that perhaps you wanted to be sure that the config checking code is executed from within the catch block but I don't think it's necessary to cover that logic. If one day we encounter a bug where someone accidentally moved it outside of the catch block then we could add a test for it. Until then, I think code review is a better way to make sure the config logic is executed with the catch block. |
||
const fakeProcess = createFakeProcess(); | ||
const fakeCommands = fake(commands, { | ||
lint: () => Promise.resolve(), | ||
}); | ||
const fakePath = 'path/to/web-ext-config.js'; | ||
const fakeLoadJSConfigFile = sinon.spy(() => { | ||
throw new UsageError('bad path'); | ||
}); | ||
const fakeApplyConfigToArgv = sinon.spy(() => {}); | ||
|
||
consoleStream.stopCapturing(); | ||
consoleStream.flushCapturedLogs(); | ||
consoleStream.startCapturing(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting. I see what you're doing here but I'd like to see it moved only to a single test instead of multiple tests. The test should be an assertion of how After you add a single test for this logic, your assertion of the Also, you should add a comment around the stream capturing/re-capturing logic because it's not obvious that it is working around the test suite itself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes a lot of sense. I was not sure what source of error should be, so I made command itself throw, let me know if it should be a dependency. |
||
|
||
return execProgram( | ||
['lint', '--config', fakePath], | ||
{ | ||
commands: fakeCommands, | ||
runOptions: { | ||
shouldExitProgram: false, | ||
systemProcess: fakeProcess, | ||
applyConfigToArgv: fakeApplyConfigToArgv, | ||
loadJSConfigFile: fakeLoadJSConfigFile, | ||
}, | ||
}) | ||
.then(makeSureItFails()) | ||
.catch(onlyInstancesOf(UsageError, (error) => { | ||
const {capturedMessages} = consoleStream; | ||
consoleStream.stopCapturing(); | ||
|
||
assert.match(error.message, /bad path/); | ||
assert.ok( | ||
capturedMessages.some( | ||
(message) => message.match(/bad path/) | ||
)); | ||
sinon.assert.notCalled(fakeApplyConfigToArgv); | ||
sinon.assert.notCalled(fakeProcess.exit); | ||
})); | ||
}); | ||
|
||
it('throws when config file loading raises an unexpected error', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to my last comment, I also don't think you need this test. |
||
const fakeProcess = createFakeProcess(); | ||
const fakeCommands = fake(commands, { | ||
lint: () => Promise.resolve(), | ||
}); | ||
const fakePath = 'path/to/web-ext-config.js'; | ||
const fakeLoadJSConfigFile = sinon.spy(() => { | ||
throw new Error('some error'); | ||
}); | ||
const fakeApplyConfigToArgv = sinon.spy(() => {}); | ||
|
||
consoleStream.stopCapturing(); | ||
consoleStream.flushCapturedLogs(); | ||
consoleStream.startCapturing(); | ||
|
||
return execProgram( | ||
['lint', '--config', fakePath], | ||
{ | ||
commands: fakeCommands, | ||
runOptions: { | ||
shouldExitProgram: false, | ||
systemProcess: fakeProcess, | ||
applyConfigToArgv: fakeApplyConfigToArgv, | ||
loadJSConfigFile: fakeLoadJSConfigFile, | ||
}, | ||
}) | ||
.then(makeSureItFails()) | ||
.catch((error) => { | ||
const {capturedMessages} = consoleStream; | ||
consoleStream.stopCapturing(); | ||
|
||
if (error instanceof UsageError) { | ||
throw error; | ||
} | ||
|
||
assert.match(error.message, /some error/); | ||
assert.ok(capturedMessages.some( | ||
(message) => message.match(/some error/) | ||
)); | ||
sinon.assert.notCalled(fakeApplyConfigToArgv); | ||
sinon.assert.notCalled(fakeProcess.exit); | ||
}); | ||
}); | ||
|
||
it('throws a UsageError when the loaded config can\'t be applied', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here :) I say delete the test. |
||
const fakeProcess = createFakeProcess(); | ||
const fakeCommands = fake(commands, { | ||
lint: () => Promise.resolve(), | ||
}); | ||
const fakePath = 'path/to/web-ext-config.js'; | ||
const fakeLoadJSConfigFile = sinon.spy(() => {}); | ||
const fakeApplyConfigToArgv = sinon.spy(() => { | ||
throw new UsageError('bad config option'); | ||
}); | ||
|
||
consoleStream.stopCapturing(); | ||
consoleStream.flushCapturedLogs(); | ||
consoleStream.startCapturing(); | ||
|
||
return execProgram( | ||
['lint', '--config', fakePath], | ||
{ | ||
commands: fakeCommands, | ||
runOptions: { | ||
shouldExitProgram: false, | ||
systemProcess: fakeProcess, | ||
applyConfigToArgv: fakeApplyConfigToArgv, | ||
loadJSConfigFile: fakeLoadJSConfigFile, | ||
}, | ||
}) | ||
.then(makeSureItFails()) | ||
.catch(onlyInstancesOf(UsageError, (error) => { | ||
const {capturedMessages} = consoleStream; | ||
consoleStream.stopCapturing(); | ||
|
||
assert.match(error.message, /bad config option/); | ||
assert.ok( | ||
capturedMessages.some( | ||
(message) => message.match(/bad config option/) | ||
)); | ||
sinon.assert.notCalled(fakeProcess.exit); | ||
})); | ||
}); | ||
|
||
it('throws when fakeApplyConfigToArgv throws an unexpected error', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be deleted. I don't think it's helpful to check that an error is thrown. |
||
const fakeProcess = createFakeProcess(); | ||
const fakeCommands = fake(commands, { | ||
lint: () => Promise.resolve(), | ||
}); | ||
const fakePath = 'path/to/web-ext-config.js'; | ||
const fakeLoadJSConfigFile = sinon.spy(() => {}); | ||
const fakeApplyConfigToArgv = sinon.spy(() => { | ||
throw new Error('some error'); | ||
}); | ||
|
||
consoleStream.stopCapturing(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @saintsebastian nit: we should probably add a comment near here to explain that: consoleStream.stopCapturing();
consoleStream.flushCapturedLogs(); are used here to ensure that we empty any captured log. |
||
consoleStream.flushCapturedLogs(); | ||
consoleStream.startCapturing(); | ||
|
||
return execProgram( | ||
['lint', '--config', fakePath], | ||
{ | ||
commands: fakeCommands, | ||
runOptions: { | ||
shouldExitProgram: false, | ||
systemProcess: fakeProcess, | ||
applyConfigToArgv: fakeApplyConfigToArgv, | ||
loadJSConfigFile: fakeLoadJSConfigFile, | ||
}, | ||
}) | ||
.then(makeSureItFails()) | ||
.catch((error) => { | ||
const {capturedMessages} = consoleStream; | ||
consoleStream.stopCapturing(); | ||
|
||
if (error instanceof UsageError) { | ||
throw error; | ||
} | ||
|
||
assert.match(error.message, /some error/); | ||
assert.ok(capturedMessages.some( | ||
(message) => message.match(/some error/) | ||
)); | ||
sinon.assert.notCalled(fakeProcess.exit); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('program.defaultVersionGetter', () => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be a
WebExtError
because when an option is defined without a type it's a programming error within web-ext itself (option types are defined at the bottom ofprogram.js
). It's not due to incorrect usage and it's not something the developer would be able to fix.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kumar303 ah, you are definitely right, and this change is my fault, it is related to this comment #1080 (comment) (Sorry @saintsebastian).
Nevertheless, to prevent this check from be read wrong in the future, how about adding an inline comment near this
throw
to briefly explain what this error is about?Another interesting option (I can file a bug so that we can handle it in a follow up) would be to use a more specific type in the flow annotations here:
web-ext/src/program.js
Line 96 in a55ee4c
so that we can make flow to raise an error during its static analysis if any of the options defined in program.js is missing the type property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rpl good idea about defining the shape of
options
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this PR a good place to add that type? @kumar303 @rpl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kumar303 @saintsebastian we could definitely define and add the additional flow types for the options in this PR, the amount of changes should not be that big, but it could be reasonable to handle it in as a separate issue and pull requests, so that we are free to discuss the details of it without blocking this PR.
In the meantime, I added an issue related to it as #1144 (which also contains some additional info about the change, e.g. where these new flow types would be used and what their flow type definitions could probably look like).