From 7c04ce0a4611a0db1c74646c1d05869f36826282 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Mon, 21 Oct 2024 16:31:00 -0700 Subject: [PATCH] working w/parameters --- packages/aws-cdk/lib/cli.ts | 330 +--------------------- packages/aws-cdk/package.json | 3 + tools/@aws-cdk/yargs-gen/bin/yargs-gen.ts | 99 +++++-- tools/@aws-cdk/yargs-gen/lib/config.ts | 47 +-- 4 files changed, 121 insertions(+), 358 deletions(-) diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index 0e28a41a4c106..5e1f78eea2aa2 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -3,10 +3,10 @@ import '@jsii/check-node/run'; import * as chalk from 'chalk'; import { install as enableSourceMapSupport } from 'source-map-support'; -import type { Argv } from 'yargs'; import { DeploymentMethod } from './api'; import { HotswapMode } from './api/hotswap/common'; import { ILock } from './api/util/rwlock'; +import { parseCommandLineArguments } from './parse-command-line-arguments'; import { checkForPlatformWarnings } from './platform-warnings'; import { enableTracing } from './util/tracing'; import { SdkProvider } from '../lib/api/aws-auth'; @@ -17,342 +17,36 @@ import { execProgram } from '../lib/api/cxapp/exec'; import { Deployments } from '../lib/api/deployments'; import { PluginHost } from '../lib/api/plugin'; import { ToolkitInfo } from '../lib/api/toolkit-info'; -import { StackActivityProgress } from '../lib/api/util/cloudformation/stack-activity-monitor'; import { CdkToolkit, AssetBuildTime } from '../lib/cdk-toolkit'; import { realHandler as context } from '../lib/commands/context'; import { realHandler as docs } from '../lib/commands/docs'; import { realHandler as doctor } from '../lib/commands/doctor'; import { MIGRATE_SUPPORTED_LANGUAGES, getMigrateScanType } from '../lib/commands/migrate'; -import { RequireApproval } from '../lib/diff'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; import { data, debug, error, print, setLogLevel, setCI } from '../lib/logging'; import { Notices } from '../lib/notices'; import { Command, Configuration, Settings } from '../lib/settings'; import * as version from '../lib/version'; -// https://github.com/yargs/yargs/issues/1929 -// https://github.com/evanw/esbuild/issues/1492 -// eslint-disable-next-line @typescript-eslint/no-require-imports -const yargs = require('yargs'); - /* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-shadow */ // yargs -async function parseCommandLineArguments(args: string[]) { - // Use the following configuration for array arguments: - // - // { type: 'array', default: [], nargs: 1, requiresArg: true } - // - // The default behavior of yargs is to eat all strings following an array argument: - // - // ./prog --arg one two positional => will parse to { arg: ['one', 'two', 'positional'], _: [] } (so no positional arguments) - // ./prog --arg one two -- positional => does not help, for reasons that I can't understand. Still gets parsed incorrectly. - // - // By using the config above, every --arg will only consume one argument, so you can do the following: - // - // ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. - - const defaultBrowserCommand: { [key in NodeJS.Platform]?: string } = { - darwin: 'open %u', - win32: 'start %u', - }; - - const initTemplateLanguages = await availableInitLanguages(); - return yargs - .env('CDK') - .usage('Usage: cdk -a COMMAND') - .option('app', { type: 'string', alias: 'a', desc: 'REQUIRED WHEN RUNNING APP: command-line for executing your app or a cloud assembly directory (e.g. "node bin/my-app.js"). Can also be specified in cdk.json or ~/.cdk.json', requiresArg: true }) - .option('build', { type: 'string', desc: 'Command-line for a pre-synth build' }) - .option('context', { type: 'array', alias: 'c', desc: 'Add contextual string parameter (KEY=VALUE)', nargs: 1, requiresArg: true }) - .option('plugin', { type: 'array', alias: 'p', desc: 'Name or path of a node package that extend the CDK features. Can be specified multiple times', nargs: 1 }) - .option('trace', { type: 'boolean', desc: 'Print trace for stack warnings' }) - .option('strict', { type: 'boolean', desc: 'Do not construct stacks with warnings' }) - .option('lookups', { type: 'boolean', desc: 'Perform context lookups (synthesis fails if this is disabled and context lookups need to be performed)', default: true }) - .option('ignore-errors', { type: 'boolean', default: false, desc: 'Ignores synthesis errors, which will likely produce an invalid output' }) - .option('json', { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML when templates are printed to STDOUT', default: false }) - .option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs (specify multiple times to increase verbosity)', default: false }) - .count('verbose') - .option('debug', { type: 'boolean', desc: 'Enable emission of additional debugging information, such as creation stack traces of tokens', default: false }) - .option('profile', { type: 'string', desc: 'Use the indicated AWS profile as the default environment', requiresArg: true }) - .option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified', requiresArg: true }) - .option('ca-bundle-path', { type: 'string', desc: 'Path to CA certificate to use when validating HTTPS requests. Will read from AWS_CA_BUNDLE environment variable if not specified', requiresArg: true }) - .option('ec2creds', { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status' }) - .option('version-reporting', { type: 'boolean', desc: 'Include the "AWS::CDK::Metadata" resource in synthesized templates (enabled by default)', default: undefined }) - .option('path-metadata', { type: 'boolean', desc: 'Include "aws:cdk:path" CloudFormation metadata for each resource (enabled by default)', default: undefined }) - .option('asset-metadata', { type: 'boolean', desc: 'Include "aws:asset:*" CloudFormation metadata for resources that uses assets (enabled by default)', default: undefined }) - .option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined, requiresArg: true }) - .option('staging', { type: 'boolean', desc: 'Copy assets to the output directory (use --no-staging to disable the copy of assets which allows local debugging via the SAM CLI to reference the original source files)', default: true }) - .option('output', { type: 'string', alias: 'o', desc: 'Emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true }) - .option('notices', { type: 'boolean', desc: 'Show relevant notices' }) - .option('no-color', { type: 'boolean', desc: 'Removes colors and other style from console output', default: false }) - .option('ci', { type: 'boolean', desc: 'Force CI detection. If CI=true then logs will be sent to stdout instead of stderr', default: process.env.CI !== undefined }) - .command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => yargs - .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }) - .option('show-dependencies', { type: 'boolean', default: false, alias: 'd', desc: 'Display stack dependency information for each stack' }), - ) - .command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', (yargs: Argv) => yargs - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' }) - .option('validation', { type: 'boolean', desc: 'After synthesis, validate stacks with the "validateOnSynth" attribute set (can also be controlled with CDK_VALIDATION)', default: true }) - .option('quiet', { type: 'boolean', alias: 'q', desc: 'Do not output CloudFormation Template to stdout', default: false })) - .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', (yargs: Argv) => yargs - .option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined }) - .option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' }) - .option('example-permissions-boundary', { type: 'boolean', alias: 'epb', desc: 'Use the example permissions boundary.', default: undefined, conflicts: 'custom-permissions-boundary' }) - .option('custom-permissions-boundary', { type: 'string', alias: 'cpb', desc: 'Use the permissions boundary specified by name.', default: undefined, conflicts: 'example-permissions-boundary' }) - .option('bootstrap-customer-key', { type: 'boolean', desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', default: undefined, conflicts: 'bootstrap-kms-key-id' }) - .option('qualifier', { type: 'string', desc: 'String which must be unique for each bootstrap stack. You must configure it on your CDK app if you change this from the default.', default: undefined }) - .option('public-access-block-configuration', { type: 'boolean', desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', default: undefined }) - .option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] }) - .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - .option('trust', { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('trust-for-lookup', { type: 'array', desc: 'The AWS account IDs that should be trusted to look up values in this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('cloudformation-execution-policies', { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false }) - .option('termination-protection', { type: 'boolean', default: undefined, desc: 'Toggle CloudFormation termination protection on the bootstrap stacks' }) - .option('show-template', { type: 'boolean', desc: 'Instead of actual bootstrapping, print the current CLI\'s bootstrapping template to stdout for customization', default: false }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack to create', requiresArg: true }) - .option('template', { type: 'string', requiresArg: true, desc: 'Use the template from the given file instead of the built-in one (use --show-template to obtain an example)' }) - .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }), - ) - .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', (yargs: Argv) => yargs - .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' }) - .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }) - .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' }) - .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true }) - // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment - .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }) - .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet) (deprecated)', deprecated: true }) - .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create (only if method is not direct)' }) - .options('method', { - alias: 'm', - type: 'string', - choices: ['direct', 'change-set', 'prepare-change-set'], - requiresArg: true, - desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information', - }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) - .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) - .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) - .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the existing CDK toolkit stack (only used for app using legacy synthesis)', requiresArg: true }) - .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }) - .option('rollback', { - type: 'boolean', - desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + - 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', - }) - // Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729 - .option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true) - .option('hotswap', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'but does not fall back to a full deployment if that is not possible. ' + - 'Instead, changes to any non-hotswappable properties are ignored.' + - 'Do not use this in production environments', - }) - .option('hotswap-fallback', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'which skips CloudFormation and updates the resources directly, ' + - 'and falls back to a full deployment if that is not possible. ' + - 'Do not use this in production environments', - }) - .option('watch', { - type: 'boolean', - desc: 'Continuously observe the project files, ' + - 'and deploy the given stack(s) automatically when changes are detected. ' + - 'Implies --hotswap by default', - }) - .options('logs', { - type: 'boolean', - default: true, - desc: 'Show CloudWatch log events from all resources in the selected Stacks in the terminal. ' + - "'true' by default, use --no-logs to turn off. " + - "Only in effect if specified alongside the '--watch' option", - }) - .option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true }) - .option('asset-parallelism', { type: 'boolean', desc: 'Whether to build/publish assets in parallel' }) - .option('asset-prebuild', { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true }) - .option('ignore-no-stacks', { type: 'boolean', desc: 'Whether to deploy if the app contains no stacks', default: false }), - ) - .command('rollback [STACKS..]', 'Rolls back the stack(s) named STACKS to their last stable state', (yargs: Argv) => yargs - .option('all', { type: 'boolean', default: false, desc: 'Roll back all available stacks' }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack the environment is bootstrapped with', requiresArg: true }) - .option('force', { - alias: 'f', - type: 'boolean', - desc: 'Orphan all resources for which the rollback operation fails.', - }) - .option('validate-bootstrap-version', { - type: 'boolean', - desc: 'Whether to validate the bootstrap stack version. Defaults to \'true\', disable with --no-validate-bootstrap-version.', - }) - .option('orphan', { - // alias: 'o' conflicts with --output - type: 'array', - nargs: 1, - requiresArg: true, - desc: 'Orphan the given resources, identified by their logical ID (can be specified multiple times)', - default: [], - }), - ) - .command('import [STACK]', 'Import existing resource(s) into the given STACK', (yargs: Argv) => yargs - .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack to create', requiresArg: true }) - .option('rollback', { - type: 'boolean', - desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + - 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', - }) - .option('force', { - alias: 'f', - type: 'boolean', - desc: 'Do not abort if the template diff includes updates or deletes. This is probably safe but we\'re not sure, let us know how it goes.', - }) - .option('record-resource-mapping', { - type: 'string', - alias: 'r', - requiresArg: true, - desc: 'If specified, CDK will generate a mapping of existing physical resources to CDK resources to be imported as. The mapping ' + - 'will be written in the given file path. No actual import operation will be performed', - }) - .option('resource-mapping', { - type: 'string', - alias: 'm', - requiresArg: true, - desc: 'If specified, CDK will use the given file to map physical resources to CDK resources for import, instead of interactively ' + - 'asking the user. Can be run from scripts', - }), - ) - .command('watch [STACKS..]', "Shortcut for 'deploy --watch'", (yargs: Argv) => yargs - // I'm fairly certain none of these options, present for 'deploy', make sense for 'watch': - // .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' }) - // .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined }) - // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment - // .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }) - // .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - // These options, however, are more subtle - I could be convinced some of these should also be available for 'watch': - // .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' }) - // .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) - // .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) - // .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) - // .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true }) - .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }) - .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the existing CDK toolkit stack (only used for app using legacy synthesis)', requiresArg: true }) - .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }) - .option('rollback', { - type: 'boolean', - desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + - 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', - }) - // same hack for -R as above in 'deploy' - .option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true) - .option('hotswap', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'but does not fall back to a full deployment if that is not possible. ' + - 'Instead, changes to any non-hotswappable properties are ignored.' + - "'true' by default, use --no-hotswap to turn off", - }) - .option('hotswap-fallback', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'which skips CloudFormation and updates the resources directly, ' + - 'and falls back to a full deployment if that is not possible.', - }) - .options('logs', { - type: 'boolean', - default: true, - desc: 'Show CloudWatch log events from all resources in the selected Stacks in the terminal. ' + - "'true' by default, use --no-logs to turn off", - }) - .option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true }), - ) - .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', (yargs: Argv) => yargs - .option('all', { type: 'boolean', default: false, desc: 'Destroy all available stacks' }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only destroy requested stacks, don\'t include dependees' }) - .option('force', { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' })) - .command('diff [STACKS..]', 'Compares the specified stack with the deployed stack or a local template file, and returns with status 1 if any difference is found', (yargs: Argv) => yargs - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only diff requested stacks, don\'t include dependencies' }) - .option('context-lines', { type: 'number', desc: 'Number of context lines to include in arbitrary JSON diff rendering', default: 3, requiresArg: true }) - .option('template', { type: 'string', desc: 'The path to the CloudFormation template to compare with', requiresArg: true }) - .option('strict', { type: 'boolean', desc: 'Do not filter out AWS::CDK::Metadata resources, mangled non-ASCII characters, or the CheckBootstrapVersionRule', default: false }) - .option('security-only', { type: 'boolean', desc: 'Only diff for broadened security changes', default: false }) - .option('fail', { type: 'boolean', desc: 'Fail with exit code 1 in case of diff' }) - .option('processed', { type: 'boolean', desc: 'Whether to compare against the template with Transforms already processed', default: false }) - .option('quiet', { type: 'boolean', alias: 'q', desc: 'Do not print stack name and default message when there is no diff to stdout', default: false }) - .option('change-set', { type: 'boolean', alias: 'changeset', desc: 'Whether to create a changeset to analyze resource replacements. In this mode, diff will use the deploy role instead of the lookup role.', default: true })) - .command('metadata [STACK]', 'Returns all metadata associated with this stack') - .command(['acknowledge [ID]', 'ack [ID]'], 'Acknowledge a notice so that it does not show up anymore') - .command('notices', 'Returns a list of relevant notices', (yargs: Argv) => yargs - .option('unacknowledged', { type: 'boolean', alias: 'u', default: false, desc: 'Returns a list of unacknowledged notices' }), - ) - .command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', (yargs: Argv) => yargs - .option('language', { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanguages }) - .option('list', { type: 'boolean', desc: 'List the available templates' }) - .option('generate-only', { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }), - ) - .command('migrate', false /* hidden from "cdk --help" */, (yargs: Argv) => yargs - .option('stack-name', { type: 'string', alias: 'n', desc: 'The name assigned to the stack created in the new project. The name of the app will be based off this name as well.', requiresArg: true }) - .option('language', { type: 'string', default: 'typescript', alias: 'l', desc: 'The language to be used for the new project', choices: MIGRATE_SUPPORTED_LANGUAGES }) - .option('account', { type: 'string', desc: 'The account to retrieve the CloudFormation stack template from' }) - .option('region', { type: 'string', desc: 'The region to retrieve the CloudFormation stack template from' }) - .option('from-path', { type: 'string', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' }) - .option('from-stack', { type: 'boolean', desc: 'Use this flag to retrieve the template for an existing CloudFormation stack' }) - .option('output-path', { type: 'string', desc: 'The output path for the migrated CDK app' }) - .option('from-scan', { - type: 'string', - desc: 'Determines if a new scan should be created, or the last successful existing scan should be used ' + - '\n options are "new" or "most-recent"', - }) - .option('filter', { - type: 'array', - desc: 'Filters the resource scan based on the provided criteria in the following format: "key1=value1,key2=value2"' + - '\n This field can be passed multiple times for OR style filtering: ' + - '\n filtering options: ' + - '\n resource-identifier: A key-value pair that identifies the target resource. i.e. {"ClusterName", "myCluster"}' + - '\n resource-type-prefix: A string that represents a type-name prefix. i.e. "AWS::DynamoDB::"' + - '\n tag-key: a string that matches resources with at least one tag with the provided key. i.e. "myTagKey"' + - '\n tag-value: a string that matches resources with at least one tag with the provided value. i.e. "myTagValue"', - }) - .option('compress', { type: 'boolean', desc: 'Use this flag to zip the generated CDK app' }), - ) - .command('context', 'Manage cached context values', (yargs: Argv) => yargs - .option('reset', { alias: 'e', desc: 'The context key (or its index) to reset', type: 'string', requiresArg: true }) - .option('force', { alias: 'f', desc: 'Ignore missing key error', type: 'boolean', default: false }) - .option('clear', { desc: 'Clear all context', type: 'boolean' })) - .command(['docs', 'doc'], 'Opens the reference documentation in a browser', (yargs: Argv) => yargs - .option('browser', { - alias: 'b', - desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open', - type: 'string', - default: process.platform in defaultBrowserCommand ? defaultBrowserCommand[process.platform] : 'xdg-open %u', - })) - .command('doctor', 'Check your set-up for potential problems') - .version(version.DISPLAY_VERSION) - .demandCommand(1, '') // just print help - .recommendCommands() - .help() - .alias('h', 'help') - .epilogue([ - 'If your app has a single stack, there is no need to specify the stack name', - 'If one of cdk.json or ~/.cdk.json exists, options specified there will be used as defaults. Settings in cdk.json take precedence.', - ].join('\n\n')) - .parse(args); -} - if (!process.stdout.isTTY) { // Disable chalk color highlighting process.env.FORCE_COLOR = '0'; } export async function exec(args: string[], synthesizer?: Synthesizer): Promise { - const argv = await parseCommandLineArguments(args); + function makeBrowserDefault(): string | undefined { + const defaultBrowserCommand: { [key in NodeJS.Platform]?: string } = { + darwin: 'open %u', + win32: 'start %u', + }; + + return process.platform in defaultBrowserCommand ? defaultBrowserCommand[process.platform] : 'xdg-open %u'; + } + + const argv = await parseCommandLineArguments(args, makeBrowserDefault(), await availableInitLanguages(), MIGRATE_SUPPORTED_LANGUAGES as string[], version.DISPLAY_VERSION, yargsNegativeAlias); if (argv.verbose) { setLogLevel(argv.verbose); @@ -792,7 +486,7 @@ function arrayFromYargs(xs: string[]): string[] | undefined { return xs.filter(x => x !== ''); } -function yargsNegativeAlias(shortName: S, longName: L) { +function yargsNegativeAlias(shortName: S, longName: L): (argv: T) => T { return (argv: T) => { if (shortName in argv && argv[shortName]) { (argv as any)[longName] = false; diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 6fbe829021441..ac7c1483b8edd 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -28,6 +28,9 @@ "attributions:update": "yarn node-bundle validate --entrypoint lib/index.ts --dont-attribute \"^@aws-cdk/|^cdk-assets|^cdk-cli-wrapper$\" --fix" }, "cdk-build": { + "pre": [ + "yarn yargs-gen" + ], "post": [ "cp ../../node_modules/cdk-from-cfn/index_bg.wasm ./lib/", "cp ../../node_modules/@aws-cdk/aws-service-spec/db.json.gz ./" diff --git a/tools/@aws-cdk/yargs-gen/bin/yargs-gen.ts b/tools/@aws-cdk/yargs-gen/bin/yargs-gen.ts index 66acd12f04ed5..665d5bcba74b6 100755 --- a/tools/@aws-cdk/yargs-gen/bin/yargs-gen.ts +++ b/tools/@aws-cdk/yargs-gen/bin/yargs-gen.ts @@ -1,30 +1,72 @@ -import { Expression, FreeFunction, Module, Statement, TypeScriptRenderer, code } from '@cdklabs/typewriter'; +import * as fs from 'fs'; +import { Expression, FreeFunction, Module, SelectiveModuleImport, Statement, Type, TypeScriptRenderer, code } from '@cdklabs/typewriter'; import { CliConfig, makeConfig } from '../lib/config'; async function main() { const scope = new Module('aws-cdk'); + + scope.addImport(new SelectiveModuleImport(scope, 'yargs', ['Argv'])); + //scope.addImport(new SelectiveModuleImport(scope, '../lib/api/util/cloudformation/stack-activity-monitor', ['StackActivityProgress'])); + //scope.addImport(new SelectiveModuleImport(scope, '../lib/diff', ['RequireApproval'])); + + scope.addInitialization(code.comment( + 'https://github.com/yargs/yargs/issues/1929', + 'https://github.com/evanw/esbuild/issues/1492', + 'eslint-disable-next-line @typescript-eslint/no-require-imports', + )); + scope.addInitialization(code.stmt.constVar(code.expr.ident('yargs'), code.expr.directCode("require('yargs')"))); + const parseCommandLineArguments = new FreeFunction(scope, { name: 'parseCommandLineArguments', + export: true, + returnType: Type.ANY, + parameters: [ + { name: 'args', type: Type.arrayOf(Type.STRING) }, + { name: 'browserDefault', type: Type.STRING, optional: true }, + { name: 'availableInitLanguages', type: Type.arrayOf(Type.STRING) }, + { name: 'migrateSupportedLanguages', type: Type.arrayOf(Type.STRING) }, + { name: 'version', type: Type.STRING }, + { name: 'yargsNegativeAlias', type: Type.ANY }, + ], }); - parseCommandLineArguments.addBody(makeYargs(makeConfig())); + parseCommandLineArguments.addBody(makeYargs(await makeConfig()/*, scope*/)); const renderer = new TypeScriptRenderer(); // eslint-disable-next-line no-console console.log(renderer.render(scope)); + const eslintBlock = ` +/* eslint-disable comma-spacing */ +/* eslint-disable @typescript-eslint/comma-dangle */ +/* eslint-disable quote-props */ +/* eslint-disable quotes */`; + + fs.writeFileSync('./lib/parse-command-line-arguments.ts', `${eslintBlock}\n`); + fs.appendFileSync('./lib/parse-command-line-arguments.ts', renderer.render(scope)); } interface MiddlewareExpression { - callbacks: Expression; + callback: Expression; applyBeforeValidation?: Expression; } -function makeYargs(config: CliConfig): Statement { - const preamble = `yargs - .env('CDK') - .usage('Usage: cdk -a COMMAND') - `; +// Use the following configuration for array arguments: +// +// { type: 'array', default: [], nargs: 1, requiresArg: true } +// +// The default behavior of yargs is to eat all strings following an array argument: +// +// ./prog --arg one two positional => will parse to { arg: ['one', 'two', 'positional'], _: [] } (so no positional arguments) +// ./prog --arg one two -- positional => does not help, for reasons that I can't understand. Still gets parsed incorrectly. +// +// By using the config above, every --arg will only consume one argument, so you can do the following: +// +// ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. +function makeYargs(config: CliConfig/*, scope: ScopeImpl*/): Statement { + //const yargs = new ThingSymbol('yargs', scope); + //let yargsExpr = new SymbolReference(yargs).callMethod('env', code.expr.lit('CDK')); + let yargsExpr: Expression = code.expr.ident('yargs'); + yargsExpr = yargsExpr.callMethod('usage', code.expr.lit('Usage: cdk -a COMMAND')); - let yargsExpr = code.expr.directCode(preamble); for (const command of Object.keys(config.commands)) { const commandFacts = config.commands[command]; const commandArg = commandFacts.arg @@ -48,34 +90,55 @@ function makeYargs(config: CliConfig): Statement { // middleware is a separate function call, so we can't store it with the regular option arguments, as those will all be treated as parameters: // .option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true) middleware = { - callbacks: code.expr.lit(optionFacts.middleware!.callbacks.toString()), + callback: code.expr.builtInFn(optionFacts.middleware!.callback, code.expr.lit(optionFacts.middleware!.args)), applyBeforeValidation: code.expr.lit(optionFacts.middleware!.applyBeforeValidation), }; break; - default: - optionArgs[optionProp] = code.expr.lit((optionFacts as any)[optionProp]); + if ((optionFacts as any)[optionProp].dynamicType === 'parameter') { + optionArgs[optionProp] = code.expr.ident((optionFacts as any)[optionProp].dynamicValue); + } else { + optionArgs[optionProp] = code.expr.lit((optionFacts as any)[optionProp]); + } } } optionsExpr = optionsExpr.callMethod('option', code.expr.lit(`${option}`), code.expr.object(optionArgs)); if (middleware) { - optionsExpr = optionsExpr.callMethod('middleware', middleware.callbacks, middleware.applyBeforeValidation ?? code.expr.UNDEFINED); + optionsExpr = optionsExpr.callMethod('middleware', middleware.callback, middleware.applyBeforeValidation ?? code.expr.UNDEFINED); middleware = undefined; } } - // tail-recursive? yargsExpr = commandFacts.options ? yargsExpr.callMethod('command', code.expr.directCode(`['${command}${commandArg}'${aliases}]`), code.expr.lit(commandFacts.description), optionsExpr) : yargsExpr.callMethod('command', code.expr.directCode(`['${command}${commandArg}'${aliases}]`), code.expr.lit(commandFacts.description)); } - return code.stmt.ret(yargsExpr); + return code.stmt.ret(makeEpilogue(yargsExpr)); } -main().then(() => { +function makeEpilogue(prefix: Expression) { + let completeThing = prefix.callMethod('version', code.expr.ident('version')); + completeThing = completeThing.callMethod('demandCommand', code.expr.lit(1), code.expr.lit("''")); // just print help + completeThing = completeThing.callMethod('recommendCommands'); + completeThing = completeThing.callMethod('help'); + completeThing = completeThing.callMethod('alias', code.expr.lit('h'), code.expr.lit('help')); + completeThing = completeThing.callMethod('epilogue', code.expr.lit([ + 'If your app has a single stack, there is no need to specify the stack name', + 'If one of cdk.json or ~/.cdk.json exists, options specified there will be used as defaults. Settings in cdk.json take precedence.', + ].join('\n\n'))); -}).catch(() => { + completeThing = completeThing.callMethod('parse', code.expr.ident('args')); -}); \ No newline at end of file + return completeThing; +} + +main().then(() => { + // eslint-disable-next-line no-console + console.log('waoh 1'); + +}).catch((e) => { + // eslint-disable-next-line no-console + console.error(e); +}); diff --git a/tools/@aws-cdk/yargs-gen/lib/config.ts b/tools/@aws-cdk/yargs-gen/lib/config.ts index 4de430b9dcf8e..626e66f721564 100644 --- a/tools/@aws-cdk/yargs-gen/lib/config.ts +++ b/tools/@aws-cdk/yargs-gen/lib/config.ts @@ -5,7 +5,6 @@ interface YargsCommand { description: string; options?: { [optionName: string]: YargsOption }; aliases?: string[]; - //args?: { [argName: string]: YargsArg }; arg?: YargsArg; } @@ -53,7 +52,8 @@ interface YargsOption { } export interface Middleware { - callbacks: MiddlewareFunction | ReadonlyArray; + callback: string; + args: string[]; applyBeforeValidation?: boolean; } @@ -61,20 +61,7 @@ export interface CliConfig { commands: { [commandName: string]: YargsCommand }; } -// copied from yargs -type MiddlewareFunction = (args: any) => void; -// end copied from yargs - -function yargsNegativeAlias(shortName: S, longName: L) { - return (argv: T) => { - if (shortName in argv && argv[shortName]) { - (argv as any)[longName] = false; - } - return argv; - }; -} - -export function makeConfig(): CliConfig { +export async function makeConfig(): Promise { const config: CliConfig = { commands: { deploy: { @@ -112,7 +99,8 @@ export function makeConfig(): CliConfig { hidden: true, // Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729 middleware: { - callbacks: yargsNegativeAlias('R', 'rollback'), + callback: 'yargsNegativeAlias', + args: ['R', 'rollback'], applyBeforeValidation: true, }, }, @@ -252,7 +240,8 @@ export function makeConfig(): CliConfig { type: 'boolean', hidden: true, middleware: { - callbacks: yargsNegativeAlias('R', 'rollback'), + callback: 'yargsNegativeAlias', + args: ['R', 'rollback'], applyBeforeValidation: true, }, }, @@ -336,7 +325,7 @@ export function makeConfig(): CliConfig { variadic: false, }, options: { - 'language': { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)' /*, choices: initTemplateLanguages*/ }, // TODO: preamble, this initTemplateLanguages variable needs to go as a statement there. + 'language': { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: DynamicValue.fromParameter('availableInitLanguages') } as any, // TODO: preamble, this initTemplateLanguages variable needs to go as a statement there. 'list': { type: 'boolean', desc: 'List the available templates' }, 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, }, @@ -345,7 +334,7 @@ export function makeConfig(): CliConfig { description: false as any, options: { 'stack-name': { type: 'string', alias: 'n', desc: 'The name assigned to the stack created in the new project. The name of the app will be based off this name as well.', requiresArg: true }, - 'language': { type: 'string', default: 'typescript', alias: 'l', desc: 'The language to be used for the new project'/*, choices: MIGRATE_SUPPORTED_LANGUAGES*/ }, // TODO: preamble + 'language': { type: 'string', default: 'typescript', alias: 'l', desc: 'The language to be used for the new project', choices: DynamicValue.fromParameter('migrateSupportedLanguages') as any }, 'account': { type: 'string', desc: 'The account to retrieve the CloudFormation stack template from' }, 'region': { type: 'string', desc: 'The region to retrieve the CloudFormation stack template from' }, 'from-path': { type: 'string', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' }, @@ -385,7 +374,7 @@ export function makeConfig(): CliConfig { alias: 'b', desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open', type: 'string', - //default: process.platform in defaultBrowserCommand ? defaultBrowserCommand[process.platform] : 'xdg-open %u', // TODO: preamble + default: DynamicValue.fromParameter('browserDefault'), }, }, }, @@ -396,4 +385,18 @@ export function makeConfig(): CliConfig { }; return config; -} \ No newline at end of file +} + +export interface DynamicResult { + dynamicType: 'parameter'; + dynamicValue: string; +} + +export class DynamicValue { + public static fromParameter(parameterName: string): DynamicResult { + return { + dynamicType: 'parameter', + dynamicValue: parameterName, + }; + } +}