Skip to content

Commit 97e1bf8

Browse files
Spencerspalger
andauthored
[7.x] [kbn/dev-utils] add RunWithCommands utility (#72311) (#72342)
Co-authored-by: spalger <spalger@users.noreply.github.com>
1 parent 4defcd6 commit 97e1bf8

File tree

10 files changed

+743
-138
lines changed

10 files changed

+743
-138
lines changed

packages/kbn-dev-utils/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export {
3333
KBN_P12_PATH,
3434
KBN_P12_PASSWORD,
3535
} from './certs';
36-
export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run';
3736
export { REPO_ROOT } from './repo_root';
3837
export { KbnClient } from './kbn_client';
38+
export * from './run';
3939
export * from './axios';
4040
export * from './stdio';
4141
export * from './ci_stats_reporter';
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { inspect } from 'util';
21+
22+
import exitHook from 'exit-hook';
23+
24+
import { ToolingLog } from '../tooling_log';
25+
import { isFailError } from './fail';
26+
27+
export type CleanupTask = () => void;
28+
29+
export class Cleanup {
30+
static setup(log: ToolingLog, helpText: string) {
31+
const onUnhandledRejection = (error: any) => {
32+
log.error('UNHANDLED PROMISE REJECTION');
33+
log.error(
34+
error instanceof Error
35+
? error
36+
: new Error(`non-Error type rejection value: ${inspect(error)}`)
37+
);
38+
process.exit(1);
39+
};
40+
41+
process.on('unhandledRejection', onUnhandledRejection);
42+
43+
const cleanup = new Cleanup(log, helpText, [
44+
() => process.removeListener('unhandledRejection', onUnhandledRejection),
45+
]);
46+
47+
cleanup.add(exitHook(() => cleanup.execute()));
48+
49+
return cleanup;
50+
}
51+
52+
constructor(
53+
private readonly log: ToolingLog,
54+
public helpText: string,
55+
private readonly tasks: CleanupTask[]
56+
) {}
57+
58+
add(task: CleanupTask) {
59+
this.tasks.push(task);
60+
}
61+
62+
execute(topLevelError?: any) {
63+
const tasks = this.tasks.slice(0);
64+
this.tasks.length = 0;
65+
66+
for (const task of tasks) {
67+
try {
68+
task();
69+
} catch (error) {
70+
this.onError(error);
71+
}
72+
}
73+
74+
if (topLevelError) {
75+
this.onError(topLevelError);
76+
}
77+
}
78+
79+
private onError(error: any) {
80+
if (isFailError(error)) {
81+
this.log.error(error.message);
82+
83+
if (error.showHelp) {
84+
this.log.write(this.helpText);
85+
}
86+
87+
process.exitCode = error.exitCode;
88+
} else {
89+
this.log.error('UNHANDLED ERROR');
90+
this.log.error(error);
91+
process.exitCode = 1;
92+
}
93+
}
94+
}

packages/kbn-dev-utils/src/run/flags.test.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ import { getFlags } from './flags';
2222
it('gets flags correctly', () => {
2323
expect(
2424
getFlags(['-a', '--abc=bcd', '--foo=bar', '--no-bar', '--foo=baz', '--box', 'yes', '-zxy'], {
25-
flags: {
26-
boolean: ['x'],
27-
string: ['abc'],
28-
alias: {
29-
x: 'extra',
30-
},
31-
allowUnexpected: true,
25+
boolean: ['x'],
26+
string: ['abc'],
27+
alias: {
28+
x: 'extra',
3229
},
30+
allowUnexpected: true,
3331
})
3432
).toMatchInlineSnapshot(`
3533
Object {
@@ -60,10 +58,8 @@ it('gets flags correctly', () => {
6058
it('guesses types for unexpected flags', () => {
6159
expect(
6260
getFlags(['-abc', '--abc=bcd', '--no-foo', '--bar'], {
63-
flags: {
64-
allowUnexpected: true,
65-
guessTypesForUnexpectedFlags: true,
66-
},
61+
allowUnexpected: true,
62+
guessTypesForUnexpectedFlags: true,
6763
})
6864
).toMatchInlineSnapshot(`
6965
Object {

packages/kbn-dev-utils/src/run/flags.ts

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,9 @@
1717
* under the License.
1818
*/
1919

20-
import { relative } from 'path';
21-
22-
import dedent from 'dedent';
2320
import getopts from 'getopts';
2421

25-
import { Options } from './run';
22+
import { RunOptions } from './run';
2623

2724
export interface Flags {
2825
verbose: boolean;
@@ -36,23 +33,52 @@ export interface Flags {
3633
[key: string]: undefined | boolean | string | string[];
3734
}
3835

39-
export function getFlags(argv: string[], options: Options): Flags {
36+
export interface FlagOptions {
37+
allowUnexpected?: boolean;
38+
guessTypesForUnexpectedFlags?: boolean;
39+
help?: string;
40+
alias?: { [key: string]: string | string[] };
41+
boolean?: string[];
42+
string?: string[];
43+
default?: { [key: string]: any };
44+
}
45+
46+
export function mergeFlagOptions(global: FlagOptions = {}, local: FlagOptions = {}): FlagOptions {
47+
return {
48+
alias: {
49+
...global.alias,
50+
...local.alias,
51+
},
52+
boolean: [...(global.boolean || []), ...(local.boolean || [])],
53+
string: [...(global.string || []), ...(local.string || [])],
54+
default: {
55+
...global.alias,
56+
...local.alias,
57+
},
58+
59+
help: local.help,
60+
61+
allowUnexpected: !!(global.allowUnexpected || local.allowUnexpected),
62+
guessTypesForUnexpectedFlags: !!(global.allowUnexpected || local.allowUnexpected),
63+
};
64+
}
65+
66+
export function getFlags(argv: string[], flagOptions: RunOptions['flags'] = {}): Flags {
4067
const unexpectedNames = new Set<string>();
41-
const flagOpts = options.flags || {};
4268

4369
const { verbose, quiet, silent, debug, help, _, ...others } = getopts(argv, {
44-
string: flagOpts.string,
45-
boolean: [...(flagOpts.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'],
70+
string: flagOptions.string,
71+
boolean: [...(flagOptions.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'],
4672
alias: {
47-
...(flagOpts.alias || {}),
73+
...flagOptions.alias,
4874
v: 'verbose',
4975
},
50-
default: flagOpts.default,
76+
default: flagOptions.default,
5177
unknown: (name: string) => {
5278
unexpectedNames.add(name);
53-
return flagOpts.guessTypesForUnexpectedFlags;
79+
return !!flagOptions.guessTypesForUnexpectedFlags;
5480
},
55-
} as any);
81+
});
5682

5783
const unexpected: string[] = [];
5884
for (const unexpectedName of unexpectedNames) {
@@ -119,32 +145,3 @@ export function getFlags(argv: string[], options: Options): Flags {
119145
...others,
120146
};
121147
}
122-
123-
export function getHelp(options: Options) {
124-
const usage = options.usage || `node ${relative(process.cwd(), process.argv[1])}`;
125-
126-
const optionHelp = (
127-
dedent(options?.flags?.help || '') +
128-
'\n' +
129-
dedent`
130-
--verbose, -v Log verbosely
131-
--debug Log debug messages (less than verbose)
132-
--quiet Only log errors
133-
--silent Don't log anything
134-
--help Show this message
135-
`
136-
)
137-
.split('\n')
138-
.filter(Boolean)
139-
.join('\n ');
140-
141-
return `
142-
${usage}
143-
144-
${dedent(options.description || 'Runs a dev task')
145-
.split('\n')
146-
.join('\n ')}
147-
148-
Options:
149-
${optionHelp + '\n\n'}`;
150-
}

0 commit comments

Comments
 (0)