Skip to content

Commit

Permalink
fix(cli): support Jest-specific CLI flag aliases (#4124)
Browse files Browse the repository at this point in the history
This adds support for the following Jest-specific CLI flag aliases:

- "b" for "bail"
- "e" for "expand"
- "w" for "maxWorkers"
- "o" for "onlyChanged"
- "f" for "onlyFailures"
- "i" for "runInBand"
- "t" for "testNamePattern"
- "u" for "updateSnapshot"

Two additional flags supported by Jest ('c' for 'config' and 'h' for
'help') were already supported by Stencil.

This also makes a small refactor to the CLI argument parser, mainly
building up the `flags.unknownArgs` array proactively whenever we're in
a situation where we don't know what to do, rather than filtering all
the arguments after the fact.
  • Loading branch information
alicewriteswrongs committed Mar 13, 2023
1 parent 23a73f0 commit 56389a4
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 31 deletions.
14 changes: 13 additions & 1 deletion src/cli/config-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,25 @@ export const CLI_FLAG_ALIASES: AliasMap = {
h: 'help',
p: 'port',
v: 'version',

// JEST SPECIFIC CLI FLAGS
// these are defined in
// https://github.com/facebook/jest/blob/4156f86/packages/jest-cli/src/args.ts
b: 'bail',
e: 'expand',
f: 'onlyFailures',
i: 'runInBand',
o: 'onlyChanged',
t: 'testNamePattern',
u: 'updateSnapshot',
w: 'maxWorkers',
};

/**
* A regular expression which can be used to match a CLI flag for one of our
* short aliases.
*/
export const CLI_FLAG_REGEX = new RegExp(`^-[chpv]{1}$`);
export const CLI_FLAG_REGEX = new RegExp(`^-[chpvbewofitu]{1}$`);

/**
* Given two types `K` and `T` where `K` extends `ReadonlyArray<string>`,
Expand Down
75 changes: 45 additions & 30 deletions src/cli/parse-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,6 @@ export const parseFlags = (args: string[]): ConfigFlags => {
}
}

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

return flags;
};

Expand Down Expand Up @@ -131,13 +123,13 @@ const parseCLITerm = (flags: ConfigFlags, args: string[]) => {
else if (arg.startsWith('-') && arg.includes('=')) {
// we're dealing with an AliasEqualsArg, we have a special helper for that
const [originalArg, value] = parseEqualsArg(arg);
setCLIArg(flags, arg.split('=')[0], normalizeFlagName(originalArg), value);
setCLIArg(flags, desugarRawAlias(originalArg), normalizeFlagName(originalArg), value);
}

// AliasArg → "-" AliasName ( " " CLIValue )? ;
else if (CLI_FLAG_REGEX.test(arg)) {
// this is a short alias, like `-c` for Config
setCLIArg(flags, arg, normalizeFlagName(arg), parseCLIValue(args));
setCLIArg(flags, desugarRawAlias(arg), normalizeFlagName(arg), parseCLIValue(args));
}

// NegativeDashArg → "--no-" ArgName ;
Expand All @@ -164,13 +156,14 @@ const parseCLITerm = (flags: ConfigFlags, args: string[]) => {
// SimpleArg → "--" ArgName ( " " CLIValue )? ;
else if (arg.startsWith('--') && arg.length > '--'.length) {
setCLIArg(flags, arg, normalizeFlagName(arg), parseCLIValue(args));
} else {
// if we get here then `arg` is not an argument in our list of supported
// arguments. This doesn't necessarily mean we want to report an error or
// anything though! Instead, with unknown / unrecognized arguments we want
// to stick them into the `unknownArgs` array, which is used when we pass
// CLI args to Jest, for instance.
flags.unknownArgs.push(arg);
}

// if we get here it is not an argument in our list of supported arguments.
// This doesn't necessarily mean we want to report an error or anything
// though! Instead, with unknown / unrecognized arguments we stick them into
// the `unknownArgs` array, which is used when we pass CLI args to Jest, for
// instance. So we just return void here.
};

/**
Expand Down Expand Up @@ -219,7 +212,7 @@ const normalizeFlagName = (flagName: string): string => {
* @param value the raw value to be set onto the config flags object
*/
const setCLIArg = (flags: ConfigFlags, rawArg: string, normalizedArg: string, value: CLIValueResult) => {
normalizedArg = dereferenceAlias(normalizedArg);
normalizedArg = desugarAlias(normalizedArg);

// We're setting a boolean!
if (readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizedArg)) {
Expand Down Expand Up @@ -320,6 +313,14 @@ const setCLIArg = (flags: ConfigFlags, rawArg: string, normalizedArg: string, va
} else {
throwCLIParsingError(rawArg, 'expected to receive a valid log level but received nothing');
}
} else {
// we haven't found this flag in any of our lists of arguments, so we
// should put it in our list of unknown arguments
flags.unknownArgs.push(rawArg);

if (typeof value === 'string') {
flags.unknownArgs.push(value);
}
}
};

