From f0cfd50545daa56061af97179c28c067d017adb2 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 11 Sep 2023 22:00:45 +0400 Subject: [PATCH] feat(jest-validate): Allow deprecation warnings for unknown options (#14499) --- CHANGELOG.md | 1 + packages/jest-validate/README.md | 20 ++++++ .../validateCLIOptions.test.ts.snap | 20 ++++++ .../src/__tests__/validateCLIOptions.test.ts | 47 +++++++++++++ packages/jest-validate/src/types.ts | 2 + .../jest-validate/src/validateCLIOptions.ts | 68 +++++++++++-------- 6 files changed, 131 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 676e0753208a..6066b005efcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/jest-validate/README.md b/packages/jest-validate/README.md index 2406bd568ec2..41b275ce415a 100644 --- a/packages/jest-validate/README.md +++ b/packages/jest-validate/README.md @@ -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. diff --git a/packages/jest-validate/src/__tests__/__snapshots__/validateCLIOptions.test.ts.snap b/packages/jest-validate/src/__tests__/__snapshots__/validateCLIOptions.test.ts.snap index 9531b979a3bc..db5f2152ad08 100644 --- a/packages/jest-validate/src/__tests__/__snapshots__/validateCLIOptions.test.ts.snap +++ b/packages/jest-validate/src/__tests__/__snapshots__/validateCLIOptions.test.ts.snap @@ -31,6 +31,26 @@ exports[`fails for unknown option 1`] = ` " `; +exports[`handles deprecated CLI options print warning for deprecated options that are listed in config 1`] = ` +"foo: + +Deprecation message + + CLI Options Documentation: + https://jestjs.io/docs/cli +" +`; + +exports[`handles deprecated CLI options throw an error for deprecated options that are not listed in config 1`] = ` +"foo: + +Deprecation message + + CLI Options Documentation: + https://jestjs.io/docs/cli +" +`; + exports[`shows suggestion when unrecognized cli param length > 1 1`] = ` " Unrecognized CLI Parameter: diff --git a/packages/jest-validate/src/__tests__/validateCLIOptions.test.ts b/packages/jest-validate/src/__tests__/validateCLIOptions.test.ts index 51f92528a904..d4116b7ea8b6 100644 --- a/packages/jest-validate/src/__tests__/validateCLIOptions.test.ts +++ b/packages/jest-validate/src/__tests__/validateCLIOptions.test.ts @@ -6,6 +6,7 @@ * */ +import type {DeprecatedOptions} from '../types'; import validateCLIOptions from '../validateCLIOptions'; test('validates yargs special options', () => { @@ -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(); + }); +}); diff --git a/packages/jest-validate/src/types.ts b/packages/jest-validate/src/types.ts index cefd03bfe916..189039d88bc8 100644 --- a/packages/jest-validate/src/types.ts +++ b/packages/jest-validate/src/types.ts @@ -15,6 +15,8 @@ export type DeprecatedOptionFunc = (arg: Record) => string; export type DeprecatedOptions = Record; +export type DeprecationItem = {fatal: boolean; name: string}; + export type ValidationOptions = { comment?: string; condition?: (option: unknown, validOption: unknown) => boolean; diff --git a/packages/jest-validate/src/validateCLIOptions.ts b/packages/jest-validate/src/validateCLIOptions.ts index 5931bcffc3d6..0e227ce872e2 100644 --- a/packages/jest-validate/src/validateCLIOptions.ts +++ b/packages/jest-validate/src/validateCLIOptions.ts @@ -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:')} @@ -48,16 +55,21 @@ const createCLIValidationError = ( return new ValidationError(title, message, comment); }; -const logDeprecatedOptions = ( - deprecatedOptions: Array, +const validateDeprecatedOptions = ( + deprecatedOptions: Array, 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); + } }); }; @@ -69,29 +81,19 @@ export default function validateCLIOptions( rawArgv: Array = [], ): 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 >((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]; @@ -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;