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

import { Module, SelectiveModuleImport, StructType, Type, TypeScriptRenderer } from '@cdklabs/typewriter'; import { EsLintRules } from '@cdklabs/typewriter/lib/eslint-rules'; import * as prettier from 'prettier'; import { kebabToCamelCase, kebabToPascal, SOURCE_OF_TRUTH } from './util'; import type { CliConfig } from './yargs-types'; export async function renderUserInputType(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('-------------------------------------------------------------------------------------------'); const userInputType = new StructType(scope, { export: true, name: 'UserInput', docs: { summary: 'The structure of the user input -- either CLI options or cdk.json -- generated from packages/aws-cdk/lib/config.ts', }, }); // add required command scope.addImport(new SelectiveModuleImport(scope, './user-configuration', ['Command'])); const commandEnum = Type.fromName(scope, 'Command'); userInputType.addProperty({ name: 'command', type: commandEnum, docs: { summary: 'The CLI command name', }, optional: true, }); // add global options const globalOptionType = new StructType(scope, { export: true, name: 'GlobalOptions', docs: { summary: 'Global options available to all CLI commands', }, }); for (const [optionName, option] of Object.entries(config.globalOptions)) { globalOptionType.addProperty({ name: kebabToCamelCase(optionName), type: convertType(option.type, option.count), docs: { default: normalizeDefault(option.default), summary: option.desc, deprecated: option.deprecated ? String(option.deprecated) : undefined, }, optional: true, }); } userInputType.addProperty({ name: 'globalOptions', type: Type.fromName(scope, globalOptionType.name), docs: { summary: 'Global options available to all CLI commands', }, optional: true, }); // add command-specific options for (const [commandName, command] of Object.entries(config.commands)) { let commandType: Type = Type.anonymousInterface([]); const commandOptions = Object.entries(command.options ?? {}); // if we have something to add to an interface if (command.arg || commandOptions.length) { const commandStruct = new StructType(scope, { export: true, name: `${kebabToPascal(commandName)}Options`, docs: { summary: command.description, remarks: command.aliases ? `aliases: ${command.aliases.join(' ')}` : undefined, }, }); commandType = Type.fromName(scope, commandStruct.name); // add command level options for (const [optionName, option] of commandOptions) { commandStruct.addProperty({ name: kebabToCamelCase(optionName), type: convertType(option.type, option.count), docs: { // Notification Arns is a special property where undefined and [] mean different things default: optionName === 'notification-arns' ? 'undefined' : normalizeDefault(option.default), summary: option.desc, deprecated: option.deprecated ? String(option.deprecated) : undefined, remarks: option.alias ? `aliases: ${Array.isArray(option.alias) ? option.alias.join(' ') : option.alias}` : undefined, }, optional: true, }); } // add positional argument associated with the command if (command.arg) { commandStruct.addProperty({ name: command.arg.name, type: command.arg.variadic ? Type.arrayOf(Type.STRING) : Type.STRING, docs: { summary: `Positional argument for ${commandName}`, }, optional: true, }); } } userInputType.addProperty({ name: kebabToCamelCase(commandName), type: commandType, docs: { summary: command.description, remarks: command.aliases ? `aliases: ${command.aliases.join(' ')}` : undefined, }, optional: true, }); } 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 convertType(type: 'string' | 'array' | 'number' | 'boolean' | 'count', count?: boolean): Type { switch (type) { case 'boolean': return count ? Type.NUMBER : Type.BOOLEAN; case 'string': return Type.STRING; case 'number': return Type.NUMBER; case 'array': return Type.arrayOf(Type.STRING); case 'count': return Type.NUMBER; } } function normalizeDefault(defaultValue: any): string { switch (typeof defaultValue) { case 'boolean': case 'string': case 'number': case 'object': return JSON.stringify(defaultValue); // In these cases we cannot use the given defaultValue, so we then check the type // of the option to determine the default value case 'undefined': case 'function': default: return 'undefined'; } }