Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@aws-cdk/toolkit/lib/actions/watch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface WatchOptions extends BaseDeployOptions {
* The output directory to write CloudFormation template to
*
* @deprecated this should be grabbed from the cloud assembly itself
*
* @default 'cdk.out'
*/
readonly outdir?: string;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,19 +531,20 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
rootDir,
returnRootDirIfEmpty: true,
});
await ioHost.notify(debug(`'include' patterns for 'watch': ${watchIncludes}`));
await ioHost.notify(debug(`'include' patterns for 'watch': ${JSON.stringify(watchIncludes)}`));

// For the "exclude" subkey under the "watch" key,
// the behavior is to add some default excludes in addition to the ones specified by the user:
// 1. The CDK output directory.
// 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 outdir = options.outdir ?? 'cdk.out';
const watchExcludes = patternsArrayForWatch(options.exclude, {
rootDir,
returnRootDirIfEmpty: false,
}).concat(`${options.outdir}/**`, '**/.*', '**/.*/**', '**/node_modules/**');
await ioHost.notify(debug(`'exclude' patterns for 'watch': ${watchExcludes}`));
}).concat(`${outdir}/**`, '**/.*', '**/.*/**', '**/node_modules/**');
await ioHost.notify(debug(`'exclude' patterns for 'watch': ${JSON.stringify(watchExcludes)}`));

// Since 'cdk deploy' is a relatively slow operation for a 'watch' process,
// introduce a concurrency latch that tracks the state.
Expand Down
3 changes: 0 additions & 3 deletions packages/@aws-cdk/toolkit/test/actions/destroy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ jest.mock('../../lib/api/aws-cdk', () => {
...jest.requireActual('../../lib/api/aws-cdk'),
Deployments: jest.fn().mockImplementation(() => ({
destroyStack: mockDestroyStack,
// resolveEnvironment: jest.fn().mockResolvedValue({}),
// isSingleAssetPublished: jest.fn().mockResolvedValue(true),
// readCurrentTemplate: jest.fn().mockResolvedValue({ Resources: {} }),
})),
};
});
Expand Down
171 changes: 171 additions & 0 deletions packages/@aws-cdk/toolkit/test/actions/watch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// We need to mock the chokidar library, used by 'cdk watch'
// This needs to happen ABOVE the import statements due to quirks with how jest works
// Apparently, they hoist jest.mock commands just below the import statements so we
// need to make sure that the constants they access are initialized before the imports.
const mockChokidarWatcherOn = jest.fn();
const fakeChokidarWatcher = {
on: mockChokidarWatcherOn,
};
const fakeChokidarWatcherOn = {
get readyCallback(): () => void {
expect(mockChokidarWatcherOn.mock.calls.length).toBeGreaterThanOrEqual(1);
// The call to the first 'watcher.on()' in the production code is the one we actually want here.
// This is a pretty fragile, but at least with this helper class,
// we would have to change it only in one place if it ever breaks
const firstCall = mockChokidarWatcherOn.mock.calls[0];
// let's make sure the first argument is the 'ready' event,
// just to be double safe
expect(firstCall[0]).toBe('ready');
// the second argument is the callback
return firstCall[1];
},

get fileEventCallback(): (
event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir',
path: string,
) => Promise<void> {
expect(mockChokidarWatcherOn.mock.calls.length).toBeGreaterThanOrEqual(2);
const secondCall = mockChokidarWatcherOn.mock.calls[1];
// let's make sure the first argument is not the 'ready' event,
// just to be double safe
expect(secondCall[0]).not.toBe('ready');
// the second argument is the callback
return secondCall[1];
},
};

const mockChokidarWatch = jest.fn();
jest.mock('chokidar', () => ({
watch: mockChokidarWatch,
}));

import { HotswapMode } from '../../lib';
import { Toolkit } from '../../lib/toolkit';
import { builderFixture, TestIoHost } from '../_helpers';

const ioHost = new TestIoHost();
const toolkit = new Toolkit({ ioHost });
jest.spyOn(toolkit, 'rollback').mockResolvedValue();

let mockDeployStack = jest.fn().mockResolvedValue({
type: 'did-deploy-stack',
stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack',
outputs: {},
noOp: false,
});

jest.mock('../../lib/api/aws-cdk', () => {
return {
...jest.requireActual('../../lib/api/aws-cdk'),
Deployments: jest.fn().mockImplementation(() => ({
deployStack: mockDeployStack,
resolveEnvironment: jest.fn().mockResolvedValue({}),
isSingleAssetPublished: jest.fn().mockResolvedValue(true),
readCurrentTemplate: jest.fn().mockResolvedValue({ Resources: {} }),
})),
};
});

beforeEach(() => {
ioHost.notifySpy.mockClear();
ioHost.requestSpy.mockClear();
jest.clearAllMocks();

mockChokidarWatch.mockReturnValue(fakeChokidarWatcher);
// on() in chokidar's Watcher returns 'this'
mockChokidarWatcherOn.mockReturnValue(fakeChokidarWatcher);
});

describe('watch', () => {
test('no include & no exclude results in error', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
await expect(async () => toolkit.watch(cx, {})).rejects.toThrow(/Cannot use the 'watch' command without specifying at least one directory to monitor. Make sure to add a \"watch\" key to your cdk.json/);
});

test('observes cwd as default rootdir', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
ioHost.level = 'debug';
await toolkit.watch(cx, {
include: [],
});

// THEN
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
action: 'watch',
level: 'debug',
message: expect.stringContaining(`root directory used for 'watch' is: ${process.cwd()}`),
}));
});

test('ignores output dir, dot files, dot directories, node_modules by default', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
ioHost.level = 'debug';
await toolkit.watch(cx, {
exclude: [],
});

// THEN
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
action: 'watch',
level: 'debug',
message: expect.stringContaining(`'exclude' patterns for 'watch': ["cdk.out/**","**/.*","**/.*/**","**/node_modules/**"]`),
}));
});

test('can include specific files', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
ioHost.level = 'debug';
await toolkit.watch(cx, {
include: ['index.ts'],
});

// THEN
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
action: 'watch',
level: 'debug',
message: expect.stringContaining(`'include' patterns for 'watch': ["index.ts"]`),
}));
});

test('can exclude specific files', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
ioHost.level = 'debug';
await toolkit.watch(cx, {
exclude: ['index.ts'],
});

// THEN
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
action: 'watch',
level: 'debug',
message: expect.stringContaining(`'exclude' patterns for 'watch': ["index.ts"`),
}));
});

describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => {
test('passes through the correct hotswap mode to deployStack()', async () => {
// WHEN
const cx = await builderFixture(toolkit, 'stack-with-role');
ioHost.level = 'warn';
await toolkit.watch(cx, {
include: [],
hotswap: hotswapMode,
});

fakeChokidarWatcherOn.readyCallback();
await new Promise(resolve => setTimeout(resolve, 3000));

// THEN
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
action: 'deploy',
level: 'warn',
message: expect.stringContaining('The --hotswap and --hotswap-fallback flags deliberately'),
}));
});
});
});
Loading