diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index ed38899c79555..336f93e225b2c 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -615,9 +615,7 @@ export class CdkToolkit { const rootDir = path.dirname(path.resolve(PROJECT_CONFIG)); debug("root directory used for 'watch' is: %s", rootDir); - const watchSettings: { include?: string | string[]; exclude: string | string[] } | undefined = - this.props.configuration.settings.get(['watch']); - if (!watchSettings) { + if (options.include === undefined && options.exclude === undefined) { throw new ToolkitError( "Cannot use the 'watch' command without specifying at least one directory to monitor. " + 'Make sure to add a "watch" key to your cdk.json', @@ -629,7 +627,7 @@ export class CdkToolkit { // 2. "watch" setting without an "include" key? We default to observing "./**". // 3. "watch" setting with an empty "include" key? We default to observing "./**". // 4. Non-empty "include" key? Just use the "include" key. - const watchIncludes = this.patternsArrayForWatch(watchSettings.include, { + const watchIncludes = this.patternsArrayForWatch(options.include, { rootDir, returnRootDirIfEmpty: true, }); @@ -641,11 +639,11 @@ export class CdkToolkit { // 2. Any file whose name starts with a dot. // 3. Any directory's content whose name starts with a dot. // 4. Any node_modules and its content (even if it's not a JS/TS project, you might be using a local aws-cli package) - const outputDir = this.props.configuration.settings.get(['globalOptions', 'output']); - const watchExcludes = this.patternsArrayForWatch(watchSettings.exclude, { + const output = options.output ?? 'cdk.out'; + const watchExcludes = this.patternsArrayForWatch(options.exclude, { rootDir, returnRootDirIfEmpty: false, - }).concat(`${outputDir}/**`, '**/.*', '**/.*/**', '**/node_modules/**'); + }).concat(`${output}/**`, '**/.*', '**/.*/**', '**/node_modules/**'); debug("'exclude' patterns for 'watch': %s", watchExcludes); // Since 'cdk deploy' is a relatively slow operation for a 'watch' process, @@ -1522,6 +1520,27 @@ interface WatchOptions extends Omit { * @default 1 */ readonly concurrency?: number; + + /** + * Path where the CloudAssembly is located + * + * @default 'cdk.out' + */ + readonly output?: string; + + /** + * Include these patterns in watch + * + * @default [] + */ + readonly include?: string[]; + + /** + * Exclude these patterns from watch + * + * @default [] + */ + readonly exclude?: string[]; } export interface DeployOptions extends CfnDeployOptions, WatchOptions { diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index c0110ebe2ecb0..a960ca2bbada2 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -354,6 +354,8 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { 'and deploy the given stack(s) automatically when changes are detected. ' + 'Implies --hotswap by default', }, + 'include': { type: 'array', desc: 'Watch files with these patterns', default: [] }, + 'exclude': { type: 'array', desc: 'Do not watch files with these patterns', default: [] }, 'logs': { type: 'boolean', default: true, @@ -249,6 +251,8 @@ export async function makeConfig(): Promise { variadic: true, }, options: { + 'include': { type: 'array', desc: 'Include files with these patterns', default: [] }, + 'exclude': { type: 'array', desc: 'Exclude files with these patterns', default: [] }, 'build-exclude': { type: 'array', alias: 'E', desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }, 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }, 'change-set-name': { type: 'string', desc: 'Name of the CloudFormation change set to create' }, diff --git a/packages/aws-cdk/lib/convert-to-user-input.ts b/packages/aws-cdk/lib/convert-to-user-input.ts index cb103b8f65f5c..dedd6d875fd1b 100644 --- a/packages/aws-cdk/lib/convert-to-user-input.ts +++ b/packages/aws-cdk/lib/convert-to-user-input.ts @@ -114,6 +114,8 @@ export function convertYargsToUserInput(args: any): UserInput { hotswap: args.hotswap, hotswapFallback: args.hotswapFallback, watch: args.watch, + include: args.include, + exclude: args.exclude, logs: args.logs, concurrency: args.concurrency, assetParallelism: args.assetParallelism, @@ -149,6 +151,8 @@ export function convertYargsToUserInput(args: any): UserInput { case 'watch': commandOptions = { + include: args.include, + exclude: args.exclude, buildExclude: args.buildExclude, exclusively: args.exclusively, changeSetName: args.changeSetName, @@ -346,6 +350,8 @@ export function convertConfigToUserInput(config: any): UserInput { hotswap: config.deploy?.hotswap, hotswapFallback: config.deploy?.hotswapFallback, watch: config.deploy?.watch, + include: Array.isArray(config.deploy?.include) ? config.deploy?.include : [config.deploy?.include], + exclude: Array.isArray(config.deploy?.exclude) ? config.deploy?.exclude : [config.deploy?.exclude], logs: config.deploy?.logs, concurrency: config.deploy?.concurrency, assetParallelism: config.deploy?.assetParallelism, @@ -369,6 +375,8 @@ export function convertConfigToUserInput(config: any): UserInput { resourceMapping: config.import?.resourceMapping, }; const watchOptions = { + include: Array.isArray(config.watch?.include) ? config.watch?.include : [config.watch?.include], + exclude: Array.isArray(config.watch?.exclude) ? config.watch?.exclude : [config.watch?.exclude], buildExclude: config.watch?.buildExclude, exclusively: config.watch?.exclusively, changeSetName: config.watch?.changeSetName, diff --git a/packages/aws-cdk/lib/parse-command-line-arguments.ts b/packages/aws-cdk/lib/parse-command-line-arguments.ts index 20b09694290fa..a3750237c72ad 100644 --- a/packages/aws-cdk/lib/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/parse-command-line-arguments.ts @@ -465,6 +465,20 @@ export function parseCommandLineArguments(args: Array): any { type: 'boolean', desc: 'Continuously observe the project files, and deploy the given stack(s) automatically when changes are detected. Implies --hotswap by default', }) + .option('include', { + default: [], + type: 'array', + desc: 'Watch files with these patterns', + nargs: 1, + requiresArg: true, + }) + .option('exclude', { + default: [], + type: 'array', + desc: 'Do not watch files with these patterns', + nargs: 1, + requiresArg: true, + }) .option('logs', { default: true, type: 'boolean', @@ -570,6 +584,20 @@ export function parseCommandLineArguments(args: Array): any { ) .command('watch [STACKS..]', "Shortcut for 'deploy --watch'", (yargs: Argv) => yargs + .option('include', { + default: [], + type: 'array', + desc: 'Include files with these patterns', + nargs: 1, + requiresArg: true, + }) + .option('exclude', { + default: [], + type: 'array', + desc: 'Exclude files with these patterns', + nargs: 1, + requiresArg: true, + }) .option('build-exclude', { default: [], type: 'array', diff --git a/packages/aws-cdk/lib/user-input.ts b/packages/aws-cdk/lib/user-input.ts index 03342227148fe..02ec356e1d4a7 100644 --- a/packages/aws-cdk/lib/user-input.ts +++ b/packages/aws-cdk/lib/user-input.ts @@ -734,6 +734,20 @@ export interface DeployOptions { */ readonly watch?: boolean; + /** + * Watch files with these patterns + * + * @default - [] + */ + readonly include?: Array; + + /** + * Do not watch files with these patterns + * + * @default - [] + */ + readonly exclude?: Array; + /** * 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 * @@ -897,6 +911,20 @@ export interface ImportOptions { * @struct */ export interface WatchOptions { + /** + * Include files with these patterns + * + * @default - [] + */ + readonly include?: Array; + + /** + * Exclude files with these patterns + * + * @default - [] + */ + readonly exclude?: Array; + /** * Do not rebuild asset with the given ID. Can be specified multiple times * diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 8dad7142baea7..2a4ae902906fa 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -92,6 +92,7 @@ import { CdkToolkit, markTesting, Tag } from '../lib/cdk-toolkit'; import { RequireApproval } from '../lib/diff'; import { Configuration } from '../lib/settings'; import { flatten } from '../lib/util'; +import { convertConfigToUserInput } from '../lib/convert-to-user-input'; markTesting(); @@ -971,12 +972,12 @@ describe('watch', () => { }); test('observes only the root directory by default', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY, + include: [], }); const includeArgs = fakeChokidarWatch.includeArgs; @@ -984,55 +985,59 @@ describe('watch', () => { }); test("allows providing a single string in 'watch.include'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - include: 'my-dir', + const userInput = convertConfigToUserInput({ + watch: { + include: 'my-dir', + }, }); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY, + include: userInput.watch?.include, }); expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir']); }); test("allows providing an array of strings in 'watch.include'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - include: ['my-dir1', '**/my-dir2/*'], - }); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY, + include: ['my-dir1', '**/my-dir2/*'], }); expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir1', '**/my-dir2/*']); }); test('ignores the output dir, dot files, dot directories, and node_modules by default', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - cloudExecutable.configuration.settings.set(['output'], 'cdk.out'); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY, + include: [], + output: 'cdk.out', }); expect(fakeChokidarWatch.excludeArgs).toStrictEqual(['cdk.out/**', '**/.*', '**/.*/**', '**/node_modules/**']); }); test("allows providing a single string in 'watch.exclude'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - exclude: 'my-dir', + const userInput = convertConfigToUserInput({ + watch: { + exclude: 'my-dir', + }, }); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY, + exclude: userInput.watch?.exclude, }); const excludeArgs = fakeChokidarWatch.excludeArgs; @@ -1041,14 +1046,12 @@ describe('watch', () => { }); test("allows providing an array of strings in 'watch.exclude'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - exclude: ['my-dir1', '**/my-dir2'], - }); const toolkit = defaultToolkitSetup(); await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY, + exclude: ['my-dir1', '**/my-dir2'], }); const excludeArgs = fakeChokidarWatch.excludeArgs; @@ -1058,7 +1061,6 @@ describe('watch', () => { }); test('allows watching with deploy concurrency', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; @@ -1067,6 +1069,7 @@ describe('watch', () => { selector: { patterns: [] }, concurrency: 3, hotswap: HotswapMode.HOTSWAP_ONLY, + include: [], }); fakeChokidarWatcherOn.readyCallback(); @@ -1075,7 +1078,6 @@ describe('watch', () => { describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { test('passes through the correct hotswap mode to deployStack()', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; @@ -1083,6 +1085,7 @@ describe('watch', () => { await toolkit.watch({ selector: { patterns: [] }, hotswap: hotswapMode, + include: [], }); fakeChokidarWatcherOn.readyCallback(); @@ -1091,7 +1094,6 @@ describe('watch', () => { }); test('respects HotswapMode.HOTSWAP_ONLY', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; @@ -1099,6 +1101,7 @@ describe('watch', () => { await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY, + include: [], }); fakeChokidarWatcherOn.readyCallback(); @@ -1106,7 +1109,6 @@ describe('watch', () => { }); test('respects HotswapMode.FALL_BACK', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; @@ -1114,6 +1116,7 @@ describe('watch', () => { await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FALL_BACK, + include: [], }); fakeChokidarWatcherOn.readyCallback(); @@ -1121,7 +1124,6 @@ describe('watch', () => { }); test('respects HotswapMode.FULL_DEPLOYMENT', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; @@ -1129,6 +1131,7 @@ describe('watch', () => { await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FULL_DEPLOYMENT, + include: [], }); fakeChokidarWatcherOn.readyCallback(); @@ -1140,13 +1143,13 @@ describe('watch', () => { let cdkDeployMock: jest.Mock; beforeEach(async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); toolkit = defaultToolkitSetup(); cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY, + include: [], }); }); diff --git a/tools/@aws-cdk/user-input-gen/lib/convert-to-user-input-gen.ts b/tools/@aws-cdk/user-input-gen/lib/convert-to-user-input-gen.ts index c7dc010328864..969a461808b52 100644 --- a/tools/@aws-cdk/user-input-gen/lib/convert-to-user-input-gen.ts +++ b/tools/@aws-cdk/user-input-gen/lib/convert-to-user-input-gen.ts @@ -123,8 +123,18 @@ function buildCommandOptions(options: CliAction, argName: string, prefix?: strin const commandOptions: string[] = []; for (const optionName of Object.keys(options.options ?? {})) { const name = kebabToCamelCase(optionName); + if (prefix) { - commandOptions.push(`'${name}': ${argName}.${prefix}?.${name},`); + const rhs = `${argName}.${prefix}?.${name}`; + + // For legacy reasons, `watch.include` and `watch.exclude` are special cases where strings + // are allowed in place of string arrays. We therefore need to do the conversion only on + // the config function to be backwards compatible. + if (argName == CONFIG_ARG_NAME && (name == 'include' || name == 'exclude')) { + commandOptions.push(`'${name}': Array.isArray(${rhs}) ? ${rhs} : [${rhs}],`); + } else { + commandOptions.push(`'${name}': ${rhs},`); + } } else { commandOptions.push(`'${name}': ${argName}.${name},`); }