Expand Down Expand Up @@ -355,9 +356,10 @@ type CLIValueResult = string | typeof Empty;
* A little helper which tries to parse a CLI value (as opposed to a flag) off
* of the argument array.
*
* We support a variety of different argument formats, but all of them start
* with `-`, so we can check the first character to test whether the next token
* in our array of CLI arguments is a flag name or a value.
* We support a variety of different argument formats for flags (as opposed to
* values), but all of them start with `-`, so we can check the first character
* to test whether the next token in our array of CLI arguments is a flag name
* or a value.
*
* @param args an array of CLI args
* @returns either a string result or an Empty sentinel
Expand Down Expand Up @@ -454,22 +456,35 @@ const throwNumberParsingError = (flag: string, value: string) => {
};

/**
* A little helper to 'dereference' a flag alias, which if you squint a little
* you can think of like a pointer to a full flag name. Thus 'c' is like a
* pointer to 'config', so here we're doing something like `*c`. Of course, this
* being JS, this is just a metaphor!
* A little helper to 'desugar' a flag alias, meaning expand it to its full
* name. For instance, the alias `"c"` will desugar to `"config"`.
*
* If no 'dereference' is found for the possible alias we just return the
* passed string unmodified.
* If no expansion is found for the possible alias we just return the passed
* string unmodified.
*
* @param maybeAlias a string which _could_ be an alias to a full flag name
* @returns the full aliased flag name, if found, or the passed string if not
*/
const dereferenceAlias = (maybeAlias: string): string => {
const possibleDereference = CLI_FLAG_ALIASES[maybeAlias];
const desugarAlias = (maybeAlias: string): string => {
const possiblyDesugared = CLI_FLAG_ALIASES[maybeAlias];

if (typeof possibleDereference === 'string') {
return possibleDereference;
if (typeof possiblyDesugared === 'string') {
return possiblyDesugared;
}
return maybeAlias;
};

/**
* Desugar a 'raw' alias (with a leading dash) and return an equivalent,
* desugared argument.
*
* For instance, passing `"-c` will return `"--config"`.
*
* The reason we'd like to do this is not so much for our own code, but so that
* we can transform an alias like `"-u"` to `"--updateSnapshot"` in order to
* pass it along to Jest.
*
* @param rawAlias a CLI flag alias as found on the command line (like `"-c"`)
* @returns an equivalent full command (like `"--config"`)
*/
const desugarRawAlias = (rawAlias: string): string => '--' + desugarAlias(normalizeFlagName(rawAlias));
37 changes: 37 additions & 0 deletions src/cli/test/parse-flags.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toDashCase } from '@utils';
import { LogLevel } from '../../declarations';
import {
BOOLEAN_CLI_FLAGS,
ConfigFlags,
NUMBER_CLI_FLAGS,
STRING_ARRAY_CLI_FLAGS,
STRING_CLI_FLAGS,
Expand Down Expand Up @@ -230,11 +231,47 @@ describe('parseFlags', () => {
it('should parse -c /my-config.js', () => {
const flags = parseFlags(['-c', '/my-config.js']);
expect(flags.config).toBe('/my-config.js');
expect(flags.knownArgs).toEqual(['--config', '/my-config.js']);
});

it('should parse -c=/my-config.js', () => {
const flags = parseFlags(['-c=/my-config.js']);
expect(flags.config).toBe('/my-config.js');
expect(flags.knownArgs).toEqual(['--config', '/my-config.js']);
});
});

describe('Jest aliases', () => {
it.each([
['w', 'maxWorkers', '4'],
['t', 'testNamePattern', 'testname'],
])('should support the string Jest alias %p for %p', (alias, fullArgument, value) => {
const flags = parseFlags([`-${alias}`, value]);
expect(flags.knownArgs).toEqual([`--${fullArgument}`, value]);
expect(flags.unknownArgs).toHaveLength(0);
});

it.each([
['w', 'maxWorkers', '4'],
['t', 'testNamePattern', 'testname'],
])('should support the string Jest alias %p for %p in an AliasEqualsArg', (alias, fullArgument, value) => {
const flags = parseFlags([`-${alias}=${value}`]);
expect(flags.knownArgs).toEqual([`--${fullArgument}`, value]);
expect(flags.unknownArgs).toHaveLength(0);
});

it.each<[string, keyof ConfigFlags]>([
['b', 'bail'],
['e', 'expand'],
['o', 'onlyChanged'],
['f', 'onlyFailures'],
['i', 'runInBand'],
['u', 'updateSnapshot'],
])('should support the boolean Jest alias %p for %p', (alias, fullArgument) => {
const flags = parseFlags([`-${alias}`]);
expect(flags.knownArgs).toEqual([`--${fullArgument}`]);
expect(flags[fullArgument]).toBe(true);
expect(flags.unknownArgs).toHaveLength(0);
});
});
});
Expand Down
40 changes: 40 additions & 0 deletions src/testing/jest/test/jest-config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Config } from '@jest/types';
import type * as d from '@stencil/core/declarations';
import { mockValidatedConfig } from '@stencil/core/testing';
import path from 'path';
Expand Down Expand Up @@ -225,4 +226,43 @@ describe('jest-config', () => {
// the `_` field holds any filename pattern matches
expect(jestArgv._).toEqual(['foobar/*', 'my-spec.ts']);
});

