diff --git a/packages/@aws-cdk/toolkit/lib/api/io/private/logger.ts b/packages/@aws-cdk/toolkit/lib/api/io/private/logger.ts index 2df2b444cd0ee..caaaceef7a619 100644 --- a/packages/@aws-cdk/toolkit/lib/api/io/private/logger.ts +++ b/packages/@aws-cdk/toolkit/lib/api/io/private/logger.ts @@ -79,6 +79,28 @@ function stripEmojis(msg: string): string { return msg.replace(/\p{Emoji_Presentation}/gu, ''); } +/** + * An IoHost wrapper that trims whitespace at the beginning and end of messages. + * This is required, since after removing emojis and ANSI colors, + * we might end up with floating whitespace at either end. + */ +export function withTrimmedWhitespace(ioHost: IIoHost): IIoHost { + return { + notify: async (msg: IoMessage) => { + await ioHost.notify({ + ...msg, + message: msg.message.trim(), + }); + }, + requestResponse: async (msg: IoRequest) => { + return ioHost.requestResponse({ + ...msg, + message: msg.message.trim(), + }); + }, + }; +} + // @todo these cannot be awaited WTF export function asSdkLogger(ioHost: IIoHost, action: ToolkitAction): Logger { return new class implements Logger { diff --git a/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts index 42c8f1335a479..e8294ed22a9d6 100644 --- a/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts @@ -19,7 +19,7 @@ import { CachedCloudAssemblySource, IdentityCloudAssemblySource, StackAssembly, import { ALL_STACKS, CloudAssemblySourceBuilder } from '../api/cloud-assembly/private'; import { ToolkitError } from '../api/errors'; import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; -import { asSdkLogger, withAction, Timer, confirm, error, highlight, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor } from '../api/io/private'; +import { asSdkLogger, withAction, Timer, confirm, error, highlight, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace } from '../api/io/private'; /** * The current action being performed by the CLI. 'none' represents the absence of an action. @@ -115,7 +115,9 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab if (props.color === false) { ioHost = withoutColor(ioHost); } - this.ioHost = ioHost; + // After removing emojis and color, we might end up with floating whitespace at either end of the message + // This also removes newlines that we currently emit for CLI backwards compatibility. + this.ioHost = withTrimmedWhitespace(ioHost); } public async dispose(): Promise { diff --git a/packages/@aws-cdk/toolkit/test/toolkit/toolkit.test.ts b/packages/@aws-cdk/toolkit/test/toolkit/toolkit.test.ts index 4680f75873a59..156b428b844ea 100644 --- a/packages/@aws-cdk/toolkit/test/toolkit/toolkit.test.ts +++ b/packages/@aws-cdk/toolkit/test/toolkit/toolkit.test.ts @@ -9,42 +9,64 @@ import * as chalk from 'chalk'; import { Toolkit } from '../../lib'; import { TestIoHost } from '../_helpers'; -test('emojis can be stripped from message', async () => { - const ioHost = new TestIoHost(); - const toolkit = new Toolkit({ ioHost, emojis: false }); - - await toolkit.ioHost.notify({ - message: '💯Smile123😀', - action: 'deploy', - level: 'info', - code: 'CDK_TOOLKIT_I0000', - time: new Date(), +describe('message formatting', () => { + test('emojis can be stripped from message', async () => { + const ioHost = new TestIoHost(); + const toolkit = new Toolkit({ ioHost, emojis: false }); + + await toolkit.ioHost.notify({ + message: '💯Smile123😀', + action: 'deploy', + level: 'info', + code: 'CDK_TOOLKIT_I0000', + time: new Date(), + }); + + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'deploy', + level: 'info', + code: 'CDK_TOOLKIT_I0000', + message: 'Smile123', + })); }); - expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - action: 'deploy', - level: 'info', - code: 'CDK_TOOLKIT_I0000', - message: 'Smile123', - })); -}); + test('color can be stripped from message', async () => { + const ioHost = new TestIoHost(); + const toolkit = new Toolkit({ ioHost, color: false }); -test('color can be stripped from message', async () => { - const ioHost = new TestIoHost(); - const toolkit = new Toolkit({ ioHost, color: false }); + await toolkit.ioHost.notify({ + message: chalk.red('RED') + chalk.bold('BOLD') + chalk.blue('BLUE'), + action: 'deploy', + level: 'info', + code: 'CDK_TOOLKIT_I0000', + time: new Date(), + }); - await toolkit.ioHost.notify({ - message: chalk.red('RED') + chalk.bold('BOLD') + chalk.blue('BLUE'), - action: 'deploy', - level: 'info', - code: 'CDK_TOOLKIT_I0000', - time: new Date(), + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'deploy', + level: 'info', + code: 'CDK_TOOLKIT_I0000', + message: 'REDBOLDBLUE', + })); }); - expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - action: 'deploy', - level: 'info', - code: 'CDK_TOOLKIT_I0000', - message: 'REDBOLDBLUE', - })); + test('whitespace is always trimmed from a message', async () => { + const ioHost = new TestIoHost(); + const toolkit = new Toolkit({ ioHost, color: false }); + + await toolkit.ioHost.notify({ + message: ' test message\n\n', + action: 'deploy', + level: 'info', + code: 'CDK_TOOLKIT_I0000', + time: new Date(), + }); + + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'deploy', + level: 'info', + code: 'CDK_TOOLKIT_I0000', + message: 'test message', + })); + }); });