From b398b4ade4111d4e203fa1eb4826f4ad42fbe5d4 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 18 Mar 2023 15:37:28 -0500 Subject: [PATCH 01/10] chore: merge initial attempt from #183 --- index.d.ts | 9 ++-- index.js | 50 +++++++++++++++++++ index.test-d.ts | 22 +++++++++ readme.md | 2 + test/test.js | 129 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5c06aaa..5d90c4a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -15,12 +15,13 @@ Callback function to determine if a flag is required during runtime. */ export type IsRequiredPredicate = (flags: Readonly, input: readonly string[]) => boolean; -export type Flag = { - readonly type?: Type; +export type Flag = { + readonly type?: LiteralType; readonly alias?: string; - readonly default?: Default; + readonly default?: Type; readonly isRequired?: boolean | IsRequiredPredicate; readonly isMultiple?: IsMultiple; + readonly choices?: Type extends unknown[] ? Type : Type[]; }; type StringFlag = Flag<'string', string> | Flag<'string', string[], true>; @@ -47,6 +48,7 @@ export type Options = { If it's only known at runtime whether the flag is required or not you can pass a Function instead of a boolean, which based on the given flags and other non-flag arguments should decide if the flag is required. - `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false) Multiple values are provided by specifying the flag multiple times, for example, `$ foo -u rainbow -u cat`. Space- or comma-separated values are *not* supported. + - `choices`: Limit valid values to a predefined set of choices. Note that flags are always defined using a camel-case key (`myKey`), but will match arguments in kebab-case (`--my-key`). @@ -58,6 +60,7 @@ export type Options = { alias: 'u', default: ['rainbow', 'cat'], isMultiple: true, + choices: ['rainbow', 'cat', 'unicorn'], isRequired: (flags, input) => { if (flags.otherFlag) { return true; diff --git a/index.js b/index.js index b17499c..cc36c6d 100644 --- a/index.js +++ b/index.js @@ -59,6 +59,55 @@ const validateOptions = ({flags}) => { } }; +const validateChoicesByFlag = (flagKey, flagValue, receivedInput) => { + const {choices, isRequired} = flagValue; + + if (!choices) { + return; + } + + if (!Array.isArray(choices)) { + throw new TypeError('Choices should be array'); + } + + if (receivedInput === undefined) { + if (isRequired) { + return `Flag ${flagKey} has no value. Value must be one of: ${choices.join(', ')}`; + } + + return; + } + + if (Array.isArray(receivedInput)) { + const unknownValues = receivedInput.filter(index => !choices.includes(index)); + if (unknownValues.length === 1) { + return `Unknown value: \`${unknownValues[0]}\`. Value must be one of: ${choices.join(', ')}`; + } + + if (unknownValues.length > 1) { + return `Unknown values: \`${unknownValues.join(', ')}\`. Value must be one of: ${choices.join(', ')}`; + } + } else if (!choices.includes(receivedInput)) { + return `Unknown value: \`${receivedInput}\`. Value must be one of: ${choices.join(', ')}`; + } +}; + +const validateChoices = (flags, receivedFlags) => { + const errors = []; + + for (const [flagKey, flagValue] of Object.entries(flags)) { + const receivedInput = receivedFlags[flagKey]; + const errorMessage = validateChoicesByFlag(flagKey, flagValue, receivedInput); + if (errorMessage) { + errors.push(errorMessage); + } + } + + if (errors.length > 0) { + throw new Error(`${errors.join('. ')}`); + } +}; + const reportUnknownFlags = unknownFlags => { console.error([ `Unknown flag${unknownFlags.length > 1 ? 's' : ''}`, @@ -222,6 +271,7 @@ const meow = (helpText, options = {}) => { const unnormalizedFlags = {...flags}; validateFlags(flags, options); + validateChoices(options.flags, flags); for (const flagValue of Object.values(options.flags)) { delete flags[flagValue.alias]; diff --git a/index.test-d.ts b/index.test-d.ts index add5eb3..c129eb2 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -56,6 +56,7 @@ const result = meow('Help text', { 'foo-bar': {type: 'number'}, bar: {type: 'string', default: ''}, abc: {type: 'string', isMultiple: true}, + baz: {type: 'string', choices: ['rainbow', 'cat', 'unicorn']}, }, }); @@ -67,11 +68,13 @@ expectType(result.flags.foo); expectType(result.flags.fooBar); expectType(result.flags.bar); expectType(result.flags.abc); +expectType(result.flags.baz); expectType(result.unnormalizedFlags.foo); expectType(result.unnormalizedFlags.f); expectType(result.unnormalizedFlags['foo-bar']); expectType(result.unnormalizedFlags.bar); expectType(result.unnormalizedFlags.abc); +expectType(result.unnormalizedFlags.baz); result.showHelp(); result.showHelp(1); @@ -104,3 +107,22 @@ expectAssignable({type: 'boolean', isMultiple: true, default: [false]}) expectError({type: 'string', isMultiple: true, default: 'cat'}); expectError({type: 'number', isMultiple: true, default: 42}); expectError({type: 'boolean', isMultiple: true, default: false}); + +expectAssignable({type: 'string', choices: ['cat', 'unicorn']}); +expectAssignable({type: 'number', choices: [1, 2]}); +expectAssignable({type: 'boolean', choices: [true, false]}); +expectAssignable({type: 'string', isMultiple: true, choices: ['cat']}); +expectAssignable({type: 'string', isMultiple: false, choices: ['cat']}); + +expectError({type: 'string', choices: 'cat'}); +expectError({type: 'number', choices: 1}); +expectError({type: 'boolean', choices: true}); + +expectError({type: 'string', choices: [1]}); +expectError({type: 'number', choices: ['cat']}); +expectError({type: 'boolean', choices: ['cat']}); + +expectAssignable({choices: ['cat']}); +expectAssignable({choices: [1]}); +expectAssignable({choices: [true]}); +expectError({choices: ['cat', 1, true]}); diff --git a/readme.md b/readme.md index 188d32e..1b38e2e 100644 --- a/readme.md +++ b/readme.md @@ -112,6 +112,7 @@ The key is the flag name in camel-case and the value is an object with any of: - The function should return a `boolean`, true if the flag is required, otherwise false. - `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false) - Multiple values are provided by specifying the flag multiple times, for example, `$ foo -u rainbow -u cat`. Space- or comma-separated values are [currently *not* supported](https://github.com/sindresorhus/meow/issues/164). +- `choices`: Limit valid values to a predefined set of choices. Note that flags are always defined using a camel-case key (`myKey`), but will match arguments in kebab-case (`--my-key`). @@ -124,6 +125,7 @@ flags: { alias: 'u', default: ['rainbow', 'cat'], isMultiple: true, + choices: ['rainbow', 'cat', 'unicorn'], isRequired: (flags, input) => { if (flags.otherFlag) { return true; diff --git a/test/test.js b/test/test.js index d9b8033..3e1272f 100644 --- a/test/test.js +++ b/test/test.js @@ -580,6 +580,135 @@ test('isMultiple - handles multi-word flag name', t => { }); }); +test('choices - success case', t => { + const cli = meow({ + importMeta, + argv: ['--animal', 'cat', '--number=2.2'], + flags: { + animal: { + choices: ['dog', 'cat', 'unicorn'], + }, + number: { + type: 'number', + choices: [1.1, 2.2, 3.3], + }, + }, + }); + + t.is(cli.flags.animal, 'cat'); + t.is(cli.flags.number, 2.2); +}); + +test('choices - throws if input does not match choices', t => { + t.throws(() => { + meow({ + importMeta, + argv: ['--animal', 'rainbow', '--number', 5], + flags: { + animal: { + choices: ['dog', 'cat', 'unicorn'], + }, + number: { + choices: [1, 2, 3], + }, + }, + }); + }, {message: 'Unknown value: `rainbow`. Value must be one of: dog, cat, unicorn. ' + + 'Unknown value: `5`. Value must be one of: 1, 2, 3'}); +}); + +test('choices - throws if choices is not array', t => { + t.throws(() => { + meow({ + importMeta, + argv: ['--animal', 'cat'], + flags: { + animal: { + choices: 'cat', + }, + }, + }); + }, {message: 'Choices should be array'}); +}); + +test('choices - does not throw error when isRequired is false', t => { + t.notThrows(() => { + meow({ + importMeta, + argv: [], + flags: { + animal: { + isRequired: false, + choices: ['dog', 'cat', 'unicorn'], + }, + }, + }); + }); +}); + +test('choices - throw error when isRequired is true', t => { + t.throws(() => { + meow({ + importMeta, + argv: [], + flags: { + animal: { + isRequired: true, + choices: ['dog', 'cat', 'unicorn'], + }, + }, + }); + }, {message: 'Flag animal has no value. Value must be one of: dog, cat, unicorn'}); +}); + +test('choices - success with isMultiple', t => { + const cli = meow({ + importMeta, + argv: ['--animal=dog', '--animal=unicorn'], + flags: { + animal: { + type: 'string', + isMultiple: true, + choices: ['dog', 'cat', 'unicorn'], + }, + }, + }); + + t.deepEqual(cli.flags.animal, ['dog', 'unicorn']); +}); + +test('choices - throws with isMultiple, one unknown value', t => { + t.throws(() => { + meow({ + importMeta, + argv: ['--animal=dog', '--animal=rabbit'], + flags: { + animal: { + type: 'string', + isMultiple: true, + choices: ['dog', 'cat', 'unicorn'], + }, + }, + }); + }, {message: 'Unknown value: `rabbit`. Value must be one of: dog, cat, unicorn'}); +}); + +test('choices - throws with isMultiple, multiple unknown values', t => { + t.throws(() => { + meow({ + importMeta, + argv: ['--animal=dog', '--animal=rabbit'], + flags: { + animal: { + type: 'string', + isMultiple: true, + choices: ['cat', 'unicorn'], + }, + }, + }); + }, {message: 'Unknown values: `dog, rabbit`. Value must be one of: cat, unicorn'}); +}); + if (NODE_MAJOR_VERSION >= 14) { test('supports es modules', async t => { try { From cc46bbc3cb37f9a866a62af55c792bc01ccf5c4a Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 18 Mar 2023 15:39:37 -0500 Subject: [PATCH 02/10] chore(`Flag`): rename generic `LiteralType` to `PrimitiveType` --- index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5d90c4a..0728367 100644 --- a/index.d.ts +++ b/index.d.ts @@ -15,8 +15,8 @@ Callback function to determine if a flag is required during runtime. */ export type IsRequiredPredicate = (flags: Readonly, input: readonly string[]) => boolean; -export type Flag = { - readonly type?: LiteralType; +export type Flag = { + readonly type?: PrimitiveType; readonly alias?: string; readonly default?: Type; readonly isRequired?: boolean | IsRequiredPredicate; From 0ec65ce50c69a2072e231730eb1b796ebe2422c5 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Sat, 18 Mar 2023 21:13:05 -0500 Subject: [PATCH 03/10] refactor(`validateChoices`): improve messages --- index.js | 29 +++++++++++++++++------------ package.json | 1 + test/test.js | 19 ++++++++++++------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index cc36c6d..6eafaa0 100644 --- a/index.js +++ b/index.js @@ -57,6 +57,13 @@ const validateOptions = ({flags}) => { if (invalidFlags.length > 0) { throw new Error(`Flag keys may not contain '-': ${invalidFlags.join(', ')}`); } + + // TODO: test for multiple, format string/number/boolean + const invalidChoices = Object.entries(flags).filter(([_, {choices}]) => choices && !Array.isArray(choices)); + if (invalidChoices.length > 0) { + const formattedChoices = invalidChoices.map(([flagKey, {choices}]) => `flag \`${flagKey}\`: ${choices}`).join(', '); + throw new TypeError(`Flag choices must be an array: ${formattedChoices}`); + } }; const validateChoicesByFlag = (flagKey, flagValue, receivedInput) => { @@ -66,13 +73,11 @@ const validateChoicesByFlag = (flagKey, flagValue, receivedInput) => { return; } - if (!Array.isArray(choices)) { - throw new TypeError('Choices should be array'); - } + const valueMustBeOneOf = `Value must be one of: [${choices.join(', ')}]`; - if (receivedInput === undefined) { + if (!receivedInput) { if (isRequired) { - return `Flag ${flagKey} has no value. Value must be one of: ${choices.join(', ')}`; + return `Flag \`${flagKey}\` has no value. ${valueMustBeOneOf}`; } return; @@ -80,15 +85,14 @@ const validateChoicesByFlag = (flagKey, flagValue, receivedInput) => { if (Array.isArray(receivedInput)) { const unknownValues = receivedInput.filter(index => !choices.includes(index)); - if (unknownValues.length === 1) { - return `Unknown value: \`${unknownValues[0]}\`. Value must be one of: ${choices.join(', ')}`; - } - if (unknownValues.length > 1) { - return `Unknown values: \`${unknownValues.join(', ')}\`. Value must be one of: ${choices.join(', ')}`; + if (unknownValues.length > 0) { + const valuesText = unknownValues.length > 1 ? 'values' : 'value'; + + return `Unknown ${valuesText} for flag \`${flagKey}\`: \`${unknownValues.join(', ')}\`. ${valueMustBeOneOf}`; } } else if (!choices.includes(receivedInput)) { - return `Unknown value: \`${receivedInput}\`. Value must be one of: ${choices.join(', ')}`; + return `Unknown value for flag \`${flagKey}\`: \`${receivedInput}\`. ${valueMustBeOneOf}`; } }; @@ -98,13 +102,14 @@ const validateChoices = (flags, receivedFlags) => { for (const [flagKey, flagValue] of Object.entries(flags)) { const receivedInput = receivedFlags[flagKey]; const errorMessage = validateChoicesByFlag(flagKey, flagValue, receivedInput); + if (errorMessage) { errors.push(errorMessage); } } if (errors.length > 0) { - throw new Error(`${errors.join('. ')}`); + throw new Error(`${errors.join('.\n')}`); } }; diff --git a/package.json b/package.json index 39cdedb..5e0b5cc 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ }, "devDependencies": { "ava": "^4.3.3", + "common-tags": "^1.8.2", "execa": "^6.1.0", "indent-string": "^5.0.0", "read-pkg": "^7.1.0", diff --git a/test/test.js b/test/test.js index 3e1272f..ab18c77 100644 --- a/test/test.js +++ b/test/test.js @@ -3,6 +3,7 @@ import process from 'node:process'; import {fileURLToPath} from 'node:url'; import test from 'ava'; import indentString from 'indent-string'; +import {stripIndent} from 'common-tags'; import {execa} from 'execa'; import {readPackage} from 'read-pkg'; import meow from '../index.js'; @@ -613,8 +614,12 @@ test('choices - throws if input does not match choices', t => { }, }, }); - }, {message: 'Unknown value: `rainbow`. Value must be one of: dog, cat, unicorn. ' - + 'Unknown value: `5`. Value must be one of: 1, 2, 3'}); + }, { + message: stripIndent` + Unknown value for flag \`animal\`: \`rainbow\`. Value must be one of: [dog, cat, unicorn]. + Unknown value for flag \`number\`: \`5\`. Value must be one of: [1, 2, 3] + `, + }); }); test('choices - throws if choices is not array', t => { @@ -628,7 +633,7 @@ test('choices - throws if choices is not array', t => { }, }, }); - }, {message: 'Choices should be array'}); + }, {message: 'Flag choices must be an array: flag `animal`: cat'}); }); test('choices - does not throw error when isRequired is false', t => { @@ -658,7 +663,7 @@ test('choices - throw error when isRequired is true', t => { }, }, }); - }, {message: 'Flag animal has no value. Value must be one of: dog, cat, unicorn'}); + }, {message: 'Flag `animal` has no value. Value must be one of: [dog, cat, unicorn]'}); }); test('choices - success with isMultiple', t => { @@ -690,10 +695,10 @@ test('choices - throws with isMultiple, one unknown value', t => { }, }, }); - }, {message: 'Unknown value: `rabbit`. Value must be one of: dog, cat, unicorn'}); + }, {message: 'Unknown value for flag `animal`: `rabbit`. Value must be one of: [dog, cat, unicorn]'}); }); -test('choices - throws with isMultiple, multiple unknown values', t => { +test('choices - throws with isMultiple, multiple unknown value', t => { t.throws(() => { meow({ importMeta, @@ -706,7 +711,7 @@ test('choices - throws with isMultiple, multiple unknown values', t => { }, }, }); - }, {message: 'Unknown values: `dog, rabbit`. Value must be one of: cat, unicorn'}); + }, {message: 'Unknown values for flag `animal`: `dog, rabbit`. Value must be one of: [cat, unicorn]'}); }); if (NODE_MAJOR_VERSION >= 14) { From 2a25bd79589c3040ac6360a8e134851ad35b5dea Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Mon, 20 Mar 2023 22:58:22 -0500 Subject: [PATCH 04/10] chore: update changes from main --- estest/index.js | 2 +- index.d.ts | 19 +-- index.js | 30 ++++- index.test-d.ts | 8 +- readme.md | 16 +-- .../fixture-allow-unknown-flags-with-help.js | 4 +- .../fixture-conditional-required-multiple.js | 2 +- test/fixtures/fixture-required-function.js | 4 +- test/fixtures/fixture-required-multiple.js | 2 +- test/fixtures/fixture-required.js | 2 +- test/fixtures/fixture.js | 2 +- test/test.js | 115 +++++++++++++++++- 12 files changed, 171 insertions(+), 35 deletions(-) diff --git a/estest/index.js b/estest/index.js index 1a87119..4372709 100644 --- a/estest/index.js +++ b/estest/index.js @@ -17,7 +17,7 @@ meow( flags: { rainbow: { type: 'boolean', - alias: 'r', + shortFlag: 'r', }, }, }, diff --git a/index.d.ts b/index.d.ts index 0728367..a68251a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -17,10 +17,11 @@ export type IsRequiredPredicate = (flags: Readonly, input: readonly st export type Flag = { readonly type?: PrimitiveType; - readonly alias?: string; + readonly shortFlag?: string; readonly default?: Type; readonly isRequired?: boolean | IsRequiredPredicate; readonly isMultiple?: IsMultiple; + readonly aliases?: string[]; readonly choices?: Type extends unknown[] ? Type : Type[]; }; @@ -42,12 +43,13 @@ export type Options = { The key is the flag name in camel-case and the value is an object with any of: - `type`: Type of value. (Possible values: `string` `boolean` `number`) - - `alias`: Usually used to define a short flag alias. + - `shortFlag`: A short flag alias. - `default`: Default value when the flag is not specified. - `isRequired`: Determine if the flag is required. If it's only known at runtime whether the flag is required or not you can pass a Function instead of a boolean, which based on the given flags and other non-flag arguments should decide if the flag is required. - `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false) Multiple values are provided by specifying the flag multiple times, for example, `$ foo -u rainbow -u cat`. Space- or comma-separated values are *not* supported. + - `aliases`: Other names for the flag. - `choices`: Limit valid values to a predefined set of choices. Note that flags are always defined using a camel-case key (`myKey`), but will match arguments in kebab-case (`--my-key`). @@ -57,7 +59,7 @@ export type Options = { flags: { unicorn: { type: 'string', - alias: 'u', + shortFlag: 'u', default: ['rainbow', 'cat'], isMultiple: true, choices: ['rainbow', 'cat', 'unicorn'], @@ -67,7 +69,8 @@ export type Options = { } return false; - } + }, + aliases: ['unicorns'] } } ``` @@ -168,16 +171,16 @@ export type Options = { rainbow: { type: 'boolean', default: true, - alias: 'r' + shortFlag: 'r' }, unicorn: { type: 'boolean', default: false, - alias: 'u' + shortFlag: 'u' }, cake: { type: 'boolean', - alias: 'c' + shortFlag: 'c' }, sparkles: { type: 'boolean', @@ -305,7 +308,7 @@ const cli = meow(` flags: { rainbow: { type: 'boolean', - alias: 'r' + shortFlag: 'r' } } }); diff --git a/index.js b/index.js index 6eafaa0..25737ff 100644 --- a/index.js +++ b/index.js @@ -48,7 +48,7 @@ const getMissingRequiredFlags = (flags, receivedFlags, input) => { const reportMissingRequiredFlags = missingRequiredFlags => { console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`); for (const flag of missingRequiredFlags) { - console.error(`\t--${decamelize(flag.key, {separator: '-'})}${flag.alias ? `, -${flag.alias}` : ''}`); + console.error(`\t--${decamelize(flag.key, {separator: '-'})}${flag.shortFlag ? `, -${flag.shortFlag}` : ''}`); } }; @@ -58,6 +58,11 @@ const validateOptions = ({flags}) => { throw new Error(`Flag keys may not contain '-': ${invalidFlags.join(', ')}`); } + const flagsWithAlias = Object.keys(flags).filter(flagKey => flags[flagKey].alias !== undefined); + if (flagsWithAlias.length > 0) { + throw new Error(`The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`${flagsWithAlias.join('`, `')}\``); + } + // TODO: test for multiple, format string/number/boolean const invalidChoices = Object.entries(flags).filter(([_, {choices}]) => choices && !Array.isArray(choices)); if (invalidChoices.length > 0) { @@ -126,6 +131,12 @@ const buildParserFlags = ({flags, booleanDefault}) => { for (const [flagKey, flagValue] of Object.entries(flags)) { const flag = {...flagValue}; + // `buildParserOptions` expects `flag.alias` + if (flag.shortFlag) { + flag.alias = flag.shortFlag; + delete flag.shortFlag; + } + if ( typeof booleanDefault !== 'undefined' && flag.type === 'boolean' @@ -140,6 +151,15 @@ const buildParserFlags = ({flags, booleanDefault}) => { delete flag.isMultiple; } + if (Array.isArray(flag.aliases)) { + if (flag.alias) { + flag.aliases.push(flag.alias); + } + + flag.alias = flag.aliases; + delete flag.aliases; + } + parserFlags[flagKey] = flag; } @@ -279,7 +299,13 @@ const meow = (helpText, options = {}) => { validateChoices(options.flags, flags); for (const flagValue of Object.values(options.flags)) { - delete flags[flagValue.alias]; + if (Array.isArray(flagValue.aliases)) { + for (const alias of flagValue.aliases) { + delete flags[alias]; + } + } + + delete flags[flagValue.shortFlag]; } const missingRequiredFlags = getMissingRequiredFlags(options.flags, flags, input); diff --git a/index.test-d.ts b/index.test-d.ts index c129eb2..09250a7 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -52,8 +52,8 @@ expectType>(meow({importMeta, hardRejection: false})); const result = meow('Help text', { importMeta, flags: { - foo: {type: 'boolean', alias: 'f'}, - 'foo-bar': {type: 'number'}, + foo: {type: 'boolean', shortFlag: 'f'}, + 'foo-bar': {type: 'number', aliases: ['foobar', 'fooBar']}, bar: {type: 'string', default: ''}, abc: {type: 'string', isMultiple: true}, baz: {type: 'string', choices: ['rainbow', 'cat', 'unicorn']}, @@ -72,6 +72,8 @@ expectType(result.flags.baz); expectType(result.unnormalizedFlags.foo); expectType(result.unnormalizedFlags.f); expectType(result.unnormalizedFlags['foo-bar']); +expectType(result.unnormalizedFlags.foobar); +expectType(result.unnormalizedFlags.fooBar); expectType(result.unnormalizedFlags.bar); expectType(result.unnormalizedFlags.abc); expectType(result.unnormalizedFlags.baz); @@ -85,7 +87,7 @@ const options = { flags: { rainbow: { type: 'boolean', - alias: 'r', + shortFlag: 'r', }, }, } as const; diff --git a/readme.md b/readme.md index 1b38e2e..b24cc51 100644 --- a/readme.md +++ b/readme.md @@ -48,7 +48,7 @@ const cli = meow(` flags: { rainbow: { type: 'boolean', - alias: 'r' + shortFlag: 'r' } } }); @@ -103,7 +103,7 @@ Define argument flags. The key is the flag name in camel-case and the value is an object with any of: - `type`: Type of value. (Possible values: `string` `boolean` `number`) -- `alias`: Usually used to define a short flag alias. +- `shortFlag`: A short flag alias. - `default`: Default value when the flag is not specified. - `isRequired`: Determine if the flag is required. (Default: false) - If it's only known at runtime whether the flag is required or not, you can pass a `Function` instead of a `boolean`, which based on the given flags and other non-flag arguments, should decide if the flag is required. Two arguments are passed to the function: @@ -112,6 +112,7 @@ The key is the flag name in camel-case and the value is an object with any of: - The function should return a `boolean`, true if the flag is required, otherwise false. - `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false) - Multiple values are provided by specifying the flag multiple times, for example, `$ foo -u rainbow -u cat`. Space- or comma-separated values are [currently *not* supported](https://github.com/sindresorhus/meow/issues/164). +- `aliases`: Other names for the flag. - `choices`: Limit valid values to a predefined set of choices. Note that flags are always defined using a camel-case key (`myKey`), but will match arguments in kebab-case (`--my-key`). @@ -122,7 +123,7 @@ Example: flags: { unicorn: { type: 'string', - alias: 'u', + shortFlag: 'u', default: ['rainbow', 'cat'], isMultiple: true, choices: ['rainbow', 'cat', 'unicorn'], @@ -132,7 +133,8 @@ flags: { } return false; - } + }, + aliases: ['unicorns'] } } ``` @@ -244,16 +246,16 @@ const cli = meow(` rainbow: { type: 'boolean', default: true, - alias: 'r' + shortFlag: 'r' }, unicorn: { type: 'boolean', default: false, - alias: 'u' + shortFlag: 'u' }, cake: { type: 'boolean', - alias: 'c' + shortFlag: 'c' }, sparkles: { type: 'boolean', diff --git a/test/fixtures/fixture-allow-unknown-flags-with-help.js b/test/fixtures/fixture-allow-unknown-flags-with-help.js index 74725a9..e84a527 100755 --- a/test/fixtures/fixture-allow-unknown-flags-with-help.js +++ b/test/fixtures/fixture-allow-unknown-flags-with-help.js @@ -11,11 +11,11 @@ const cli = meow({ allowUnknownFlags: false, flags: { help: { - alias: 'h', + shortFlag: 'h', type: 'boolean', }, version: { - alias: 'v', + shortFlag: 'v', type: 'boolean', }, }, diff --git a/test/fixtures/fixture-conditional-required-multiple.js b/test/fixtures/fixture-conditional-required-multiple.js index 25c63f7..59c1601 100755 --- a/test/fixtures/fixture-conditional-required-multiple.js +++ b/test/fixtures/fixture-conditional-required-multiple.js @@ -11,7 +11,7 @@ const cli = meow({ flags: { test: { type: 'number', - alias: 't', + shortFlag: 't', isRequired: () => false, isMultiple: true, }, diff --git a/test/fixtures/fixture-required-function.js b/test/fixtures/fixture-required-function.js index 833f8b6..d6beda4 100755 --- a/test/fixtures/fixture-required-function.js +++ b/test/fixtures/fixture-required-function.js @@ -11,7 +11,7 @@ const cli = meow({ flags: { trigger: { type: 'boolean', - alias: 't', + shortFlag: 't', }, withTrigger: { type: 'string', @@ -19,7 +19,7 @@ const cli = meow({ }, allowError: { type: 'boolean', - alias: 'a', + shortFlag: 'a', }, shouldError: { type: 'boolean', diff --git a/test/fixtures/fixture-required-multiple.js b/test/fixtures/fixture-required-multiple.js index cf3325a..f94d534 100755 --- a/test/fixtures/fixture-required-multiple.js +++ b/test/fixtures/fixture-required-multiple.js @@ -11,7 +11,7 @@ const cli = meow({ flags: { test: { type: 'number', - alias: 't', + shortFlag: 't', isRequired: true, isMultiple: true, }, diff --git a/test/fixtures/fixture-required.js b/test/fixtures/fixture-required.js index 5a8a55d..fffb59d 100755 --- a/test/fixtures/fixture-required.js +++ b/test/fixtures/fixture-required.js @@ -11,7 +11,7 @@ const cli = meow({ flags: { test: { type: 'string', - alias: 't', + shortFlag: 't', isRequired: true, }, number: { diff --git a/test/fixtures/fixture.js b/test/fixtures/fixture.js index f7adc0e..342fd49 100755 --- a/test/fixtures/fixture.js +++ b/test/fixtures/fixture.js @@ -12,7 +12,7 @@ const cli = meow({ autoVersion: !process.argv.includes('--no-auto-version'), autoHelp: !process.argv.includes('--no-auto-help'), flags: { - unicorn: {alias: 'u'}, + unicorn: {shortFlag: 'u'}, meow: {default: 'dog'}, camelCaseOption: {default: 'foo'}, }, diff --git a/test/test.js b/test/test.js index ab18c77..8cc75a7 100644 --- a/test/test.js +++ b/test/test.js @@ -31,7 +31,7 @@ test('return object', t => { foo `, flags: { - unicorn: {alias: 'u'}, + unicorn: {shortFlag: 'u'}, meow: {default: 'dog'}, '--': true, }, @@ -227,7 +227,7 @@ test('accept help and options', t => { flags: { foo: { type: 'boolean', - alias: 'f', + shortFlag: 'f', }, }, }).flags, { @@ -242,11 +242,11 @@ test('grouped short-flags work', t => { flags: { coco: { type: 'boolean', - alias: 'c', + shortFlag: 'c', }, loco: { type: 'boolean', - alias: 'l', + shortFlag: 'l', }, }, }); @@ -265,11 +265,11 @@ test('grouped flags work', t => { flags: { coco: { type: 'boolean', - alias: 'c', + shortFlag: 'c', }, loco: { type: 'boolean', - alias: 'l', + shortFlag: 'l', }, }, }); @@ -581,6 +581,109 @@ test('isMultiple - handles multi-word flag name', t => { }); }); +test('suggests renaming alias to shortFlag', t => { + t.throws(() => { + meow({ + importMeta, + flags: { + foo: { + type: 'string', + alias: 'f', + }, + bar: { + type: 'string', + alias: 'b', + }, + baz: { + type: 'string', + shortFlag: 'z', + }, + }, + }); + }, {message: 'The option `alias` has been renamed to `shortFlag`. The following flags need to be updated: `foo`, `bar`'}); +}); + +test('aliases - accepts one', t => { + t.deepEqual(meow({ + importMeta, + argv: ['--foo=baz'], + flags: { + fooBar: { + type: 'string', + aliases: ['foo'], + }, + }, + }).flags, { + fooBar: 'baz', + }); +}); + +test('aliases - accepts multiple', t => { + t.deepEqual(meow({ + importMeta, + argv: ['--foo=baz1', '--bar=baz2'], + flags: { + fooBar: { + type: 'string', + aliases: ['foo', 'bar'], + isMultiple: true, + }, + }, + }).flags, { + fooBar: ['baz1', 'baz2'], + }); +}); + +test('aliases - can be a short flag', t => { + t.deepEqual(meow({ + importMeta, + argv: ['--f=baz'], + flags: { + fooBar: { + type: 'string', + aliases: ['f'], + }, + }, + }).flags, { + fooBar: 'baz', + }); +}); + +test('aliases - works with short flag', t => { + t.deepEqual(meow({ + importMeta, + argv: ['--foo=baz1', '--bar=baz2', '-f=baz3'], + flags: { + fooBar: { + type: 'string', + shortFlag: 'f', + aliases: ['foo', 'bar'], + isMultiple: true, + }, + }, + }).flags, { + fooBar: ['baz1', 'baz2', 'baz3'], + }); +}); + +test('aliases - unnormalized flags', t => { + t.deepEqual(meow({ + importMeta, + argv: ['--foo=baz'], + flags: { + fooBar: { + type: 'string', + aliases: ['foo'], + shortFlag: 'f', + }, + }, + }).unnormalizedFlags, { + fooBar: 'baz', + foo: 'baz', + f: 'baz', + }); +}); + test('choices - success case', t => { const cli = meow({ importMeta, From aae5b47db0ce9d0e265c53c9bc36f1e9d5209523 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Mon, 20 Mar 2023 23:09:31 -0500 Subject: [PATCH 05/10] tests(`choices`): multiple flags with incorrect value --- index.js | 3 +-- test/test.js | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 25737ff..07895d2 100644 --- a/index.js +++ b/index.js @@ -63,7 +63,6 @@ const validateOptions = ({flags}) => { throw new Error(`The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`${flagsWithAlias.join('`, `')}\``); } - // TODO: test for multiple, format string/number/boolean const invalidChoices = Object.entries(flags).filter(([_, {choices}]) => choices && !Array.isArray(choices)); if (invalidChoices.length > 0) { const formattedChoices = invalidChoices.map(([flagKey, {choices}]) => `flag \`${flagKey}\`: ${choices}`).join(', '); @@ -114,7 +113,7 @@ const validateChoices = (flags, receivedFlags) => { } if (errors.length > 0) { - throw new Error(`${errors.join('.\n')}`); + throw new Error(`${errors.join('\n')}`); } }; diff --git a/test/test.js b/test/test.js index 8cc75a7..d6410fd 100644 --- a/test/test.js +++ b/test/test.js @@ -719,7 +719,7 @@ test('choices - throws if input does not match choices', t => { }); }, { message: stripIndent` - Unknown value for flag \`animal\`: \`rainbow\`. Value must be one of: [dog, cat, unicorn]. + Unknown value for flag \`animal\`: \`rainbow\`. Value must be one of: [dog, cat, unicorn] Unknown value for flag \`number\`: \`5\`. Value must be one of: [1, 2, 3] `, }); @@ -817,6 +817,28 @@ test('choices - throws with isMultiple, multiple unknown value', t => { }, {message: 'Unknown values for flag `animal`: `dog, rabbit`. Value must be one of: [cat, unicorn]'}); }); +test('choices - throws with multiple flags', t => { + t.throws(() => { + meow({ + importMeta, + argv: ['--animal=dog', '--plant=succulent'], + flags: { + animal: { + type: 'string', + choices: ['cat', 'unicorn'], + }, + plant: { + type: 'string', + choices: ['tree', 'flower'], + }, + }, + }); + }, {message: stripIndent` + Unknown value for flag \`animal\`: \`dog\`. Value must be one of: [cat, unicorn] + Unknown value for flag \`plant\`: \`succulent\`. Value must be one of: [tree, flower] + `}); +}); + if (NODE_MAJOR_VERSION >= 14) { test('supports es modules', async t => { try { From 9369c145d1aea8678393b0c3ba7ac87fa4b05e99 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Tue, 21 Mar 2023 00:13:38 -0500 Subject: [PATCH 06/10] refactor(`validateOptions`): support multiple error messages --- index.js | 39 ++++++++++++++++++++++++++++----------- test/test.js | 25 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 07895d2..efcf6e0 100644 --- a/index.js +++ b/index.js @@ -53,20 +53,37 @@ const reportMissingRequiredFlags = missingRequiredFlags => { }; const validateOptions = ({flags}) => { - const invalidFlags = Object.keys(flags).filter(flagKey => flagKey.includes('-') && flagKey !== '--'); - if (invalidFlags.length > 0) { - throw new Error(`Flag keys may not contain '-': ${invalidFlags.join(', ')}`); - } + const keys = Object.keys(flags); + const entries = Object.entries(flags); + + const invalidOptions = { + flagsWithDash: { + values: keys.filter(flagKey => flagKey.includes('-') && flagKey !== '--'), + message: values => `Flag keys may not contain '-': ${values.join(', ')}`, + }, + flagsWithAlias: { + values: keys.filter(flagKey => flags[flagKey].alias !== undefined), + message: values => `The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`${values.join('`, `')}\``, + }, + flagsWithNonArrayChoices: { + values: entries.filter(([_, {choices}]) => choices && !Array.isArray(choices)), + message(values) { + const formattedChoices = values.map(([flagKey, {choices}]) => `flag \`${flagKey}\`: ${choices}`).join(', '); + return `Flag choices must be an array: ${formattedChoices}`; + }, + }, + }; - const flagsWithAlias = Object.keys(flags).filter(flagKey => flags[flagKey].alias !== undefined); - if (flagsWithAlias.length > 0) { - throw new Error(`The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`${flagsWithAlias.join('`, `')}\``); + const errorMessages = []; + + for (const {values, message} of Object.values(invalidOptions)) { + if (values.length > 0) { + errorMessages.push(message(values)); + } } - const invalidChoices = Object.entries(flags).filter(([_, {choices}]) => choices && !Array.isArray(choices)); - if (invalidChoices.length > 0) { - const formattedChoices = invalidChoices.map(([flagKey, {choices}]) => `flag \`${flagKey}\`: ${choices}`).join(', '); - throw new TypeError(`Flag choices must be an array: ${formattedChoices}`); + if (errorMessages.length > 0) { + throw new Error(errorMessages.join('\n')); } }; diff --git a/test/test.js b/test/test.js index d6410fd..bf9b70c 100644 --- a/test/test.js +++ b/test/test.js @@ -839,6 +839,31 @@ test('choices - throws with multiple flags', t => { `}); }); +test('options - multiple validation errors', t => { + t.throws(() => { + meow({ + importMeta, + flags: { + animal: { + type: 'string', + choices: 'cat', + }, + plant: { + type: 'string', + alias: 'p', + }, + 'some-thing': { + type: 'string', + }, + }, + }); + }, {message: stripIndent` + Flag keys may not contain '-': some-thing + The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`plant\` + Flag choices must be an array: flag \`animal\`: cat + `}); +}); + if (NODE_MAJOR_VERSION >= 14) { test('supports es modules', async t => { try { From e5caa1b4446e2dc4aea00fb59490ecc43a2cf802 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Tue, 21 Mar 2023 00:19:17 -0500 Subject: [PATCH 07/10] tests: document that choice types aren't validated --- test/test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/test.js b/test/test.js index bf9b70c..d1e23c6 100644 --- a/test/test.js +++ b/test/test.js @@ -839,6 +839,20 @@ test('choices - throws with multiple flags', t => { `}); }); +test.failing('choices - choices must be of the same type', t => { + t.throws(() => { + meow({ + importMeta, + flags: { + number: { + type: 'number', + choices: [1, '2'], + }, + }, + }); + }); +}); + test('options - multiple validation errors', t => { t.throws(() => { meow({ From 3c8f2364bc9936a7e8e878179e76c2be9e035fcc Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Fri, 24 Mar 2023 11:21:42 -0500 Subject: [PATCH 08/10] refactor: improve option validation --- index.js | 47 ++++++++++++++++++++++++++--------------------- test/test.js | 12 ++++++------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/index.js b/index.js index ac6136b..e20d379 100644 --- a/index.js +++ b/index.js @@ -52,33 +52,38 @@ const reportMissingRequiredFlags = missingRequiredFlags => { } }; -const validateOptions = ({flags}) => { - const keys = Object.keys(flags); - const entries = Object.entries(flags); - - const invalidOptions = { - flagsWithDash: { - values: keys.filter(flagKey => flagKey.includes('-') && flagKey !== '--'), - message: values => `Flag keys may not contain '-': ${values.join(', ')}`, - }, - flagsWithAlias: { - values: keys.filter(flagKey => flags[flagKey].alias !== undefined), - message: values => `The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`${values.join('`, `')}\``, - }, - flagsWithNonArrayChoices: { - values: entries.filter(([_, {choices}]) => choices && !Array.isArray(choices)), - message(values) { - const formattedChoices = values.map(([flagKey, {choices}]) => `flag \`${flagKey}\`: ${choices}`).join(', '); - return `Flag choices must be an array: ${formattedChoices}`; +const joinFlagKeys = (flagKeys, prefix = '--') => `\`${prefix}${flagKeys.join(`\`, \`${prefix}`)}\``; + +const validateOptions = options => { + const invalidOptionFilters = { + flags: { + flagsWithDashes: { + filter: ([flagKey]) => flagKey.includes('-') && flagKey !== '--', + message: flagKeys => `Flag keys may not contain '-'. Invalid flags: ${joinFlagKeys(flagKeys, '')}`, + }, + flagsWithAlias: { + filter: ([, flag]) => flag.alias !== undefined, + message: flagKeys => `The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: ${joinFlagKeys(flagKeys)}`, + }, + flagsWithNonArrayChoices: { + filter: ([, flag]) => flag.choices !== undefined && !Array.isArray(flag.choices), + message: flagKeys => `The option \`choices\` must be an array. Invalid flags: ${joinFlagKeys(flagKeys)}`, }, }, }; const errorMessages = []; - for (const {values, message} of Object.values(invalidOptions)) { - if (values.length > 0) { - errorMessages.push(message(values)); + for (const [optionKey, filters] of Object.entries(invalidOptionFilters)) { + const optionEntries = Object.entries(options[optionKey]); + + for (const {filter, message} of Object.values(filters)) { + const invalidOptions = optionEntries.filter(option => filter(option)); + const invalidOptionKeys = invalidOptions.map(([key]) => key); + + if (invalidOptions.length > 0) { + errorMessages.push(message(invalidOptionKeys)); + } } } diff --git a/test/test.js b/test/test.js index d1e23c6..c085002 100644 --- a/test/test.js +++ b/test/test.js @@ -129,7 +129,7 @@ test('flag declared in kebab-case is an error', t => { flags: {'kebab-case': 'boolean', test: 'boolean', 'another-one': 'boolean'}, }); }); - t.is(error.message, 'Flag keys may not contain \'-\': kebab-case, another-one'); + t.is(error.message, 'Flag keys may not contain \'-\'. Invalid flags: `kebab-case`, `another-one`'); }); test('type inference', t => { @@ -600,7 +600,7 @@ test('suggests renaming alias to shortFlag', t => { }, }, }); - }, {message: 'The option `alias` has been renamed to `shortFlag`. The following flags need to be updated: `foo`, `bar`'}); + }, {message: 'The option `alias` has been renamed to `shortFlag`. The following flags need to be updated: `--foo`, `--bar`'}); }); test('aliases - accepts one', t => { @@ -736,7 +736,7 @@ test('choices - throws if choices is not array', t => { }, }, }); - }, {message: 'Flag choices must be an array: flag `animal`: cat'}); + }, {message: 'The option `choices` must be an array. Invalid flags: `--animal`'}); }); test('choices - does not throw error when isRequired is false', t => { @@ -872,9 +872,9 @@ test('options - multiple validation errors', t => { }, }); }, {message: stripIndent` - Flag keys may not contain '-': some-thing - The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`plant\` - Flag choices must be an array: flag \`animal\`: cat + Flag keys may not contain '-'. Invalid flags: \`some-thing\` + The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`--plant\` + The option \`choices\` must be an array. Invalid flags: \`--animal\` `}); }); From ff794298ee4f7444e7a5d91cae52e6116e51a0c2 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Fri, 24 Mar 2023 11:31:07 -0500 Subject: [PATCH 09/10] feat: kebab-case flag keys in errors --- index.js | 17 +++++++---------- test/test.js | 14 +++++++------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index e20d379..c9fa9d1 100644 --- a/index.js +++ b/index.js @@ -45,10 +45,12 @@ const getMissingRequiredFlags = (flags, receivedFlags, input) => { return missingRequiredFlags; }; +const decamelizeFlagKey = flagKey => `--${decamelize(flagKey, {separator: '-'})}`; + const reportMissingRequiredFlags = missingRequiredFlags => { console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`); for (const flag of missingRequiredFlags) { - console.error(`\t--${decamelize(flag.key, {separator: '-'})}${flag.shortFlag ? `, -${flag.shortFlag}` : ''}`); + console.error(`\t${decamelizeFlagKey(flag.key)}${flag.shortFlag ? `, -${flag.shortFlag}` : ''}`); } }; @@ -99,11 +101,11 @@ const validateChoicesByFlag = (flagKey, flagValue, receivedInput) => { return; } - const valueMustBeOneOf = `Value must be one of: [${choices.join(', ')}]`; + const valueMustBeOneOf = `Value must be one of: [\`${choices.join('`, `')}\`]`; if (!receivedInput) { if (isRequired) { - return `Flag \`${flagKey}\` has no value. ${valueMustBeOneOf}`; + return `Flag \`${decamelizeFlagKey(flagKey)}\` has no value. ${valueMustBeOneOf}`; } return; @@ -115,10 +117,10 @@ const validateChoicesByFlag = (flagKey, flagValue, receivedInput) => { if (unknownValues.length > 0) { const valuesText = unknownValues.length > 1 ? 'values' : 'value'; - return `Unknown ${valuesText} for flag \`${flagKey}\`: \`${unknownValues.join(', ')}\`. ${valueMustBeOneOf}`; + return `Unknown ${valuesText} for flag \`${decamelizeFlagKey(flagKey)}\`: \`${unknownValues.join('`, `')}\`. ${valueMustBeOneOf}`; } } else if (!choices.includes(receivedInput)) { - return `Unknown value for flag \`${flagKey}\`: \`${receivedInput}\`. ${valueMustBeOneOf}`; + return `Unknown value for flag \`${decamelizeFlagKey(flagKey)}\`: \`${receivedInput}\`. ${valueMustBeOneOf}`; } }; @@ -137,11 +139,6 @@ const validateChoices = (flags, receivedFlags) => { if (errors.length > 0) { throw new Error(`${errors.join('\n')}`); } - - const flagsWithAlias = Object.keys(flags).filter(flagKey => flags[flagKey].alias !== undefined); - if (flagsWithAlias.length > 0) { - throw new Error(`The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`${flagsWithAlias.join('`, `')}\``); - } }; const reportUnknownFlags = unknownFlags => { diff --git a/test/test.js b/test/test.js index c085002..68ba0d1 100644 --- a/test/test.js +++ b/test/test.js @@ -719,8 +719,8 @@ test('choices - throws if input does not match choices', t => { }); }, { message: stripIndent` - Unknown value for flag \`animal\`: \`rainbow\`. Value must be one of: [dog, cat, unicorn] - Unknown value for flag \`number\`: \`5\`. Value must be one of: [1, 2, 3] + Unknown value for flag \`--animal\`: \`rainbow\`. Value must be one of: [\`dog\`, \`cat\`, \`unicorn\`] + Unknown value for flag \`--number\`: \`5\`. Value must be one of: [\`1\`, \`2\`, \`3\`] `, }); }); @@ -766,7 +766,7 @@ test('choices - throw error when isRequired is true', t => { }, }, }); - }, {message: 'Flag `animal` has no value. Value must be one of: [dog, cat, unicorn]'}); + }, {message: 'Flag `--animal` has no value. Value must be one of: [`dog`, `cat`, `unicorn`]'}); }); test('choices - success with isMultiple', t => { @@ -798,7 +798,7 @@ test('choices - throws with isMultiple, one unknown value', t => { }, }, }); - }, {message: 'Unknown value for flag `animal`: `rabbit`. Value must be one of: [dog, cat, unicorn]'}); + }, {message: 'Unknown value for flag `--animal`: `rabbit`. Value must be one of: [`dog`, `cat`, `unicorn`]'}); }); test('choices - throws with isMultiple, multiple unknown value', t => { @@ -814,7 +814,7 @@ test('choices - throws with isMultiple, multiple unknown value', t => { }, }, }); - }, {message: 'Unknown values for flag `animal`: `dog, rabbit`. Value must be one of: [cat, unicorn]'}); + }, {message: 'Unknown values for flag `--animal`: `dog`, `rabbit`. Value must be one of: [`cat`, `unicorn`]'}); }); test('choices - throws with multiple flags', t => { @@ -834,8 +834,8 @@ test('choices - throws with multiple flags', t => { }, }); }, {message: stripIndent` - Unknown value for flag \`animal\`: \`dog\`. Value must be one of: [cat, unicorn] - Unknown value for flag \`plant\`: \`succulent\`. Value must be one of: [tree, flower] + Unknown value for flag \`--animal\`: \`dog\`. Value must be one of: [\`cat\`, \`unicorn\`] + Unknown value for flag \`--plant\`: \`succulent\`. Value must be one of: [\`tree\`, \`flower\`] `}); }); From 12dde27e3f0f87a9cf5cacbfcd9eb5ec6480a5d0 Mon Sep 17 00:00:00 2001 From: tommy-mitchell Date: Fri, 24 Mar 2023 11:51:43 -0500 Subject: [PATCH 10/10] feat: validate type of `choices` --- index.js | 7 +++++++ test/test.js | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index c9fa9d1..0748519 100644 --- a/index.js +++ b/index.js @@ -71,6 +71,13 @@ const validateOptions = options => { filter: ([, flag]) => flag.choices !== undefined && !Array.isArray(flag.choices), message: flagKeys => `The option \`choices\` must be an array. Invalid flags: ${joinFlagKeys(flagKeys)}`, }, + flagsWithChoicesOfDifferentTypes: { + filter: ([, flag]) => flag.type && Array.isArray(flag.choices) && flag.choices.some(choice => typeof choice !== flag.type), + message(flagKeys) { + const flagKeysAndTypes = flagKeys.map(flagKey => `(\`${decamelizeFlagKey(flagKey)}\`, type: '${options.flags[flagKey].type}')`); + return `Each value of the option \`choices\` must be of the same type as its flag. Invalid flags: ${flagKeysAndTypes.join(', ')}`; + }, + }, }, }; diff --git a/test/test.js b/test/test.js index 68ba0d1..c489753 100644 --- a/test/test.js +++ b/test/test.js @@ -839,7 +839,7 @@ test('choices - throws with multiple flags', t => { `}); }); -test.failing('choices - choices must be of the same type', t => { +test('choices - choices must be of the same type', t => { t.throws(() => { meow({ importMeta, @@ -848,9 +848,13 @@ test.failing('choices - choices must be of the same type', t => { type: 'number', choices: [1, '2'], }, + boolean: { + type: 'boolean', + choices: [true, 'false'], + }, }, }); - }); + }, {message: 'Each value of the option `choices` must be of the same type as its flag. Invalid flags: (`--number`, type: \'number\'), (`--boolean`, type: \'boolean\')'}); }); test('options - multiple validation errors', t => {