Skip to content

Instantly share code, notes, and snippets.

@eliellis
Last active February 4, 2022 02:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliellis/9c05057dc5cafb9f19a8757ea47f5281 to your computer and use it in GitHub Desktop.
Save eliellis/9c05057dc5cafb9f19a8757ea47f5281 to your computer and use it in GitHub Desktop.
Turn classes into yargs commands with Typescript decorators. WTFPL License.
import yargs, { Options } from "yargs";
import { hideBin } from "yargs/helpers";
const COMMAND_DEFINITION_METADATA = "__command:definition";
const COMMAND_PARAMETERS_METADATA = "__command:parameters";
export interface Constructor {
new (...args: any[]): {};
}
interface CommandDefinition {
command: string[] | string;
aliases?: string[] | string;
describe?: string;
}
interface CommandParameter<O = Options> {
index: number;
name: string;
option: O;
}
export async function yarginate(script: string, ...commands: any[]) {
const yargsInst = yargs(hideBin(process.argv));
for (const command of commands) {
for (const [key, descriptor] of Object.entries(
Object.getOwnPropertyDescriptors(Object.getPrototypeOf(command))
)) {
if (key !== "constructor") {
const commandMetadata: CommandDefinition = Reflect.getMetadata(
COMMAND_DEFINITION_METADATA,
descriptor.value
);
const paramsMetadata: CommandParameter[] =
Reflect.getMetadata(COMMAND_PARAMETERS_METADATA, descriptor.value) ??
[];
if (commandMetadata) {
yargsInst.command(
commandMetadata.command,
commandMetadata.describe ?? "",
(argv) => {
for (const param of paramsMetadata) {
argv.option(param.name, param.option);
}
return argv;
},
async (argv) => {
const args = [];
for (const [key, value] of Object.entries(argv)) {
const matchingParameterMetadata = paramsMetadata.find(
(p) => p.name === key
);
if (matchingParameterMetadata) {
args[matchingParameterMetadata.index] = value;
}
}
await command[key].call(command, ...args);
}
);
}
}
}
}
await yargsInst
.scriptName(script)
.demandCommand(1)
.strict()
.help("h")
.alias("h", "help")
.alias("v", "version")
.parseAsync();
}
export function command(command?: CommandDefinition): MethodDecorator {
return (target, propertyKey, descriptor: PropertyDescriptor) => {
command = { command: command?.command ?? (propertyKey as string) };
Reflect.defineMetadata(
COMMAND_DEFINITION_METADATA,
{
...command,
},
descriptor.value
);
return descriptor;
};
}
export function option(option: Options & { name: string }): ParameterDecorator {
return (target: Record<string | symbol, any>, key, parameterIndex) => {
const existingParameterMetadata =
Reflect.getMetadata(COMMAND_PARAMETERS_METADATA, target[key]) ?? [];
const parameterTypes = Reflect.getMetadata(
"design:paramtypes",
target,
key
);
const coerceYargsType = (parameter: Object) => {
switch (parameter) {
case String:
return "string";
case Number:
return "number";
case Boolean:
return "boolean";
default:
throw new Error(
`Option type could not be inferred, please specify a 'type' for argument at position ` +
`${parameterIndex} on method ${String(key)}`
);
}
};
const parameterMetadata: CommandParameter[] = [
{
index: parameterIndex,
option: {
...option,
type: option.type ?? coerceYargsType(parameterTypes[parameterIndex]),
},
name: option.name,
},
...(existingParameterMetadata ?? []),
];
Reflect.defineMetadata(
COMMAND_PARAMETERS_METADATA,
parameterMetadata,
target[key]
);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment