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

Add commands option #205

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type NumberFlag = Flag<'number', number> | Flag<'number', number[], true>;
type AnyFlag = StringFlag | BooleanFlag | NumberFlag;
type AnyFlags = Record<string, AnyFlag>;

type CommandType<Flags extends AnyFlags> = (helpMessage: string, options?: Options<Flags>) => typeof meow;

export interface Options<Flags extends AnyFlags> {
/**
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.
Expand Down Expand Up @@ -68,6 +70,30 @@ export interface Options<Flags extends AnyFlags> {
*/
readonly flags?: Flags;

/**
Define subcommands.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use tab-indentation.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description needs to be more extensive. It should describe how it works (that it doesn't actually call any commands for you, etc).


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: {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example should be complete and runnable. I think it's better to define the subcommand function at the top-level, for readability.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example should also illustrate how one might check for and call code for the subcommand.

unicorn: (helpText, options) => meow({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helpText argument is moot. helpText can already be passed in options.

...options,
description: 'Subcommand description',
flags: {
unicorn: {alias: 'u'},
}
})
}
```
*/
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs to be documented in the readme.

readonly commands?: Record<string, CommandType<Flags>>;

/**
Description to show above the help text. Default: The package.json `"description"` property.

Expand Down
11 changes: 11 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass helpText in options.

}

const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]});
const unnormalizedFlags = {...flags};

Expand All @@ -222,6 +232,7 @@ const meow = (helpText, options = {}) => {

return {
input,
commands,
flags,
unnormalizedFlags,
pkg: package_,
Expand Down
37 changes: 37 additions & 0 deletions test/fixtures/fixture-commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env node
import meow from '../../index.js';

const subcommand = (helpText, options = {}) => meow({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const subcommand = (helpText, options = {}) => meow({
const subcommand = (helpText, options) => meow({

The options should be guaranteed. Make sure the code guarantees it to exist.

...options,
description: 'Subcommand description',
help: `
Unicorn command
Usage:
foo unicorn <input>
`,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tab-indentation.

flags: {
unicorn: {alias: 'u', isRequired: true},
},
});

const cli = meow({
importMeta: import.meta,
description: 'Custom description',
help: `
Usage
foo unicorn <input>
`,
commands: {
unicorn: subcommand,
},
flags: {
test: {
type: 'number',
alias: 't',
isRequired: () => false,
isMultiple: true,
},
},
});

console.log(JSON.stringify(cli));
42 changes: 42 additions & 0 deletions test/subcommands.js
Original file line number Diff line number Diff line change
@@ -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/);
});