navy 7.0.0-alpha.2 → 7.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _chai = require("chai");
|
|
4
|
+
var _commander = require("commander");
|
|
5
|
+
/* eslint-env mocha */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* These tests check that the commander configuration the navy CLI relies on
|
|
9
|
+
* (positional options + per-subcommand option declarations) rejects options
|
|
10
|
+
* that a subcommand does not declare, using the exact error format commander
|
|
11
|
+
* emits by default.
|
|
12
|
+
*
|
|
13
|
+
* We build a minimal program with the same shape as packages/navy/src/cli/program.js
|
|
14
|
+
* rather than loading that module, because importing program.js has side effects
|
|
15
|
+
* (config reading, action wiring) that are not relevant to argument parsing.
|
|
16
|
+
*/
|
|
17
|
+
describe('cli/program unknown option handling', function () {
|
|
18
|
+
function buildProgram() {
|
|
19
|
+
const program = new _commander.Command();
|
|
20
|
+
program.exitOverride();
|
|
21
|
+
program.configureOutput({
|
|
22
|
+
writeErr: () => {},
|
|
23
|
+
writeOut: () => {}
|
|
24
|
+
});
|
|
25
|
+
program.enablePositionalOptions();
|
|
26
|
+
program.option('-e, --navy [env]', 'set the navy name to be used', 'dev');
|
|
27
|
+
program.command('status').option('--json', 'output JSON instead of a table').action(() => {});
|
|
28
|
+
program.command('start [services...]').option('-e, --navy [env]', 'set the navy name to be used', 'dev').action(() => {});
|
|
29
|
+
return program;
|
|
30
|
+
}
|
|
31
|
+
describe('a subcommand that does not declare the option', function () {
|
|
32
|
+
it('should throw a commander error for an unknown short option', function () {
|
|
33
|
+
const program = buildProgram();
|
|
34
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'status', '-e', 'foo'])).to.throw(/unknown option '-e'/);
|
|
35
|
+
});
|
|
36
|
+
it('should produce a non-zero exit code on the thrown error', function () {
|
|
37
|
+
const program = buildProgram();
|
|
38
|
+
try {
|
|
39
|
+
program.parse(['node', 'navy', 'status', '-e', 'foo']);
|
|
40
|
+
_chai.expect.fail('expected parse to throw');
|
|
41
|
+
} catch (ex) {
|
|
42
|
+
(0, _chai.expect)(ex.exitCode).to.be.greaterThan(0);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it('should emit the canonical commander unknown-option message', function () {
|
|
46
|
+
const program = buildProgram();
|
|
47
|
+
try {
|
|
48
|
+
program.parse(['node', 'navy', 'status', '-e', 'foo']);
|
|
49
|
+
_chai.expect.fail('expected parse to throw');
|
|
50
|
+
} catch (ex) {
|
|
51
|
+
(0, _chai.expect)(ex.message).to.equal("error: unknown option '-e'");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
it('should still accept declared options on the same subcommand', function () {
|
|
55
|
+
const program = buildProgram();
|
|
56
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'status', '--json'])).to.not.throw();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('a subcommand that declares the option', function () {
|
|
60
|
+
it('should accept the option after the subcommand name', function () {
|
|
61
|
+
const program = buildProgram();
|
|
62
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'start', '-e', 'foo'])).to.not.throw();
|
|
63
|
+
});
|
|
64
|
+
it('should accept the option before the subcommand name (parent inherits)', function () {
|
|
65
|
+
const program = buildProgram();
|
|
66
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', '-e', 'foo', 'start'])).to.not.throw();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -28,6 +28,7 @@ describe('cli/program', function () {
|
|
|
28
28
|
helpHandlersByCommand = {};
|
|
29
29
|
fakeProgram = {
|
|
30
30
|
option: sandbox.stub().returnsThis(),
|
|
31
|
+
enablePositionalOptions: sandbox.stub().returnsThis(),
|
|
31
32
|
command(spec) {
|
|
32
33
|
const name = spec.split(' ')[0];
|
|
33
34
|
const cmd = {
|
|
@@ -155,6 +156,10 @@ describe('cli/program', function () {
|
|
|
155
156
|
loadModule();
|
|
156
157
|
(0, _chai.expect)(fakeProgram.option.calledWith('-e, --navy [env]', _sinon.default.match.string, 'dev')).to.equal(true);
|
|
157
158
|
});
|
|
159
|
+
it('should enable positional options so unknown subcommand options are rejected', function () {
|
|
160
|
+
loadModule();
|
|
161
|
+
(0, _chai.expect)(fakeProgram.enablePositionalOptions.calledOnce).to.equal(true);
|
|
162
|
+
});
|
|
158
163
|
});
|
|
159
164
|
describe('lazyRequire wrapped action', function () {
|
|
160
165
|
it('should pass through arguments to the underlying module', async function () {
|
|
@@ -454,6 +459,7 @@ describe('cli/program', function () {
|
|
|
454
459
|
capturedActions = {};
|
|
455
460
|
return {
|
|
456
461
|
option: sandbox.stub().returnsThis(),
|
|
462
|
+
enablePositionalOptions: sandbox.stub().returnsThis(),
|
|
457
463
|
command(spec) {
|
|
458
464
|
const name = spec.split(' ')[0];
|
|
459
465
|
const cmd = {
|
package/lib/cli/program.js
CHANGED
|
@@ -135,6 +135,11 @@ function lazyRequire(path) {
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
const defaultNavy = process.env.NAVY_NAME || (0, _config.getConfig)().defaultNavy;
|
|
138
|
+
|
|
139
|
+
// Strictly separate parent and subcommand options so an option that isn't
|
|
140
|
+
// declared on a subcommand (e.g. `navy status -e foo`) is reported as
|
|
141
|
+
// unknown rather than silently absorbed by the parent program.
|
|
142
|
+
_commander.program.enablePositionalOptions();
|
|
138
143
|
_commander.program.option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy);
|
|
139
144
|
const importCommand = _commander.program.command('import').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Imports docker compose configuration from the current working directory and initialises a new navy').action(lazyRequire('./import'));
|
|
140
145
|
(0, _configProvider.getImportCommandLineOptions)().forEach(opt => importCommand.option(...opt));
|
|
@@ -44,4 +44,28 @@ describe('cli/util/merge-action-options', function () {
|
|
|
44
44
|
});
|
|
45
45
|
(0, _chai.expect)(spy.calledOnce).to.equal(true);
|
|
46
46
|
});
|
|
47
|
+
it('should let an explicit subcommand option beat the inherited parent default', function () {
|
|
48
|
+
const command = {
|
|
49
|
+
optsWithGlobals: () => ({
|
|
50
|
+
navy: 'parent-default'
|
|
51
|
+
}),
|
|
52
|
+
getOptionValueSource: key => key === 'navy' ? 'cli' : 'default'
|
|
53
|
+
};
|
|
54
|
+
const merged = (0, _mergeActionOptions.mergeActionOptions)({
|
|
55
|
+
navy: 'from-subcommand'
|
|
56
|
+
}, command);
|
|
57
|
+
(0, _chai.expect)(merged.navy).to.equal('from-subcommand');
|
|
58
|
+
});
|
|
59
|
+
it('should keep the parent value when the subcommand value came from its default', function () {
|
|
60
|
+
const command = {
|
|
61
|
+
optsWithGlobals: () => ({
|
|
62
|
+
navy: 'from-parent'
|
|
63
|
+
}),
|
|
64
|
+
getOptionValueSource: () => 'default'
|
|
65
|
+
};
|
|
66
|
+
const merged = (0, _mergeActionOptions.mergeActionOptions)({
|
|
67
|
+
navy: 'subcommand-default'
|
|
68
|
+
}, command);
|
|
69
|
+
(0, _chai.expect)(merged.navy).to.equal('from-parent');
|
|
70
|
+
});
|
|
47
71
|
});
|
|
@@ -6,15 +6,26 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.mergeActionOptions = mergeActionOptions;
|
|
7
7
|
/**
|
|
8
8
|
* Merge Commander's per-command opts with inherited globals (e.g. `navy -e dev ps`).
|
|
9
|
-
* `optsWithGlobals()`
|
|
10
|
-
*
|
|
9
|
+
* `optsWithGlobals()` has "globals overwrite locals" semantics, which makes parent
|
|
10
|
+
* defaults win over child defaults. With positional options enabled, however, a
|
|
11
|
+
* value supplied after the subcommand name (e.g. `navy ps -e dev`) is parsed onto
|
|
12
|
+
* the subcommand, not the parent, so we re-apply any option whose source on the
|
|
13
|
+
* subcommand is non-default to keep the explicit value.
|
|
11
14
|
*/
|
|
12
15
|
function mergeActionOptions(parsedOpts, command) {
|
|
13
16
|
if (!command || typeof command.optsWithGlobals !== 'function') {
|
|
14
17
|
return parsedOpts;
|
|
15
18
|
}
|
|
16
|
-
|
|
17
|
-
...parsedOpts,
|
|
19
|
+
const merged = {
|
|
18
20
|
...command.optsWithGlobals()
|
|
19
21
|
};
|
|
22
|
+
if (typeof command.getOptionValueSource === 'function') {
|
|
23
|
+
for (const key of Object.keys(parsedOpts)) {
|
|
24
|
+
const source = command.getOptionValueSource(key);
|
|
25
|
+
if (source && source !== 'default') {
|
|
26
|
+
merged[key] = parsedOpts[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return merged;
|
|
20
31
|
}
|