From 131531e38190fbbf1a36afa40474afa8a781c228 Mon Sep 17 00:00:00 2001 From: skoshx Date: Fri, 4 Mar 2022 17:45:48 +0200 Subject: [PATCH 1/3] feat: add `commands` option This commit adds support for subcommands. --- index.d.ts | 26 +++++++++++++++++++ index.js | 11 ++++++++ test/fixtures/fixture-commands.js | 37 +++++++++++++++++++++++++++ test/subcommands.js | 42 +++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100755 test/fixtures/fixture-commands.js create mode 100644 test/subcommands.js diff --git a/index.d.ts b/index.d.ts index 00054f6..7660c73 100644 --- a/index.d.ts +++ b/index.d.ts @@ -26,6 +26,8 @@ type NumberFlag = Flag<'number', number> | Flag<'number', number[], true>; type AnyFlag = StringFlag | BooleanFlag | NumberFlag; type AnyFlags = Record; +type CommandType = (helpMessage: string, options?: Options) => typeof meow; + export interface Options { /** Pass in [`import.meta`](https://nodejs.org/dist/latest/docs/api/esm.html#esm_import_meta). This is used to find the correct package.json file. @@ -68,6 +70,30 @@ export interface Options { */ readonly flags?: Flags; + /** + Define subcommands. + + The key is the name of the subcommand and the value is a function that returns an instance of `meow`. + + The following values get passed to the subcommand function: + - `helpText`: The help text of parent `meow` instance. + - `options`: The options from the parent `meow` instance. + + @example + ``` + commands: { + unicorn: (helpText, options) => meow({ + ...options, + description: 'Subcommand description', + flags: { + unicorn: {alias: 'u'}, + } + }) + } + ``` + */ + readonly commands?: Record>; + /** Description to show above the help text. Default: The package.json `"description"` property. diff --git a/index.js b/index.js index b255b73..9353a8a 100644 --- a/index.js +++ b/index.js @@ -205,6 +205,16 @@ const meow = (helpText, options = {}) => { } } + // Subcommands + const commands = {}; + for (const [command, meowInstance] of Object.entries(options.commands ?? {})) { + if (input[0] !== command) { + continue; + } + + commands[command] = meowInstance(helpText, {...options, argv: process.argv.slice(3), commands: {}}); + } + const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]}); const unnormalizedFlags = {...flags}; @@ -222,6 +232,7 @@ const meow = (helpText, options = {}) => { return { input, + commands, flags, unnormalizedFlags, pkg: package_, diff --git a/test/fixtures/fixture-commands.js b/test/fixtures/fixture-commands.js new file mode 100755 index 0000000..6bb4514 --- /dev/null +++ b/test/fixtures/fixture-commands.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +import meow from '../../index.js'; + +const subcommand = (helpText, options = {}) => meow({ + ...options, + description: 'Subcommand description', + help: ` + Unicorn command + Usage: + foo unicorn + `, + flags: { + unicorn: {alias: 'u', isRequired: true}, + }, +}); + +const cli = meow({ + importMeta: import.meta, + description: 'Custom description', + help: ` + Usage + foo unicorn + `, + commands: { + unicorn: subcommand, + }, + flags: { + test: { + type: 'number', + alias: 't', + isRequired: () => false, + isMultiple: true, + }, + }, +}); + +console.log(JSON.stringify(cli)); diff --git a/test/subcommands.js b/test/subcommands.js new file mode 100644 index 0000000..81f31d8 --- /dev/null +++ b/test/subcommands.js @@ -0,0 +1,42 @@ +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import test from 'ava'; +import execa from 'execa'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fixtureSubcommands = path.join(__dirname, 'fixtures', 'fixture-commands.js'); + +test('spawn CLI and test subcommands', async t => { + const {stdout} = await execa(fixtureSubcommands, [ + 'unicorn', + '--unicorn', + ]); + const {commands} = JSON.parse(stdout); + t.assert('unicorn' in commands); + t.deepEqual(commands.unicorn.input, []); + t.deepEqual(commands.unicorn.commands, {}); + t.deepEqual(commands.unicorn.flags, {unicorn: true}); +}); + +test('spawn CLI and test subcommand flags', async t => { + const error = await t.throwsAsync(execa(fixtureSubcommands, ['unicorn'])); + const {stderr} = error; + t.regex(stderr, /Missing required flag/); + t.regex(stderr, /--unicorn/); +}); + +test('spawn CLI and test subcommand help text', async t => { + const {stdout} = await execa(fixtureSubcommands, [ + 'unicorn', + '--help', + ]); + t.regex(stdout, /Subcommand description/); + t.regex(stdout, /Unicorn command/); +}); + +test('spawn CLI and test CLI help text', async t => { + const {stdout} = await execa(fixtureSubcommands, [ + '--help', + ]); + t.regex(stdout, /Custom description/); +}); From bdea7cf70fcbd22c5dc0f88ea52678bbaadb5995 Mon Sep 17 00:00:00 2001 From: skoshx Date: Mon, 7 Mar 2022 22:58:21 +0200 Subject: [PATCH 2/3] fix: requested changes --- index.d.ts | 80 +++++++++++++++++++++---------- index.js | 2 +- readme.md | 49 +++++++++++++++++++ test/fixtures/fixture-commands.js | 14 +++--- 4 files changed, 111 insertions(+), 34 deletions(-) diff --git a/index.d.ts b/index.d.ts index 7660c73..f6b34b3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -26,7 +26,7 @@ type NumberFlag = Flag<'number', number> | Flag<'number', number[], true>; type AnyFlag = StringFlag | BooleanFlag | NumberFlag; type AnyFlags = Record; -type CommandType = (helpMessage: string, options?: Options) => typeof meow; +type CommandType = (options: Options) => typeof meow; export interface Options { /** @@ -71,27 +71,50 @@ export interface Options { readonly flags?: Flags; /** - Define subcommands. - - The key is the name of the subcommand and the value is a function that returns an instance of `meow`. - - The following values get passed to the subcommand function: - - `helpText`: The help text of parent `meow` instance. - - `options`: The options from the parent `meow` instance. - - @example - ``` - commands: { - unicorn: (helpText, options) => meow({ - ...options, - description: 'Subcommand description', - flags: { - unicorn: {alias: 'u'}, - } - }) - } - ``` - */ + Define subcommands. Subcommands don't actually call any commands for you, it only takes care of parsing + the subcommand flags, inputs, and showing the subcommand helptext. + + The key is the name of the subcommand and the value is a function that returns an instance of `meow`. + + The following values get passed to the subcommand function: + - `options`: The options from the parent `meow` instance. + + @example + ``` + const commands = { + subcommand = (options) => meow({ + ...options, + description: 'Subcommand description', + help: ` + Unicorn command + Usage: + foo unicorn + `, + flags: { + unicorn: {alias: 'u', isRequired: true}, + }, + }); + }; + const cli = meow({ + importMeta: import.meta, + description: 'Custom description', + help: ` + Usage + foo unicorn + `, + commands: { + unicorn: commands.subcommand, + }, + flags: {}, + }); + + // call subcommand + const [command, parsedCli] = Object.entries(cli.commands ?? {})?.[0] ?? []; + // command => "unicorn" + // parsedCli => parsed options of unicorn subcommand + commands[command](parsedCli); + ``` + */ readonly commands?: Record>; /** @@ -273,6 +296,11 @@ export interface Result { */ flags: TypedFlags & Record; + /** + Parsed subcommands + */ + subcommands: Record>; + /** Flags converted camelCase including aliases. */ @@ -311,14 +339,14 @@ import foo from './index.js'; const cli = meow(` Usage - $ foo + $ foo Options - --rainbow, -r Include a rainbow + --rainbow, -r Include a rainbow Examples - $ foo unicorns --rainbow - 🌈 unicorns 🌈 + $ foo unicorns --rainbow + 🌈 unicorns 🌈 `, { importMeta: import.meta, flags: { diff --git a/index.js b/index.js index 9353a8a..3e24544 100644 --- a/index.js +++ b/index.js @@ -212,7 +212,7 @@ const meow = (helpText, options = {}) => { continue; } - commands[command] = meowInstance(helpText, {...options, argv: process.argv.slice(3), commands: {}}); + commands[command] = meowInstance({...options, argv: process.argv.slice(3), commands: {}, help: helpText}); } const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]}); diff --git a/readme.md b/readme.md index 188d32e..a9f6d19 100644 --- a/readme.md +++ b/readme.md @@ -72,6 +72,7 @@ Returns an `object` with: - `input` *(Array)* - Non-flag arguments - `flags` *(Object)* - Flags converted to camelCase excluding aliases +- `commands` *(Object)* - Subcommands with values parsed with respect to subcommand's meow instance. - `unnormalizedFlags` *(Object)* - Flags converted to camelCase including aliases - `pkg` *(Object)* - The `package.json` object - `help` *(string)* - The help text used with `--help` @@ -135,6 +136,54 @@ flags: { } ``` +##### commands + +Type: `object` + +Define subcommands. Subcommands don't actually call any commands for you, it only takes care of parsing the subcommand flags, inputs, and showing the subcommand helptext. + +The key is the name of the subcommand and the value is a function that returns an instance of `meow`. + +The following values get passed to the subcommand function: + - `options`: The options from the parent `meow` instance. + +Example: + +```js +const commands = { + subcommand = (options) => meow({ + ...options, + description: 'Subcommand description', + help: ` + Unicorn command + Usage: + foo unicorn + `, + flags: { + unicorn: {alias: 'u', isRequired: true}, + }, + }); +}; +const cli = meow({ + importMeta: import.meta, + description: 'Custom description', + help: ` + Usage + foo unicorn + `, + commands: { + unicorn: commands.subcommand, + }, + flags: {}, +}); + +// call subcommand +const [command, parsedCli] = Object.entries(cli.commands ?? {})?.[0] ?? []; +// command => "unicorn" +// parsedCli => parsed meow instance of unicorn subcommand +commands[command](parsedCli); +``` + ##### description Type: `string | boolean`\ diff --git a/test/fixtures/fixture-commands.js b/test/fixtures/fixture-commands.js index 6bb4514..ac40c50 100755 --- a/test/fixtures/fixture-commands.js +++ b/test/fixtures/fixture-commands.js @@ -1,14 +1,14 @@ #!/usr/bin/env node import meow from '../../index.js'; -const subcommand = (helpText, options = {}) => meow({ +const subcommand = options => meow({ ...options, description: 'Subcommand description', help: ` - Unicorn command - Usage: - foo unicorn - `, + Unicorn command + Usage: + foo unicorn + `, flags: { unicorn: {alias: 'u', isRequired: true}, }, @@ -19,8 +19,8 @@ const cli = meow({ description: 'Custom description', help: ` Usage - foo unicorn - `, + foo unicorn + `, commands: { unicorn: subcommand, }, From ab6c820772590fc90dfa8d51931cd5a1d51ceab1 Mon Sep 17 00:00:00 2001 From: skoshx Date: Thu, 17 Mar 2022 13:55:05 +0200 Subject: [PATCH 3/3] fix: improved types --- index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index f6b34b3..8bbeb84 100644 --- a/index.d.ts +++ b/index.d.ts @@ -115,7 +115,7 @@ export interface Options { commands[command](parsedCli); ``` */ - readonly commands?: Record>; + readonly commands?: Record>; /** Description to show above the help text. Default: The package.json `"description"` property. @@ -299,7 +299,7 @@ export interface Result { /** Parsed subcommands */ - subcommands: Record>; + commands: Record>; /** Flags converted camelCase including aliases.