describe('Jest aliases', () => {
it('should support the string Jest alias "-w=4" for maxWorkers', () => {
const args = ['test', '--spec', '-w=4'];
const jestArgv = buildJestArgv(mockValidatedConfig({ flags: parseFlags(args) }));
expect(jestArgv.maxWorkers).toBe(4);
});

it('should support the string Jest alias "-w 4" for maxWorkers', () => {
const args = ['test', '--spec', '-w', '4'];
const jestArgv = buildJestArgv(mockValidatedConfig({ flags: parseFlags(args) }));
expect(jestArgv.maxWorkers).toBe(4);
});

it('should support the string Jest alias "-t" for testNamePattern', () => {
const args = ['test', '--spec', '-t=my-test-pattern'];
const jestArgv = buildJestArgv(mockValidatedConfig({ flags: parseFlags(args) }));
expect(jestArgv.testNamePattern).toBe('my-test-pattern');
});

it('should support the string Jest alias "-t pattern" for testNamePattern', () => {
const args = ['test', '--spec', '-t', 'my-test-pattern'];
const jestArgv = buildJestArgv(mockValidatedConfig({ flags: parseFlags(args) }));
expect(jestArgv.testNamePattern).toBe('my-test-pattern');
});

it.each<[string, keyof Config.Argv]>([
['b', 'bail'],
['e', 'expand'],
['o', 'onlyChanged'],
['f', 'onlyFailures'],
['i', 'runInBand'],
['u', 'updateSnapshot'],
])('should support the boolean Jest alias %p for %p', (alias, fullArgument) => {
const args = ['test', '--spec', `-${alias}`];
const jestArgv = buildJestArgv(mockValidatedConfig({ flags: parseFlags(args) }));
expect(jestArgv[fullArgument]).toBe(true);
});
});
});

0 comments on commit 56389a4

Please sign in to comment.