refactor(CLI): Resolve commands and options by schema

This commit is contained in:
Mariusz Nowak 2021-03-12 16:14:49 +01:00 committed by Mariusz Nowak
parent f12095a31d
commit fe663ead50
4 changed files with 143 additions and 34 deletions

View File

@ -17,6 +17,16 @@ disabledDeprecations:
- '*' # To disable all deprecation messages
```
<a name="CLI_OPTIONS_BEFORE_COMMAND"><div>&nbsp;</div></a>
## CLI command options should follow command
Deprecation code: `CLI_OPTIONS_BEFORE_COMMAND`
Starting with v3.0.0, Serverless will not support putting options before command, e.g. `sls -v deploy` will no longer be recognized as `deploy` command.
Ensure to always format CLI command as `sls [command..] [options...]`
<a name="CONFIG_VALIDATION_MODE_DEFAULT"><div>&nbsp;</div></a>
## `configValidationMode: error` will be new default`

View File

@ -44,6 +44,7 @@ commands.set('config credentials', {
overwrite: {
usage: 'Overwrite the existing profile configuration in the credentials file',
shortcut: 'o',
type: 'boolean',
},
},
});
@ -105,6 +106,7 @@ commands.set('deploy', {
options: {
'conceal': {
usage: 'Hide secrets from the output (e.g. API Gateway key values)',
type: 'boolean',
},
'package': {
usage: 'Path of the deployment package',
@ -113,9 +115,11 @@ commands.set('deploy', {
'verbose': {
usage: 'Show all stack events during deployment',
shortcut: 'v',
type: 'boolean',
},
'force': {
usage: 'Forces a deployment to take place',
type: 'boolean',
},
'function': {
usage: "Function name. Deploys a single function (see 'deploy function')",
@ -123,6 +127,7 @@ commands.set('deploy', {
},
'aws-s3-accelerate': {
usage: 'Enables S3 Transfer Acceleration making uploading artifacts much faster.',
type: 'boolean',
},
},
});
@ -139,10 +144,12 @@ commands.set('deploy function', {
},
'force': {
usage: 'Forces a deployment to take place',
type: 'boolean',
},
'update-config': {
usage: 'Updates function configuration, e.g. Timeout or Memory Size without deploying code', // eslint-disable-line max-len
shortcut: 'u',
type: 'boolean',
},
},
});
@ -183,6 +190,7 @@ commands.set('info', {
options: {
conceal: {
usage: 'Hide secrets from the output (e.g. API Gateway key values)',
type: 'boolean',
},
},
});
@ -227,6 +235,7 @@ commands.set('invoke', {
log: {
usage: 'Trigger logging data output',
shortcut: 'l',
type: 'boolean',
},
data: {
usage: 'Input data',
@ -234,6 +243,7 @@ commands.set('invoke', {
},
raw: {
usage: 'Flag to pass input data as a raw string',
type: 'boolean',
},
context: {
usage: 'Context of the service',
@ -264,6 +274,7 @@ commands.set('invoke local', {
},
'raw': {
usage: 'Flag to pass input data as a raw string',
type: 'boolean',
},
'context': {
usage: 'Context of the service',
@ -276,7 +287,7 @@ commands.set('invoke local', {
usage: 'Override environment variables. e.g. --env VAR1=val1 --env VAR2=val2',
shortcut: 'e',
},
'docker': { usage: 'Flag to turn on docker use for node/python/ruby/java' },
'docker': { usage: 'Flag to turn on docker use for node/python/ruby/java', type: 'boolean' },
'docker-arg': {
usage: 'Arguments to docker run command. e.g. --docker-arg "-p 9229:9229"',
},
@ -304,6 +315,7 @@ commands.set('logs', {
tail: {
usage: 'Tail the log output',
shortcut: 't',
type: 'boolean',
},
startTime: {
usage:
@ -447,6 +459,7 @@ commands.set('remove', {
verbose: {
usage: 'Show all stack events during deployment',
shortcut: 'v',
type: 'boolean',
},
},
});
@ -464,6 +477,7 @@ commands.set('rollback', {
verbose: {
usage: 'Show all stack events during deployment',
shortcut: 'v',
type: 'boolean',
},
},
});
@ -491,10 +505,12 @@ commands.set('slstats', {
enable: {
usage: 'Enable stats ("--enable")',
shortcut: 'e',
type: 'boolean',
},
disable: {
usage: 'Disable stats ("--disable")',
shortcut: 'd',
type: 'boolean',
},
},
});
@ -507,6 +523,7 @@ commands.set('studio', {
autoStage: {
usage: 'If specified, generate a random stage. This stage will be removed on exit.',
shortcut: 'a',
type: 'boolean',
},
},
});
@ -532,6 +549,7 @@ commands.set('upgrade', {
options: {
major: {
usage: 'Enable upgrade to a new major release',
type: 'boolean',
},
},
});
@ -561,6 +579,7 @@ const awsServiceOptions = {
usage:
'Rely on locally resolved AWS credentials instead of loading them from ' +
'Dashboard provider settings (applies only to services integrated with Dashboard)',
type: 'boolean',
},
};
@ -571,8 +590,8 @@ for (const [name, schema] of commands) {
if (schema.hasAwsExtension) Object.assign(schema.options, awsServiceOptions);
}
if (name) {
schema.options.help = { usage: 'Show this message', shortcut: 'h' };
schema.options.help = { usage: 'Show this message', shortcut: 'h', type: 'boolean' };
} else {
schema.options['help-interactive'] = { usage: 'Show this message' };
schema.options['help-interactive'] = { usage: 'Show this message', type: 'boolean' };
}
}

View File

@ -4,42 +4,83 @@
const memoizee = require('memoizee');
const parseArgs = require('./parse-args');
const commandsSchema = require('./commands-schema');
const customSAliasCommands = new Set(['config credentials'], ['config tabcompletion install']);
const baseArgsSchema = {
boolean: new Set(['help', 'help-interactive', 'use-local-credentials', 'v', 'version']),
string: new Set(['app', 'config', 'org', 'stage']),
alias: new Map([
['c', 'config'],
['h', 'help'],
]),
};
const resolveArgsSchema = (commandOptionsSchema) => {
const options = { boolean: new Set(), string: new Set(), alias: new Map() };
for (const [name, optionSchema] of Object.entries(commandOptionsSchema)) {
switch (optionSchema.type) {
case 'boolean':
options.boolean.add(name);
break;
case 'multiple':
options.multiple.add(name);
break;
default:
options.string.add(name);
}
if (optionSchema.shortcut) options.alias.set(optionSchema.shortcut, name);
}
return options;
};
module.exports = memoizee(() => {
const args = process.argv.slice(2);
const baseArgsSchema = {
boolean: new Set(['help', 'help-interactive', 'v', 'version']),
string: new Set(['app', 'config', 'org', 'stage']),
alias: new Map([
['c', 'config'],
['h', 'help'],
]),
};
// Ideally no options should be passed before command (to know what options are supported,
// and whether they're boolean or not, we need to know command name upfront).
// Still so far we (kind of) supported such notation and we need to maintain it in current major.
// Thefore at first resolution stage we use schema that recognizes just some popular options
let options = parseArgs(args, baseArgsSchema);
const command = options._.join(' ');
if (!command) {
// Ideally we should output version info in whatever context "--version" or "-v" params
// are used. Still "-v" is defined also as a "--verbose" alias for some commands.
// Support for "--verbose" is expected to go away with
// https://github.com/serverless/serverless/issues/1720
// Until that's addressed we can recognize "-v" only with no commands
baseArgsSchema.boolean.delete('v');
baseArgsSchema.alias.set('v', 'version');
}
if (!customSAliasCommands.has(command)) {
// Unfortunately, there are few command for which "-s" aliases different param than "--stage"
// This handling ensures we do not break those commands
baseArgsSchema.alias.set('s', 'stage');
}
options = parseArgs(args, baseArgsSchema);
const commands = options._;
let commands = options._;
delete options._;
let command = commands.join(' ');
if (!command) {
// Handle eventual special cases, not reflected in commands schema
if (options.v) options.version = true;
if (options.help || options.version) return { commands, options };
}
if (command === 'help') return { commands, options };
// Having command potentially resolved, resolve options again with help of the command schema
let commandSchema = commandsSchema.get(command);
while (commandSchema) {
const resolvedOptions = parseArgs(args, resolveArgsSchema(commandSchema.options));
const resolvedCommand = resolvedOptions._.join(' ');
if (resolvedCommand === command) {
options = resolvedOptions;
commands = options._;
delete options._;
break;
}
// Unlikely scenario, where after applying the command schema different command resolves
// It can happen only in cases where e.g. for "sls deploy --force function -f foo"
// we intially assume "deploy" command, while after applying "deploy" command schema it's
// actually a "deploy function" command that resolves
command = resolvedCommand;
commandSchema = commandsSchema.get(resolvedCommand);
}
const argsString = args.join(' ');
if (command && argsString !== command && !argsString.startsWith(`${command} `)) {
// Some options were passed before command name (e.g. "sls -v deploy"), deprecate such usage
require('../utils/logDeprecation')(
'CLI_OPTIONS_BEFORE_COMMAND',
'"serverless" command options are expected to follow command and not be put before the command.\n' +
'Starting from next major Serverless will no longer support the latter form.'
);
}
return { commands, options };
});

View File

@ -71,7 +71,7 @@ describe('test/unit/lib/cli/resolve-input.test.js', () => {
resolveInput.clear();
data = overrideArgv(
{
args: ['serverless', 'cmd1', 'cmd2', '-s', 'stage'],
args: ['serverless', 'package', '-s', 'stage'],
},
() => resolveInput()
);
@ -124,5 +124,44 @@ describe('test/unit/lib/cli/resolve-input.test.js', () => {
it('should recognize --c alias', async () => {
expect(data.options.config).to.equal('conf');
});
it('should recognize --version', async () => {
resolveInput.clear();
data = overrideArgv(
{
args: ['serverless', '--version'],
},
() => resolveInput()
);
expect(data).to.deep.equal({ commands: [], options: { version: true } });
});
it('should recognize interactive setup', async () => {
resolveInput.clear();
data = overrideArgv(
{
args: ['serverless', '--app', 'foo'],
},
() => resolveInput()
);
expect(data).to.deep.equal({ commands: [], options: { app: 'foo' } });
});
});
describe('"help" command', () => {
let data;
before(() => {
resolveInput.clear();
data = overrideArgv(
{
args: ['serverless', 'help'],
},
() => resolveInput()
);
});
it('should recognize', async () => {
expect(data).to.deep.equal({ commands: ['help'], options: {} });
});
});
});