[dev/run] options can modify help, errors can show help (#33466)
* [dev/run] options can modify help, errors can show help * include minor tooling log usage * cleanup wording a little * cleanup run options # Conflicts: # src/dev/test_stats/get_test_stats_cli.ts * explicitly document opts that are passed to getopts
This commit is contained in:
parent
7c50ab8aff
commit
ba4597e085
12 changed files with 233 additions and 21 deletions
|
@ -49,5 +49,5 @@ export function lintFiles(log, files) {
|
|||
}
|
||||
|
||||
log.error(cli.getFormatter()(report.results));
|
||||
throw createFailError(`[eslint] ${failTypes.join(' & ')}`, 1);
|
||||
throw createFailError(`[eslint] ${failTypes.join(' & ')}`);
|
||||
}
|
||||
|
|
125
src/dev/run/README.md
Normal file
125
src/dev/run/README.md
Normal file
|
@ -0,0 +1,125 @@
|
|||
# dev/run
|
||||
|
||||
Helper functions for writing little scripts for random build/ci/dev tasks.
|
||||
|
||||
## Usage
|
||||
|
||||
Define the function that should validate the CLI arguments and call your task fn:
|
||||
|
||||
```ts
|
||||
// dev/my_task/run_my_task.ts
|
||||
import { createFlagError, run } from '../run';
|
||||
|
||||
run(
|
||||
async ({ flags, log }) => {
|
||||
if (typeof flags.path !== 'string') {
|
||||
throw createFlagError('please provide a single --path flag');
|
||||
}
|
||||
|
||||
await runTask(flags.path);
|
||||
log.success('task complete');
|
||||
},
|
||||
{
|
||||
description: `
|
||||
Run my special task
|
||||
`,
|
||||
flags: {
|
||||
string: ['path'],
|
||||
help: `
|
||||
--path Required, path to the file to operate on
|
||||
`
|
||||
},
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Define the script which will setup node and load the script source:
|
||||
|
||||
```js
|
||||
// scripts/my_task.js
|
||||
|
||||
require('../src/setup_node_env');
|
||||
require('../src/dev/my_task/run_my_task');
|
||||
```
|
||||
|
||||
Try out the script:
|
||||
|
||||
```sh
|
||||
$ node scripts/my_task
|
||||
|
||||
# ERROR please provide a single --path flag
|
||||
#
|
||||
# node scripts/my_task.js
|
||||
#
|
||||
# Run my special task
|
||||
#
|
||||
# Options:
|
||||
# --path Required, path to the file to operate on
|
||||
# --verbose, -v Log verbosely
|
||||
# --debug Log debug messages (less than verbose)
|
||||
# --quiet Only log errors
|
||||
# --silent Don't log anything
|
||||
# --help Show this message
|
||||
#
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- ***`run(fn: async ({ flags: Flags, log: ToolingLog }) => Promise<void>, options: Options)`***
|
||||
|
||||
Execte an async function, passing it the parsed flags and a tooling log that is configured to the requested logging level. If the returned promise is rejected with an error created by `createFailError(...)` or `createFlagError(...)` the process will exit as described by the error, otherwise the process will exit with code 1.
|
||||
|
||||
**`fn` Params:**
|
||||
- *[`log: ToolingLog`](../../../packages/kbn-dev-utils/src/tooling_log/tooling_log.js)*:
|
||||
|
||||
An instance of the `ToolingLog` that is configured with the standard flags: `--verbose`, `--quiet`, `--silent`, and `--debug`
|
||||
|
||||
- *[`flags: Flags`](flags.ts)*:
|
||||
|
||||
The parsed CLI flags, created by [`getopts`](https://www.npmjs.com/package/getopts). Includes the default flags for controlling the log level of the ToolingLog, and `flags.unexpected`, which is an array of flag names which were passed but not expected.
|
||||
|
||||
**`Options`:**
|
||||
- *`usage: string`*
|
||||
|
||||
A bit of text to replace the default usage in the `--help` text.
|
||||
|
||||
- *`description: string`*
|
||||
|
||||
A bit of text to replace the default description in the `--help` text.
|
||||
|
||||
- *`flags.help: string`*
|
||||
|
||||
A bit of text included at the top of the `Options:` section of the `--help` text.
|
||||
|
||||
- *`flags.string: string[]`*
|
||||
|
||||
An array of flag names that are expected to have a string value.
|
||||
|
||||
- *`flags.boolean: string[]`*
|
||||
|
||||
An array of flag names that are expected to have a boolean value.
|
||||
|
||||
- *`flags.alias: { [short: string], string }`*
|
||||
|
||||
A map of short flag names to long flag names, used to expand short flags like `-v` to `--verbose`.
|
||||
|
||||
- *`flags.default: { [name: string]: string | string[] | boolean | undefined }`*
|
||||
|
||||
A map of flag names to their default value. If the flag is not defined this value will be set in the flags object passed to the run `fn`.
|
||||
|
||||
- *`flags.allowUnexpected: boolean`*
|
||||
|
||||
By default, any flag that is passed but not mentioned in `flags.string`, `flags.boolean`, `flags.alias` or `flags.default` will trigger an error, preventing the run function from calling its first argument. If you have a reason to disable this behavior set this option to `true`.
|
||||
|
||||
|
||||
- ***`createFailError(reason: string, options: { exitCode: number, showHelp: boolean }): FailError`***
|
||||
|
||||
Create and return an error object that, when thrown within `run(...)` can customize the failure behavior of the CLI. `reason` is printed instead of a stacktrace, `options.exitCode` customizes the exit code of the process, and `options.showHelp` will print the help text before exiting.
|
||||
|
||||
- ***`createFlagError(reason: string)`***
|
||||
|
||||
Shortcut for calling `createFailError()` with `options.showHelp`, as errors caused by invalid flags should print the help message to help users debug their usage.
|
||||
|
||||
- ***`isFailError(error: any)`***
|
||||
|
||||
Determine if a value is an error created by `createFailError(...)`.
|
|
@ -23,16 +23,31 @@ const FAIL_TAG = Symbol('fail error');
|
|||
|
||||
interface FailError extends Error {
|
||||
exitCode: number;
|
||||
showHelp: boolean;
|
||||
[FAIL_TAG]: true;
|
||||
}
|
||||
|
||||
export function createFailError(reason: string, exitCode = 1): FailError {
|
||||
interface FailErrorOptions {
|
||||
exitCode?: number;
|
||||
showHelp?: boolean;
|
||||
}
|
||||
|
||||
export function createFailError(reason: string, options: FailErrorOptions = {}): FailError {
|
||||
const { exitCode = 1, showHelp = false } = options;
|
||||
|
||||
return Object.assign(new Error(reason), {
|
||||
exitCode,
|
||||
showHelp,
|
||||
[FAIL_TAG]: true as true,
|
||||
});
|
||||
}
|
||||
|
||||
export function createFlagError(reason: string) {
|
||||
return createFailError(reason, {
|
||||
showHelp: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function isFailError(error: any): error is FailError {
|
||||
return Boolean(error && error[FAIL_TAG]);
|
||||
}
|
||||
|
@ -46,6 +61,8 @@ export function combineErrors(errors: Array<Error | FailError>) {
|
|||
.filter(isFailError)
|
||||
.reduce((acc, error) => Math.max(acc, error.exitCode), 1);
|
||||
|
||||
const showHelp = errors.some(error => isFailError(error) && error.showHelp);
|
||||
|
||||
const message = errors.reduce((acc, error) => {
|
||||
if (isFailError(error)) {
|
||||
return acc + '\n' + error.message;
|
||||
|
@ -54,5 +71,8 @@ export function combineErrors(errors: Array<Error | FailError>) {
|
|||
return acc + `\nUNHANDLED ERROR\n${inspect(error)}`;
|
||||
}, '');
|
||||
|
||||
return createFailError(`${errors.length} errors:\n${message}`, exitCode);
|
||||
return createFailError(`${errors.length} errors:\n${message}`, {
|
||||
exitCode,
|
||||
showHelp,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { relative } from 'path';
|
||||
|
||||
import dedent from 'dedent';
|
||||
import getopts from 'getopts';
|
||||
|
||||
import { Options } from './run';
|
||||
|
@ -30,23 +31,40 @@ export interface Flags {
|
|||
debug: boolean;
|
||||
help: boolean;
|
||||
_: string[];
|
||||
unexpected: string[];
|
||||
|
||||
[key: string]: undefined | boolean | string | string[];
|
||||
}
|
||||
|
||||
export function getFlags(argv: string[]): Flags {
|
||||
export function getFlags(argv: string[], options: Options): Flags {
|
||||
const unexpected: string[] = [];
|
||||
const flagOpts = options.flags || {};
|
||||
|
||||
const { verbose, quiet, silent, debug, help, _, ...others } = getopts(argv, {
|
||||
string: flagOpts.string,
|
||||
boolean: flagOpts.boolean,
|
||||
alias: {
|
||||
...(flagOpts.alias || {}),
|
||||
v: 'verbose',
|
||||
},
|
||||
default: {
|
||||
...(flagOpts.default || {}),
|
||||
verbose: false,
|
||||
quiet: false,
|
||||
silent: false,
|
||||
debug: false,
|
||||
help: false,
|
||||
},
|
||||
});
|
||||
unknown: (name: string) => {
|
||||
unexpected.push(name);
|
||||
|
||||
if (options.flags && options.flags.allowUnexpected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
} as any);
|
||||
|
||||
return {
|
||||
verbose,
|
||||
|
@ -55,22 +73,37 @@ export function getFlags(argv: string[]): Flags {
|
|||
debug,
|
||||
help,
|
||||
_,
|
||||
unexpected,
|
||||
...others,
|
||||
};
|
||||
}
|
||||
|
||||
export function getHelp(options: Options) {
|
||||
return `
|
||||
node ${relative(process.cwd(), process.argv[1])}
|
||||
const usage = options.usage || `node ${relative(process.cwd(), process.argv[1])}`;
|
||||
|
||||
${options.helpDescription || 'Runs a dev task'}
|
||||
const optionHelp = (
|
||||
dedent((options.flags && options.flags.help) || '') +
|
||||
'\n' +
|
||||
dedent`
|
||||
--verbose, -v Log verbosely
|
||||
--debug Log debug messages (less than verbose)
|
||||
--quiet Only log errors
|
||||
--silent Don't log anything
|
||||
--help Show this message
|
||||
`
|
||||
)
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.join('\n ');
|
||||
|
||||
return `
|
||||
${usage}
|
||||
|
||||
${dedent(options.description || 'Runs a dev task')
|
||||
.split('\n')
|
||||
.join('\n ')}
|
||||
|
||||
Options:
|
||||
--verbose, -v Log verbosely
|
||||
--debug Log debug messages (less than verbose)
|
||||
--quiet Only log errors
|
||||
--silent Don't log anything
|
||||
--help Show this message
|
||||
|
||||
${optionHelp}
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -18,4 +18,4 @@
|
|||
*/
|
||||
|
||||
export { run } from './run';
|
||||
export { createFailError, combineErrors, isFailError } from './fail';
|
||||
export { createFailError, createFlagError, combineErrors, isFailError } from './fail';
|
||||
|
|
|
@ -18,17 +18,27 @@
|
|||
*/
|
||||
|
||||
import { pickLevelFromFlags, ToolingLog } from '@kbn/dev-utils';
|
||||
import { isFailError } from './fail';
|
||||
import { createFlagError, isFailError } from './fail';
|
||||
import { Flags, getFlags, getHelp } from './flags';
|
||||
|
||||
type RunFn = (args: { log: ToolingLog; flags: Flags }) => Promise<void> | void;
|
||||
|
||||
export interface Options {
|
||||
helpDescription?: string;
|
||||
usage?: string;
|
||||
description?: string;
|
||||
flags?: {
|
||||
allowUnexpected?: boolean;
|
||||
help?: string;
|
||||
alias?: { [key: string]: string | string[] };
|
||||
boolean?: string[];
|
||||
string?: string[];
|
||||
default?: { [key: string]: any };
|
||||
};
|
||||
}
|
||||
|
||||
export async function run(fn: RunFn, options: Options = {}) {
|
||||
const flags = getFlags(process.argv.slice(2));
|
||||
const flags = getFlags(process.argv.slice(2), options);
|
||||
const allowUnexpected = options.flags ? options.flags.allowUnexpected : false;
|
||||
|
||||
if (flags.help) {
|
||||
process.stderr.write(getHelp(options));
|
||||
|
@ -41,10 +51,19 @@ export async function run(fn: RunFn, options: Options = {}) {
|
|||
});
|
||||
|
||||
try {
|
||||
if (!allowUnexpected && flags.unexpected.length) {
|
||||
throw createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`);
|
||||
}
|
||||
|
||||
await fn({ log, flags });
|
||||
} catch (error) {
|
||||
if (isFailError(error)) {
|
||||
log.error(error.message);
|
||||
|
||||
if (error.showHelp) {
|
||||
log.write(getHelp(options));
|
||||
}
|
||||
|
||||
process.exit(error.exitCode);
|
||||
} else {
|
||||
log.error('UNHANDLED ERROR');
|
||||
|
|
|
@ -104,5 +104,10 @@ run(
|
|||
log.error(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
flags: {
|
||||
allowUnexpected: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -58,5 +58,10 @@ run(
|
|||
resolve(outputDir, 'en.json'),
|
||||
outputFormat === 'json5' ? serializeToJson5(sortedMessages) : serializeToJson(sortedMessages)
|
||||
);
|
||||
},
|
||||
{
|
||||
flags: {
|
||||
allowUnexpected: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -87,5 +87,10 @@ run(
|
|||
config,
|
||||
log,
|
||||
});
|
||||
},
|
||||
{
|
||||
flags: {
|
||||
allowUnexpected: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -55,5 +55,5 @@ export function lintFiles(log, files) {
|
|||
}
|
||||
|
||||
log.error(sassLint.format(report));
|
||||
throw createFailError(`[sasslint] ${failTypes.join(' & ')}`, 1);
|
||||
throw createFailError(`[sasslint] ${failTypes.join(' & ')}`);
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export async function lintFiles(log: ToolingLog, files: File[]) {
|
|||
);
|
||||
|
||||
if (exitCode > 0) {
|
||||
throw createFailError(`[tslint] failure`, 1);
|
||||
throw createFailError(`[tslint] failure`);
|
||||
} else {
|
||||
log.success('[tslint/%s] %d files linted successfully', project.name, filesInProject.length);
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ export async function runCheckTsProjectsCli() {
|
|||
process.exit(1);
|
||||
},
|
||||
{
|
||||
helpDescription:
|
||||
description:
|
||||
'Check that all .ts and .tsx files in the repository are assigned to a tsconfig.json file',
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue