Skip to content

Commit 65f5275

Browse files
fix(cli): fix bug with parsing --fooBar=baz type CLI flags (#3483)
This fixes a bug with how these flags were being parsed which was messing with Jest. Basically, the issue related to how the `knownArgs` array on the `ConfigFlags` object was being populated. For CLI key-value args of the format `--argName=value` the whole string `"--argName=value"` was being added _as well as_ the value string `"value"`, which had the effect of passing duplicate args to Jest. This refactors how such arguments are handled to always parse apart the argument name and the value, adding only the argument name and then the argument value, so that if you pass, for instance, `--outputFile=output.json`, the args passed to Jest will look like ```ts ['--outputFile', 'output.json'] ``` See #3471 and #3481 for more details
1 parent 835e00f commit 65f5275

File tree

3 files changed

+83
-13
lines changed

3 files changed

+83
-13
lines changed

src/cli/parse-flags.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ export const parseFlags = (args: string[], sys?: CompilerSystem): ConfigFlags =>
5151
}
5252
}
5353

54-
flags.unknownArgs = flags.args.filter((arg: string) => {
55-
return !flags.knownArgs.includes(arg);
56-
});
54+
// to find unknown / unrecognized arguments we filter `args`, including only
55+
// arguments whose normalized form is not found in `knownArgs`. `knownArgs`
56+
// is populated during the call to `parseArgs` above. For arguments like
57+
// `--foobar` the string `"--foobar"` will be added, while for more
58+
// complicated arguments like `--bizBoz=bop` or `--bizBoz bop` just the
59+
// string `"--bizBoz"` will be added.
60+
flags.unknownArgs = flags.args.filter((arg: string) => !flags.knownArgs.includes(parseEqualsArg(arg)[0]));
5761

