packages/@aws-cdk/user-input-gen/lib/convert-to-user-input-gen.ts (145 lines of code) (raw):

import { code, FreeFunction, Module, SelectiveModuleImport, Type, TypeScriptRenderer } from '@cdklabs/typewriter'; import { EsLintRules } from '@cdklabs/typewriter/lib/eslint-rules'; import * as prettier from 'prettier'; import { kebabToCamelCase, SOURCE_OF_TRUTH } from './util'; import type { CliAction, CliConfig } from './yargs-types'; const CLI_ARG_NAME = 'args'; const CONFIG_ARG_NAME = 'config'; export async function renderUserInputFuncs(config: CliConfig): Promise<string> { const scope = new Module('aws-cdk'); scope.documentation.push( '-------------------------------------------------------------------------------------------'); scope.documentation.push(`GENERATED FROM ${SOURCE_OF_TRUTH}.`); scope.documentation.push('Do not edit by hand; all changes will be overwritten at build time from the config file.'); scope.documentation.push('-------------------------------------------------------------------------------------------'); scope.addImport(new SelectiveModuleImport(scope, './user-configuration', ['Command'])); scope.addImport(new SelectiveModuleImport(scope, './user-input', ['UserInput', 'GlobalOptions'])); const userInputType = Type.fromName(scope, 'UserInput'); const convertYargsToUserInput = new FreeFunction(scope, { name: 'convertYargsToUserInput', export: true, returnType: userInputType, parameters: [ { name: 'args', type: Type.ANY }, ], }); convertYargsToUserInput.addBody(code.expr.directCode(buildYargsToUserInputFunction(config))); const createConfigArguments = new FreeFunction(scope, { name: 'convertConfigToUserInput', export: true, returnType: userInputType, parameters: [ { name: 'config', type: Type.ANY }, ], }); createConfigArguments.addBody(code.expr.directCode(buildConfigArgsFunction(config))); const ts = new TypeScriptRenderer({ disabledEsLintRules: [ EsLintRules.MAX_LEN, // the default disabled rules result in 'Definition for rule 'prettier/prettier' was not found '@typescript-eslint/consistent-type-imports', // (ironically) typewriter does not support type imports ], }).render(scope); return prettier.format(ts, { parser: 'typescript', printWidth: 150, singleQuote: true, quoteProps: 'consistent', }); } function buildYargsToUserInputFunction(config: CliConfig): string { const globalOptions = buildGlobalOptions(config, CLI_ARG_NAME); const commandSwitch = buildCommandSwitch(config, CLI_ARG_NAME); const userInput = buildUserInput(CLI_ARG_NAME); return [ globalOptions, commandSwitch, userInput, ].join('\n'); } function buildConfigArgsFunction(config: CliConfig): string { const globalOptions = buildGlobalOptions(config, CONFIG_ARG_NAME); const commandList = buildCommandsList(config, CONFIG_ARG_NAME); const configArgs = buildConfigArgs(config); return [ globalOptions, commandList, configArgs, ].join('\n'); } function buildGlobalOptions(config: CliConfig, argName: string): string { const globalOptionExprs = ['const globalOptions: GlobalOptions = {']; for (const optionName of Object.keys(config.globalOptions)) { const name = kebabToCamelCase(optionName); globalOptionExprs.push(`'${name}': ${argName}.${name},`); } globalOptionExprs.push('}'); return globalOptionExprs.join('\n'); } function buildCommandsList(config: CliConfig, argName: string): string { const commandOptions: string[] = []; // Note: we are intentionally not including aliases for the default options that can be // specified via `cdk.json`. These options must be specified by the command name // i.e. acknowledge rather than ack. for (const commandName of Object.keys(config.commands)) { commandOptions.push(`const ${kebabToCamelCase(commandName)}Options = {`); commandOptions.push(...buildCommandOptions(config.commands[commandName], argName, kebabToCamelCase(commandName))); commandOptions.push('}'); } return commandOptions.join('\n'); } function buildCommandSwitch(config: CliConfig, argName: string): string { const commandSwitchExprs = ['let commandOptions;', `switch (${argName}._[0] as Command) {`]; for (const commandName of Object.keys(config.commands)) { commandSwitchExprs.push( // All aliases of the command should map to the same switch branch // This ensures that we store options of the command regardless of what alias is specified ...buildAliases(commandName, config.commands[commandName].aliases), 'commandOptions = {', ...buildCommandOptions(config.commands[commandName], argName), ...(config.commands[commandName].arg ? [buildPositionalArguments(config.commands[commandName].arg, argName)] : []), '};', `break; `); } commandSwitchExprs.push('}'); return commandSwitchExprs.join('\n'); } function buildAliases(commandName: string, aliases: string[] = []): string[] { const cases = [commandName, ...aliases]; return cases.map((c) => `case '${c}':`); } function buildCommandOptions(options: CliAction, argName: string, prefix?: string): string[] { const commandOptions: string[] = []; for (const optionName of Object.keys(options.options ?? {})) { const name = kebabToCamelCase(optionName); if (prefix) { commandOptions.push(`'${name}': ${argName}.${prefix}?.${name},`); } else { commandOptions.push(`'${name}': ${argName}.${name},`); } } return commandOptions; } function buildPositionalArguments(arg: { name: string; variadic: boolean }, argName: string): string { if (arg.variadic) { return `${arg.name}: ${argName}.${arg.name}`; } return `${arg.name}: ${argName}.${arg.name}`; } function buildUserInput(argName: string): string { return [ 'const userInput: UserInput = {', `command: ${argName}._[0],`, 'globalOptions,', `[${argName}._[0]]: commandOptions`, '}', '', 'return userInput', ].join('\n'); } function buildConfigArgs(config: CliConfig): string { return [ 'const userInput: UserInput = {', 'globalOptions,', ...(Object.keys(config.commands).map((commandName) => { return `'${commandName}': ${kebabToCamelCase(commandName)}Options,`; })), '}', '', 'return userInput', ].join('\n'); }