Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(jest-validate): Allow deprecation warnings for unknown options #14499

1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
### Features

- `[create-jest]` Add `npm init` / `yarn create` initialiser for Jest projects ([#14465](https://github.com/jestjs/jest/pull/14453))
- `[jest-validate]` Allow deprecation warnings for unknown options ([#14499](https://github.com/jestjs/jest/pull/14499))

### Fixes

Expand Down
20 changes: 20 additions & 0 deletions packages/jest-validate/README.md
Expand Up @@ -194,3 +194,23 @@ Custom Deprecation:

Documentation: http://custom-docs.com
```

## Example validating CLI arguments

```js
import {validate} from 'jest-validate';

validateCLIOptions(argv, {...allowedOptions, deprecatedOptions});
```

If `argv` contains a deprecated option that is not specifid in `allowedOptions`, `validateCLIOptions` will throw an error with the message specified in the `deprecatedOptions` config:

```bash
● collectCoverageOnlyFrom:

Option "collectCoverageOnlyFrom" was replaced by "collectCoverageFrom"

CLI Options Documentation: https://jestjs.io/docs/en/cli.html
```

If the deprecation option is still listed in the `allowedOptions` config, then `validateCLIOptions` will print the warning wihout throwing an error.
Expand Up @@ -31,6 +31,26 @@ exports[`fails for unknown option 1`] = `
<red></color>"
`;

exports[`handles deprecated CLI options print warning for deprecated options that are listed in config 1`] = `
"<yellow><bold>foo</intensity>:</color>
<yellow></color>
<yellow>Deprecation message</color>
<yellow></color>
<yellow> <bold>CLI Options Documentation:</intensity></color>
<yellow> https://jestjs.io/docs/cli</color>
<yellow></color>"
`;

exports[`handles deprecated CLI options throw an error for deprecated options that are not listed in config 1`] = `
"<red><bold>foo</intensity>:</color>
<red></color>
<red>Deprecation message</color>
<red></color>
<red> <bold>CLI Options Documentation:</intensity></color>
<red> https://jestjs.io/docs/cli</color>
<red></color>"
`;

exports[`shows suggestion when unrecognized cli param length > 1 1`] = `
"<red><bold><bold>●</intensity><bold> Unrecognized CLI Parameter</intensity>:</color>
<red></color>
Expand Down
47 changes: 47 additions & 0 deletions packages/jest-validate/src/__tests__/validateCLIOptions.test.ts
Expand Up @@ -6,6 +6,7 @@
*
*/

import type {DeprecatedOptions} from '../types';
import validateCLIOptions from '../validateCLIOptions';

test('validates yargs special options', () => {
Expand Down Expand Up @@ -59,3 +60,49 @@ test('shows suggestion when unrecognized cli param length > 1', () => {

expect(() => validateCLIOptions(argv)).toThrowErrorMatchingSnapshot();
});

describe('handles deprecated CLI options', () => {
beforeEach(() => {
jest.spyOn(console, 'warn');
});

afterEach(() => {
jest.mocked(console.warn).mockRestore();
});

test('print warning for deprecated options that are listed in config', () => {
const optionName = 'foo';
const argv = {
$0: 'foo',
_: ['bar'],
[optionName]: true,
};

validateCLIOptions(argv, {
deprecationEntries: {
[optionName]: () => 'Deprecation message',
} as DeprecatedOptions,
[optionName]: {},
});

expect(jest.mocked(console.warn).mock.calls[0][0]).toMatchSnapshot();
});

test('throw an error for deprecated options that are not listed in config', () => {
const optionName = 'foo';

const argv = {
$0: 'foo',
_: ['bar'],
[optionName]: true,
};

expect(() =>
validateCLIOptions(argv, {
deprecationEntries: {
[optionName]: () => 'Deprecation message',
} as DeprecatedOptions,
}),
).toThrowErrorMatchingSnapshot();
});
});
2 changes: 2 additions & 0 deletions packages/jest-validate/src/types.ts
Expand Up @@ -15,6 +15,8 @@ export type DeprecatedOptionFunc = (arg: Record<string, unknown>) => string;

export type DeprecatedOptions = Record<string, DeprecatedOptionFunc>;

export type DeprecationItem = {fatal: boolean; name: string};

export type ValidationOptions = {
comment?: string;
condition?: (option: unknown, validOption: unknown) => boolean;
Expand Down
68 changes: 41 additions & 27 deletions packages/jest-validate/src/validateCLIOptions.ts
Expand Up @@ -9,10 +9,17 @@ import camelcase = require('camelcase');
import chalk = require('chalk');
import type {Options} from 'yargs';
import type {Config} from '@jest/types';
import defaultConfig from './defaultConfig';
import {deprecationWarning} from './deprecated';
import type {DeprecatedOptionFunc, DeprecatedOptions} from './types';
import {ValidationError, createDidYouMeanMessage, format} from './utils';
import type {
DeprecatedOptionFunc,
DeprecatedOptions,
DeprecationItem,
} from './types';
import {
ValidationError,
createDidYouMeanMessage,
format,
logValidationWarning,
} from './utils';

const BULLET: string = chalk.bold('\u25cf');
export const DOCUMENTATION_NOTE = ` ${chalk.bold('CLI Options Documentation:')}
Expand Down Expand Up @@ -48,16 +55,21 @@ const createCLIValidationError = (
return new ValidationError(title, message, comment);
};

const logDeprecatedOptions = (
deprecatedOptions: Array<string>,
const validateDeprecatedOptions = (
deprecatedOptions: Array<DeprecationItem>,
deprecationEntries: DeprecatedOptions,
argv: Config.Argv,
) => {
deprecatedOptions.forEach(opt => {
deprecationWarning(argv, opt, deprecationEntries, {
...defaultConfig,
comment: DOCUMENTATION_NOTE,
});
const name = opt.name;
const message = deprecationEntries[name](argv);
const comment = DOCUMENTATION_NOTE;

if (opt.fatal) {
throw new ValidationError(name, message, comment);
} else {
logValidationWarning(name, message, comment);
}
});
};

Expand All @@ -69,29 +81,19 @@ export default function validateCLIOptions(
rawArgv: Array<string> = [],
): boolean {
const yargsSpecialOptions = ['$0', '_', 'help', 'h'];
const deprecationEntries = options.deprecationEntries ?? {};

const allowedOptions = Object.keys(options).reduce(
(acc, option) =>
acc.add(option).add((options[option].alias as string) || option),
new Set(yargsSpecialOptions),
);
const unrecognizedOptions = Object.keys(argv).filter(
arg =>
!allowedOptions.has(camelcase(arg, {locale: 'en-US'})) &&
!allowedOptions.has(arg) &&
(!rawArgv.length || rawArgv.includes(arg)),
[],
);

if (unrecognizedOptions.length) {
throw createCLIValidationError(unrecognizedOptions, allowedOptions);
}

const deprecationEntries = options.deprecationEntries ?? {};
const CLIDeprecations = Object.keys(deprecationEntries).reduce<
Record<string, DeprecatedOptionFunc>
>((acc, entry) => {
acc[entry] = deprecationEntries[entry];
if (options[entry]) {
acc[entry] = deprecationEntries[entry];
const alias = options[entry].alias as string;
if (alias) {
acc[alias] = deprecationEntries[entry];
Expand All @@ -100,12 +102,24 @@ export default function validateCLIOptions(
return acc;
}, {});
const deprecations = new Set(Object.keys(CLIDeprecations));
const deprecatedOptions = Object.keys(argv).filter(
arg => deprecations.has(arg) && argv[arg] != null,
);
const deprecatedOptions = Object.keys(argv)
.filter(arg => deprecations.has(arg) && argv[arg] != null)
.map(arg => ({fatal: !allowedOptions.has(arg), name: arg}));

if (deprecatedOptions.length) {
logDeprecatedOptions(deprecatedOptions, CLIDeprecations, argv);
validateDeprecatedOptions(deprecatedOptions, CLIDeprecations, argv);
}

const unrecognizedOptions = Object.keys(argv).filter(
arg =>
!allowedOptions.has(camelcase(arg, {locale: 'en-US'})) &&
!allowedOptions.has(arg) &&
(!rawArgv.length || rawArgv.includes(arg)),
[],
);

if (unrecognizedOptions.length) {
throw createCLIValidationError(unrecognizedOptions, allowedOptions);
}

return true;
Expand Down