Skip to content

Commit fb07baf

Browse files
authored
chore: remove Logger class & refactor Printer (#903)
* chore: remove Logger class & refactor Printer * fix: update API.md * fix: update API.md * fix: update API.md * fix: make constructor private * fix: remove yargs & use process.argv * bump package-lock * fix: update Printer to be singleton instead of static class * update changeset * update API.md * remove unused imports * remove unused imports * fix * removing no-console linter rule override * update API.md * fix test * .eslintrc * update integ test * revert back to stderr * error logs should write to stderr * update API.md * remove printRecord * remove async * spread printRecords, updating to singleton
1 parent a91b5ad commit fb07baf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+639
-614
lines changed

.changeset/polite-meals-search.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'create-amplify': minor
3+
'@aws-amplify/cli-core': minor
4+
'@aws-amplify/sandbox': minor
5+
'@aws-amplify/backend-cli': minor
6+
---
7+
8+
Refactor Printer class & deprecate Logger

package-lock.json

Lines changed: 121 additions & 121 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli-core/API.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
```ts
66

7+
/// <reference types="node" />
8+
79
// @public
810
export class AmplifyPrompter {
911
static input: (options: {
@@ -23,12 +25,24 @@ export enum COLOR {
2325
RED = "31m"
2426
}
2527

28+
// @public (undocumented)
29+
export enum LogLevel {
30+
// (undocumented)
31+
DEBUG = 2,
32+
// (undocumented)
33+
ERROR = 0,
34+
// (undocumented)
35+
INFO = 1
36+
}
37+
2638
// @public
2739
export class Printer {
28-
static print: (message: string, colorName?: COLOR) => void;
29-
static printNewLine: () => void;
30-
static printRecord: <T extends Record<string | number, RecordValue>>(object: T) => void;
31-
static printRecords: <T extends Record<string | number, RecordValue>>(objects: T[]) => void;
40+
constructor(minimumLogLevel: LogLevel, stdout?: NodeJS.WriteStream, stderr?: NodeJS.WriteStream, refreshRate?: number);
41+
indicateProgress(message: string, callback: () => Promise<void>): Promise<void>;
42+
log(message: string, level?: LogLevel, eol?: boolean): void;
43+
print: (message: string, colorName?: COLOR) => void;
44+
printNewLine: () => void;
45+
printRecords: <T extends Record<string | number, RecordValue>>(...objects: T[]) => void;
3246
}
3347

3448
// @public (undocumented)

packages/cli-core/src/printer/.eslintrc.json

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { after, before, beforeEach, describe, it, mock } from 'node:test';
2+
import assert from 'assert';
3+
import { LogLevel, Printer } from './printer.js';
4+
5+
void describe('Printer', () => {
6+
const mockedWrite = mock.method(process.stdout, 'write');
7+
let originalWrite: typeof process.stdout.write;
8+
9+
before(() => {
10+
originalWrite = process.stdout.write;
11+
process.stdout.write = mockedWrite;
12+
});
13+
14+
after(() => {
15+
// restore write function after all tests.
16+
process.stdout.write = originalWrite;
17+
});
18+
19+
beforeEach(() => {
20+
mockedWrite.mock.resetCalls();
21+
});
22+
23+
void it('log should print message followed by new line', () => {
24+
new Printer(LogLevel.INFO).log('hello world');
25+
assert.strictEqual(mockedWrite.mock.callCount(), 2);
26+
assert.match(
27+
mockedWrite.mock.calls[0].arguments[0].toString(),
28+
/hello world/
29+
);
30+
assert.match(mockedWrite.mock.calls[1].arguments[0].toString(), /\n/);
31+
});
32+
33+
void it('log should print message without new line', () => {
34+
new Printer(LogLevel.INFO).log('hello world', LogLevel.INFO, false);
35+
assert.strictEqual(mockedWrite.mock.callCount(), 1);
36+
assert.match(
37+
mockedWrite.mock.calls[0].arguments[0].toString(),
38+
/hello world/
39+
);
40+
});
41+
42+
void it('log should not print debug logs by default', () => {
43+
new Printer(LogLevel.INFO).log('hello world', LogLevel.DEBUG);
44+
assert.strictEqual(mockedWrite.mock.callCount(), 0);
45+
});
46+
47+
void it('log should print debug logs when printer is configured with minimum log level >= DEBUG', () => {
48+
new Printer(LogLevel.DEBUG).log('hello world', LogLevel.DEBUG);
49+
assert.strictEqual(mockedWrite.mock.callCount(), 2);
50+
assert.match(
51+
mockedWrite.mock.calls[0].arguments[0].toString(),
52+
/hello world/
53+
);
54+
assert.match(mockedWrite.mock.calls[1].arguments[0].toString(), /\n/);
55+
});
56+
57+
void it('log should not print debug logs by default', () => {
58+
new Printer(LogLevel.INFO).log('hello world', LogLevel.DEBUG);
59+
assert.strictEqual(mockedWrite.mock.callCount(), 0);
60+
});
61+
62+
void it('indicateProgress logs message & animates ellipsis if on TTY', async () => {
63+
process.stdout.isTTY = true;
64+
await new Printer(LogLevel.INFO).indicateProgress(
65+
'loading a long list',
66+
() => new Promise((resolve) => setTimeout(resolve, 3000))
67+
);
68+
// filter out the escape characters.
69+
const logMessages = mockedWrite.mock.calls
70+
.filter((message) =>
71+
message.arguments.toString().match(/loading a long list/)
72+
)
73+
.map((call) => call.arguments.toString());
74+
75+
logMessages.forEach((message) => {
76+
assert.match(message, /loading a long list(.*)/);
77+
});
78+
});
79+
80+
void it('indicateProgress does not animates ellipsis if not TTY & prints log message once', async () => {
81+
process.stdout.isTTY = false;
82+
await new Printer(LogLevel.INFO).indicateProgress(
83+
'loading a long list',
84+
() => new Promise((resolve) => setTimeout(resolve, 1500))
85+
);
86+
// filter out the escape characters.
87+
const logMessages = mockedWrite.mock.calls
88+
.filter((message) =>
89+
message.arguments.toString().match(/loading a long list/)
90+
)
91+
.map((call) => call.arguments.toString());
92+
93+
assert.strictEqual(logMessages.length, 1);
94+
assert.match(logMessages[0], /loading a long list/);
95+
});
96+
});

packages/cli-core/src/printer/printer.ts

Lines changed: 148 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,175 @@ import { EOL } from 'os';
44
export type RecordValue = string | number | string[] | Date;
55

66
/**
7-
* The class that pretty prints to the console.
7+
* The class that pretty prints to the output stream.
88
*/
99
export class Printer {
10+
// Properties for ellipsis animation
11+
private timer: ReturnType<typeof setTimeout>;
12+
private timerSet: boolean;
13+
1014
/**
11-
* Print an object/record to console.
15+
* Sets default configs
1216
*/
13-
static printRecord = <T extends Record<string | number, RecordValue>>(
14-
object: T
15-
): void => {
16-
let message = '';
17-
const entries = Object.entries(object);
18-
entries.forEach(([key, val]) => {
19-
message = message.concat(` ${key}: ${val as string}${EOL}`);
20-
});
21-
console.log(message);
22-
};
17+
constructor(
18+
private readonly minimumLogLevel: LogLevel,
19+
private readonly stdout: NodeJS.WriteStream = process.stdout,
20+
private readonly stderr: NodeJS.WriteStream = process.stderr,
21+
private readonly refreshRate: number = 500
22+
) {}
2323

2424
/**
25-
* Prints an array of objects/records to console.
25+
* Prints an array of objects/records to output stream.
2626
*/
27-
static printRecords = <T extends Record<string | number, RecordValue>>(
28-
objects: T[]
27+
printRecords = <T extends Record<string | number, RecordValue>>(
28+
...objects: T[]
2929
): void => {
3030
for (const obj of objects) {
3131
this.printRecord(obj);
3232
}
3333
};
3434

3535
/**
36-
* Prints a given message (with optional color) to console.
36+
* Prints a given message (with optional color) to output stream.
3737
*/
38-
static print = (message: string, colorName?: COLOR) => {
38+
print = (message: string, colorName?: COLOR) => {
3939
if (colorName) {
40-
console.log(color(colorName, message));
40+
this.stdout.write(color(colorName, message));
4141
} else {
42-
console.log(message);
42+
this.stdout.write(message);
4343
}
4444
};
4545

4646
/**
47-
* Prints a new line to console
47+
* Logs a message with animated ellipsis
4848
*/
49-
static printNewLine = () => {
50-
console.log(EOL);
49+
async indicateProgress(message: string, callback: () => Promise<void>) {
50+
try {
51+
this.startAnimatingEllipsis(message);
52+
await callback();
53+
} finally {
54+
this.stopAnimatingEllipsis(message);
55+
}
56+
}
57+
58+
/**
59+
* Prints a new line to output stream
60+
*/
61+
printNewLine = () => {
62+
this.stdout.write(EOL);
5163
};
64+
65+
/**
66+
* Logs a message to the output stream.
67+
*/
68+
log(message: string, level: LogLevel = LogLevel.INFO, eol = true) {
69+
const doLogMessage = level <= this.minimumLogLevel;
70+
71+
if (!doLogMessage) {
72+
return;
73+
}
74+
75+
const logMessage =
76+
this.minimumLogLevel === LogLevel.DEBUG
77+
? `[${LogLevel[level]}] ${new Date().toISOString()}: ${message}`
78+
: message;
79+
80+
if (level === LogLevel.ERROR) {
81+
this.stderr.write(logMessage);
82+
} else {
83+
this.stdout.write(logMessage);
84+
}
85+
86+
if (eol) {
87+
this.printNewLine();
88+
}
89+
}
90+
91+
/**
92+
* Print an object/record to output stream.
93+
*/
94+
private printRecord = <T extends Record<string | number, RecordValue>>(
95+
object: T
96+
): void => {
97+
let message = '';
98+
const entries = Object.entries(object);
99+
entries.forEach(([key, val]) => {
100+
message = message.concat(` ${key}: ${val as string}${EOL}`);
101+
});
102+
this.stdout.write(message);
103+
};
104+
105+
/**
106+
* Start animating ellipsis at the end of a log message.
107+
*/
108+
private startAnimatingEllipsis(message: string) {
109+
if (!this.isTTY()) {
110+
this.log(message, LogLevel.INFO);
111+
return;
112+
}
113+
114+
if (this.timerSet) {
115+
throw new Error(
116+
'Timer is already set to animate ellipsis, stop the current running timer before starting a new one.'
117+
);
118+
}
119+
120+
const frameLength = 4; // number of desired dots - 1
121+
let frameCount = 0;
122+
this.timerSet = true;
123+
this.writeEscapeSequence(EscapeSequence.HIDE_CURSOR);
124+
this.stdout.write(message);
125+
this.timer = setInterval(() => {
126+
this.writeEscapeSequence(EscapeSequence.CLEAR_LINE);
127+
this.writeEscapeSequence(EscapeSequence.MOVE_CURSOR_TO_START);
128+
this.stdout.write(message + '.'.repeat(++frameCount % frameLength));
129+
}, this.refreshRate);
130+
}
131+
132+
/**
133+
* Stops animating ellipsis and replace with a log message.
134+
*/
135+
private stopAnimatingEllipsis(message: string) {
136+
if (!this.isTTY()) {
137+
return;
138+
}
139+
140+
clearInterval(this.timer);
141+
this.timerSet = false;
142+
this.writeEscapeSequence(EscapeSequence.CLEAR_LINE);
143+
this.writeEscapeSequence(EscapeSequence.MOVE_CURSOR_TO_START);
144+
this.writeEscapeSequence(EscapeSequence.SHOW_CURSOR);
145+
this.stdout.write(`${message}...${EOL}`);
146+
}
147+
148+
/**
149+
* Writes escape sequence to stdout
150+
*/
151+
private writeEscapeSequence(action: EscapeSequence) {
152+
if (!this.isTTY()) {
153+
return;
154+
}
155+
156+
this.stdout.write(action);
157+
}
158+
159+
/**
160+
* Checks if the environment is TTY
161+
*/
162+
private isTTY() {
163+
return this.stdout.isTTY;
164+
}
165+
}
166+
167+
export enum LogLevel {
168+
ERROR = 0,
169+
INFO = 1,
170+
DEBUG = 2,
171+
}
172+
173+
enum EscapeSequence {
174+
CLEAR_LINE = '\x1b[2K',
175+
MOVE_CURSOR_TO_START = '\x1b[0G',
176+
SHOW_CURSOR = '\x1b[?25h',
177+
HIDE_CURSOR = '\x1b[?25l',
52178
}

packages/cli/src/commands/configure/configure_profile_command.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import yargs, { CommandModule } from 'yargs';
33
import { TestCommandRunner } from '../../test-utils/command_runner.js';
44
import assert from 'node:assert';
55
import { ConfigureProfileCommand } from './configure_profile_command.js';
6-
import { AmplifyPrompter, Printer } from '@aws-amplify/cli-core';
6+
import { AmplifyPrompter } from '@aws-amplify/cli-core';
77
import { Open } from '../open/open.js';
88
import { ProfileController } from './profile_controller.js';
9+
import { printer } from '../../printer.js';
910

1011
const testAccessKeyId = 'testAccessKeyId';
1112
const testSecretAccessKey = 'testSecretAccessKey';
@@ -35,7 +36,7 @@ void describe('configure command', () => {
3536
'profileExists',
3637
() => Promise.resolve(true)
3738
);
38-
const mockPrint = contextual.mock.method(Printer, 'print');
39+
const mockPrint = contextual.mock.method(printer, 'print');
3940

4041
await commandRunner.runCommand(`profile --name ${testProfile}`);
4142

0 commit comments

Comments
 (0)