5862
return flags;
5963
};
@@ -259,17 +263,18 @@ const getValue = (
259263

260264
let value: string | undefined;
261265
let matchingArg: string | undefined;
266+
262267
args.forEach((arg, i) => {
263268
if (arg.startsWith(`--${dashCaseName}=`) || arg.startsWith(`--${configCaseName}=`)) {
264-
value = getEqualsValue(arg);
265-
matchingArg = arg;
269+
// our argument was passed at the command-line in the format --argName=arg-value
270+
[matchingArg, value] = parseEqualsArg(arg);
266271
} else if (arg === `--${dashCaseName}` || arg === `--${configCaseName}`) {
272+
// the next value in the array is assumed to be a value for this argument
267273
value = args[i + 1];
268274
matchingArg = arg;
269275
} else if (alias) {
270276
if (arg.startsWith(`-${alias}=`)) {
271-
value = getEqualsValue(arg);
272-
matchingArg = arg;
277+
[matchingArg, value] = parseEqualsArg(arg);
273278
} else if (arg === `-${alias}`) {
274279
value = args[i + 1];
275280
matchingArg = arg;
@@ -287,13 +292,43 @@ interface CLIArgValue {
287292
}
288293

289294
/**
290-
* When a parameter is set in the format `--foobar=12` at the CLI (as opposed to
291-
* `--foobar 12`) we want to get the value after the `=` sign
295+
* Parse an 'equals' argument, which is a CLI argument-value pair in the
296+
* format `--foobar=12` (as opposed to a space-separated format like
297+
* `--foobar 12`).
298+
*
299+
* To parse this we split on the `=`, returning the first part as the argument
300+
* name and the second part as the value. We join the value on `"="` in case
301+
* there is another `"="` in the argument.
302+
*
303+
* This function is safe to call with any arg, and can therefore be used as
304+
* an argument 'normalizer'. If CLI argument is not an 'equals' argument then
305+
* the return value will be a tuple of the original argument and an empty
306+
* string `""` for the value.
307+
*
308+
* In code terms, if you do:
292309
*
293-
* @param commandArgument the arg in question
294-
* @returns the value after the `=`
310+
* ```ts
311+
* const [arg, value] = parseEqualsArg("--myArgument")
312+
* ```
313+
*
314+
* Then `arg` will be `"--myArgument"` and `value` will be `""`, whereas if
315+
* you do:
316+
*
317+
*
318+
* ```ts
319+
* const [arg, value] = parseEqualsArg("--myArgument=myValue")
320+
* ```
321+
*
322+
* Then `arg` will be `"--myArgument"` and `value` will be `"myValue"`.
323+
*
324+
* @param arg the arg in question
325+
* @returns a tuple containing the arg name and the value (if present)
295326
*/
296-
const getEqualsValue = (commandArgument: string) => commandArgument.split('=').slice(1).join('=');
327+
export const parseEqualsArg = (arg: string): [string, string] => {
328+
const [originalArg, ...value] = arg.split('=');
329+
330+
return [originalArg, value.join('=')];
331+
};
297332

298333
/**
299334
* Small helper for getting type-system-level assurance that a `string` can be

src/cli/test/parse-flags.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type * as d from '../../declarations';
22
import { LogLevel } from '../../declarations';
33
import { BOOLEAN_CLI_ARGS, STRING_CLI_ARGS, NUMBER_CLI_ARGS } from '../config-flags';
4-
import { parseFlags } from '../parse-flags';
4+
import { parseEqualsArg, parseFlags } from '../parse-flags';
55

66
describe('parseFlags', () => {
77
let args: string[] = [];
@@ -190,19 +190,29 @@ describe('parseFlags', () => {
190190
it.each(STRING_CLI_ARGS)('should parse string flag %s', (cliArg) => {
191191
const flags = parseFlags([`--${cliArg}`, 'test-value'], sys);
192192
expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value']);
193+
expect(flags.unknownArgs).toEqual([]);
193194
expect(flags[cliArg]).toBe('test-value');
194195
});
195196

197+
it.each(STRING_CLI_ARGS)('should parse string flag --%s=value', (cliArg) => {
198+
const flags = parseFlags([`--${cliArg}=path/to/file.js`], sys);
199+
expect(flags.knownArgs).toEqual([`--${cliArg}`, 'path/to/file.js']);
200+
expect(flags.unknownArgs).toEqual([]);
201+
expect(flags[cliArg]).toBe('path/to/file.js');
202+
});
203+
196204
it.each(NUMBER_CLI_ARGS)('should parse number flag %s', (cliArg) => {
197205
const flags = parseFlags([`--${cliArg}`, '42'], sys);
198206
expect(flags.knownArgs).toEqual([`--${cliArg}`, '42']);
207+
expect(flags.unknownArgs).toEqual([]);
199208
expect(flags[cliArg]).toBe(42);
200209
});
201210

202211
it('should parse --compare', () => {
203212
args[0] = '--compare';
204213
const flags = parseFlags(args, sys);
205214
expect(flags.knownArgs).toEqual(['--compare']);
215+
expect(flags.unknownArgs).toEqual([]);
206216
expect(flags.compare).toBe(true);
207217
});
208218

@@ -574,4 +584,18 @@ describe('parseFlags', () => {
574584
expect(flags.help).toBe(true);
575585
expect(flags.config).toBe('./myconfig.json');
576586
});
587+
588+
describe('parseEqualsArg', () => {
589+
it.each([
590+
['--fooBar=baz', '--fooBar', 'baz'],
591+
['--foo-bar=4', '--foo-bar', '4'],
592+
['--fooBar=twenty=3*4', '--fooBar', 'twenty=3*4'],
593+
['--fooBar', '--fooBar', ''],
594+
['--foo-bar', '--foo-bar', ''],
595+
])('should parse %s correctly', (testArg, expectedArg, expectedValue) => {
596+
const [arg, value] = parseEqualsArg(testArg);
597+
expect(arg).toBe(expectedArg);
598+
expect(value).toBe(expectedValue);
599+
});
600+
});
577601
});

src/testing/jest/test/jest-config.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ describe('jest-config', () => {
1919
expect(jestArgv.maxWorkers).toBe(2);
2020
});
2121

22+
it('marks outputFile as a Jest argument', () => {
23+
const args = ['test', '--ci', '--outputFile=path/to/my-file'];
24+
const config = mockValidatedConfig();
25+
config.testing = {};
26+
config.flags = parseFlags(args, config.sys);
27+
expect(config.flags.args).toEqual(['--ci', '--outputFile=path/to/my-file']);
28+
expect(config.flags.unknownArgs).toEqual([]);
29+
const jestArgv = buildJestArgv(config);
30+
expect(jestArgv.outputFile).toBe('path/to/my-file');
31+
});
32+
2233
it('pass --maxWorkers=2 arg when e2e test and --ci', () => {
2334
const args = ['test', '--ci', '--e2e', '--max-workers=2'];
2435
const config = mockValidatedConfig();

0 commit comments

Comments
 (0)