Last active
February 4, 2022 02:51
-
-
Save eliellis/9c05057dc5cafb9f19a8757ea47f5281 to your computer and use it in GitHub Desktop.
Turn classes into yargs commands with Typescript decorators. WTFPL License.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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