Skip to content

Commit

Permalink
feat: add persistent post run, command meta, invalid command error (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredLunde authored Feb 25, 2023
1 parent 1cc2b7f commit ca94b64
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- [x] Global flags
- [x] Automated README generation
- [x] Command and flag aliases
- [x] `persistentPreRun`, `preRun` and `postRun` hooks
- [x] `persistentPreRun`, `preRun`, `postRun`, `persistentPostRun` hooks

## Getting started

Expand Down
116 changes: 105 additions & 11 deletions command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ export function command<
aliases = [],
deprecated,
hidden = false,
meta = {},
}: CommandConfig<Context, Args, Opts> = { flags: flags_({}) as any },
): Command<Context, Args, Opts> {
let action: Action<Context, Args, Opts> | undefined;
let persistentPreAction: PersistentAction<Context> | undefined;
let persistentPreAction:
| PersistentAction<Context, any>
| undefined;
let preAction: Action<Context, Args, Opts> | undefined;
let postAction: Action<Context, Args, Opts> | undefined;
let persistentPostAction:
| PersistentAction<Context, any>
| undefined;
const hasCmds = !!commands?.length;

function* help(context: Context): Iterable<string> {
Expand Down Expand Up @@ -248,6 +254,7 @@ export function command<
deprecated,
help,
usage: use,
meta,

short(context) {
let description: string | undefined;
Expand Down Expand Up @@ -288,23 +295,48 @@ export function command<
return this;
},

persistentPostRun(action_) {
persistentPostAction = action_;
return this;
},

postRun(action_) {
postAction = action_;
return this;
},

async execute(argv = Deno.args, ctx) {
await handleAction(persistentPreAction, { argv, ctx });

if (hasCmds) {
if (hasCmds && argv[0]?.[0] !== "-") {
const [cmd, ...rest] = argv;
const subCommand = commands.find(
(c) => c.name === cmd || c.aliases.indexOf(cmd) !== -1,
);

if (subCommand) {
// Attach persistent pre/post run hooks to the subcommand.
if (persistentPreAction) {
subCommand.persistentPreRun(persistentPreAction);
}

if (persistentPostAction) {
subCommand.persistentPostRun(persistentPostAction);
}

return subCommand.execute(rest, ctx);
}

// If the command is not found, we check if the command also receives
// arguments. If it does, we assume that the user is trying to run a
// command with arguments. Otherwise, we show a helpful error message.
if (!args) {
const message = `Unknown command "${cmd}" for "${
ctx!.path.join(" ")
}"\n${
didYouMean(cmd, commands.flatMap((c) => [c.name, ...c.aliases]))
}\n`;
await Deno.stderr.write(textEncoder.encode(message));
Deno.exit(1);
}
}

const bools: Record<string, boolean> = {};
Expand Down Expand Up @@ -491,17 +523,29 @@ export function command<
}

const actionArgs = { args: a, flags: o, "--": doubleDash, ctx };

const persistentActionArgs = {
args: a,
flags: o,
"--": doubleDash,
ctx: {
...ctx,
cmd: this,
},
};
// Run the action
await handleAction(persistentPreAction, persistentActionArgs);
await handleAction(preAction, actionArgs);
await handleAction(action, actionArgs);
await handleAction(postAction, actionArgs);
await handleAction(persistentPostAction, persistentActionArgs);
},
};
}

async function handleAction<
ActionFn extends Action<any, any, any, any> | PersistentAction<any>,
ActionFn extends
| Action<any, any, any, any>
| PersistentAction<any, any>,
>(
action: ActionFn | undefined,
args: unknown,
Expand Down Expand Up @@ -589,6 +633,10 @@ export type Command<
* Returns the help text for the command
*/
help(context: Context): Iterable<string>;
/**
* The metadata for the command
*/
meta: Readonly<Record<string, unknown>>;
/**
* The usage string for the command
*/
Expand All @@ -603,12 +651,16 @@ export type Command<
long(context: Context): string | undefined;
/**
* Run this action before the "run" command. This will also run before any
* subcommands. It will override any defined `persistentPreRun` actions on
* subcommands.
*
* @param action - The action to run before the "run" command
*/
persistentPreRun(
action: PersistentAction<Context>,
action: PersistentAction<
Context,
GlobalOpts
>,
): Command<Context, Args, Opts, GlobalOpts>;
/**
* Run this action before the "run" command
Expand All @@ -624,6 +676,19 @@ export type Command<
run(
action: Action<Context, Args, Opts, GlobalOpts>,
): Command<Context, Args, Opts, GlobalOpts>;
/**
* Run this action before the "run" command. This will also run before any
* subcommands. It will override any defined `persistentPreRun` actions on
* subcommands.
*
* @param action - The action to run before the "run" command
*/
persistentPostRun(
action: PersistentAction<
Context,
GlobalOpts
>,
): Command<Context, Args, Opts, GlobalOpts>;
/**
* Run this action after the "run" command
* @param action The action to run after the "run" command
Expand Down Expand Up @@ -682,19 +747,48 @@ export type CommandConfig<
* A long description of the command
*/
long?: string | ((context: Context) => string);
/**
* Add metadata to the command
*/
meta?: Record<string, unknown>;
};

export type PersistentAction<Context extends DefaultContext> = {
export type PersistentAction<
Context extends DefaultContext,
GlobalOpts extends Flags | unknown = unknown,
> = {
/**
* The action to run when the command is invoked
* @param argopts The parsed arguments and options
* @param ctx The context object
*/
(
opts: {
/**
* The unparsed arguments passed to this specific command.
* A parsed arguments array or tuple
*/
argv: string[];
args: unknown[];
/**
* A parsed flags object
*/
flags: GlobalOpts extends Flags ? inferFlags<GlobalOpts>
: Record<string, unknown>;
/**
* Unparsed arguments that were passed to this command after
* the `--` separator.
*/
"--": string[];
/**
* The context object
*/
ctx: Context;
ctx: Prettify<
Context & {
/**
* The command that was invoked
*/
cmd: Command<any, any, any, GlobalOpts>;
}
>;
},
): Promise<void> | AsyncGenerator<string> | Generator<string> | void;
};
Expand Down
7 changes: 7 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 23 additions & 10 deletions test/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,26 +90,39 @@ describe("command()", () => {
const postRun = spy(() => {
order.push("postRun");
});
const persistentPostRun = spy(() => {
order.push("persistentPostRun");
});
const cmd = cli.command("test").run(run).persistentPreRun(persistentPreRun)
.persistentPostRun(persistentPostRun)
.preRun(preRun).postRun(postRun);

await cmd.execute([]);

assertSpyCalls(persistentPreRun, 1);
assertSpyCall(persistentPreRun, 0, {
args: [{
argv: [],
ctx: {
root: cmd,
path: ["test"],
},
}],
});
assertSpyCalls(preRun, 1);
assertSpyCalls(preRun, 1);
assertSpyCalls(run, 1);
assertSpyCalls(postRun, 1);
assertEquals(order, ["persistentPreRun", "preRun", "run", "postRun"]);
assertSpyCalls(persistentPostRun, 1);
assertEquals(order, [
"persistentPreRun",
"preRun",
"run",
"postRun",
"persistentPostRun",
]);
});

it("should have metadata", () => {
const cli = init();
const cmd = cli.command("test", {
meta: {
foo: "bar",
},
});

assertEquals(cmd.meta.foo, "bar");
});

it("should write strings to stdout in run with a generator", async () => {
Expand Down

0 comments on commit ca94b64

Please sign in to comment.