Skip to content

Commit

Permalink
chore: remove Logger class & refactor Printer (#903)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bombguy committed Jan 22, 2024
1 parent a91b5ad commit fb07baf
Show file tree
Hide file tree
Showing 46 changed files with 639 additions and 614 deletions.
8 changes: 8 additions & 0 deletions .changeset/polite-meals-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'create-amplify': minor
'@aws-amplify/cli-core': minor
'@aws-amplify/sandbox': minor
'@aws-amplify/backend-cli': minor
---

Refactor Printer class & deprecate Logger
242 changes: 121 additions & 121 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 18 additions & 4 deletions packages/cli-core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
```ts

/// <reference types="node" />

// @public
export class AmplifyPrompter {
static input: (options: {
Expand All @@ -23,12 +25,24 @@ export enum COLOR {
RED = "31m"
}

// @public (undocumented)
export enum LogLevel {
// (undocumented)
DEBUG = 2,
// (undocumented)
ERROR = 0,
// (undocumented)
INFO = 1
}

// @public
export class Printer {
static print: (message: string, colorName?: COLOR) => void;
static printNewLine: () => void;
static printRecord: <T extends Record<string | number, RecordValue>>(object: T) => void;
static printRecords: <T extends Record<string | number, RecordValue>>(objects: T[]) => void;
constructor(minimumLogLevel: LogLevel, stdout?: NodeJS.WriteStream, stderr?: NodeJS.WriteStream, refreshRate?: number);
indicateProgress(message: string, callback: () => Promise<void>): Promise<void>;
log(message: string, level?: LogLevel, eol?: boolean): void;
print: (message: string, colorName?: COLOR) => void;
printNewLine: () => void;
printRecords: <T extends Record<string | number, RecordValue>>(...objects: T[]) => void;
}

// @public (undocumented)
Expand Down
5 changes: 0 additions & 5 deletions packages/cli-core/src/printer/.eslintrc.json

This file was deleted.

96 changes: 96 additions & 0 deletions packages/cli-core/src/printer/printer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { after, before, beforeEach, describe, it, mock } from 'node:test';
import assert from 'assert';
import { LogLevel, Printer } from './printer.js';

void describe('Printer', () => {
const mockedWrite = mock.method(process.stdout, 'write');
let originalWrite: typeof process.stdout.write;

before(() => {
originalWrite = process.stdout.write;
process.stdout.write = mockedWrite;
});

after(() => {
// restore write function after all tests.
process.stdout.write = originalWrite;
});

beforeEach(() => {
mockedWrite.mock.resetCalls();
});

void it('log should print message followed by new line', () => {
new Printer(LogLevel.INFO).log('hello world');
assert.strictEqual(mockedWrite.mock.callCount(), 2);
assert.match(
mockedWrite.mock.calls[0].arguments[0].toString(),
/hello world/
);
assert.match(mockedWrite.mock.calls[1].arguments[0].toString(), /\n/);
});

void it('log should print message without new line', () => {
new Printer(LogLevel.INFO).log('hello world', LogLevel.INFO, false);
assert.strictEqual(mockedWrite.mock.callCount(), 1);
assert.match(
mockedWrite.mock.calls[0].arguments[0].toString(),
/hello world/
);
});

void it('log should not print debug logs by default', () => {
new Printer(LogLevel.INFO).log('hello world', LogLevel.DEBUG);
assert.strictEqual(mockedWrite.mock.callCount(), 0);
});

void it('log should print debug logs when printer is configured with minimum log level >= DEBUG', () => {
new Printer(LogLevel.DEBUG).log('hello world', LogLevel.DEBUG);
assert.strictEqual(mockedWrite.mock.callCount(), 2);
assert.match(
mockedWrite.mock.calls[0].arguments[0].toString(),
/hello world/
);
assert.match(mockedWrite.mock.calls[1].arguments[0].toString(), /\n/);
});

void it('log should not print debug logs by default', () => {
new Printer(LogLevel.INFO).log('hello world', LogLevel.DEBUG);
assert.strictEqual(mockedWrite.mock.callCount(), 0);
});

void it('indicateProgress logs message & animates ellipsis if on TTY', async () => {
process.stdout.isTTY = true;
await new Printer(LogLevel.INFO).indicateProgress(
'loading a long list',
() => new Promise((resolve) => setTimeout(resolve, 3000))
);
// filter out the escape characters.
const logMessages = mockedWrite.mock.calls
.filter((message) =>
message.arguments.toString().match(/loading a long list/)
)
.map((call) => call.arguments.toString());

logMessages.forEach((message) => {
assert.match(message, /loading a long list(.*)/);
});
});

void it('indicateProgress does not animates ellipsis if not TTY & prints log message once', async () => {
process.stdout.isTTY = false;
await new Printer(LogLevel.INFO).indicateProgress(
'loading a long list',
() => new Promise((resolve) => setTimeout(resolve, 1500))
);
// filter out the escape characters.
const logMessages = mockedWrite.mock.calls
.filter((message) =>
message.arguments.toString().match(/loading a long list/)
)
.map((call) => call.arguments.toString());

assert.strictEqual(logMessages.length, 1);
assert.match(logMessages[0], /loading a long list/);
});
});
170 changes: 148 additions & 22 deletions packages/cli-core/src/printer/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,175 @@ import { EOL } from 'os';
export type RecordValue = string | number | string[] | Date;

/**
* The class that pretty prints to the console.
* The class that pretty prints to the output stream.
*/
export class Printer {
// Properties for ellipsis animation
private timer: ReturnType<typeof setTimeout>;
private timerSet: boolean;

/**
* Print an object/record to console.
* Sets default configs
*/
static printRecord = <T extends Record<string | number, RecordValue>>(
object: T
): void => {
let message = '';
const entries = Object.entries(object);
entries.forEach(([key, val]) => {
message = message.concat(` ${key}: ${val as string}${EOL}`);
});
console.log(message);
};
constructor(
private readonly minimumLogLevel: LogLevel,
private readonly stdout: NodeJS.WriteStream = process.stdout,
private readonly stderr: NodeJS.WriteStream = process.stderr,
private readonly refreshRate: number = 500
) {}

/**
* Prints an array of objects/records to console.
* Prints an array of objects/records to output stream.
*/
static printRecords = <T extends Record<string | number, RecordValue>>(
objects: T[]
printRecords = <T extends Record<string | number, RecordValue>>(
...objects: T[]
): void => {
for (const obj of objects) {
this.printRecord(obj);
}
};

/**
* Prints a given message (with optional color) to console.
* Prints a given message (with optional color) to output stream.
*/
static print = (message: string, colorName?: COLOR) => {
print = (message: string, colorName?: COLOR) => {
if (colorName) {
console.log(color(colorName, message));
this.stdout.write(color(colorName, message));
} else {
console.log(message);
this.stdout.write(message);
}
};

/**
* Prints a new line to console
* Logs a message with animated ellipsis
*/
static printNewLine = () => {
console.log(EOL);
async indicateProgress(message: string, callback: () => Promise<void>) {
try {
this.startAnimatingEllipsis(message);
await callback();
} finally {
this.stopAnimatingEllipsis(message);
}
}

/**
* Prints a new line to output stream
*/
printNewLine = () => {
this.stdout.write(EOL);
};

/**
* Logs a message to the output stream.
*/
log(message: string, level: LogLevel = LogLevel.INFO, eol = true) {
const doLogMessage = level <= this.minimumLogLevel;

if (!doLogMessage) {
return;
}

const logMessage =
this.minimumLogLevel === LogLevel.DEBUG
? `[${LogLevel[level]}] ${new Date().toISOString()}: ${message}`
: message;

if (level === LogLevel.ERROR) {
this.stderr.write(logMessage);
} else {
this.stdout.write(logMessage);
}

if (eol) {
this.printNewLine();
}
}

/**
* Print an object/record to output stream.
*/
private printRecord = <T extends Record<string | number, RecordValue>>(
object: T
): void => {
let message = '';
const entries = Object.entries(object);
entries.forEach(([key, val]) => {
message = message.concat(` ${key}: ${val as string}${EOL}`);
});
this.stdout.write(message);
};

/**
* Start animating ellipsis at the end of a log message.
*/
private startAnimatingEllipsis(message: string) {
if (!this.isTTY()) {
this.log(message, LogLevel.INFO);
return;
}

if (this.timerSet) {
throw new Error(
'Timer is already set to animate ellipsis, stop the current running timer before starting a new one.'
);
}

const frameLength = 4; // number of desired dots - 1
let frameCount = 0;
this.timerSet = true;
this.writeEscapeSequence(EscapeSequence.HIDE_CURSOR);
this.stdout.write(message);
this.timer = setInterval(() => {
this.writeEscapeSequence(EscapeSequence.CLEAR_LINE);
this.writeEscapeSequence(EscapeSequence.MOVE_CURSOR_TO_START);
this.stdout.write(message + '.'.repeat(++frameCount % frameLength));
}, this.refreshRate);
}

/**
* Stops animating ellipsis and replace with a log message.
*/
private stopAnimatingEllipsis(message: string) {
if (!this.isTTY()) {
return;
}

clearInterval(this.timer);
this.timerSet = false;
this.writeEscapeSequence(EscapeSequence.CLEAR_LINE);
this.writeEscapeSequence(EscapeSequence.MOVE_CURSOR_TO_START);
this.writeEscapeSequence(EscapeSequence.SHOW_CURSOR);
this.stdout.write(`${message}...${EOL}`);
}

/**
* Writes escape sequence to stdout
*/
private writeEscapeSequence(action: EscapeSequence) {
if (!this.isTTY()) {
return;
}

this.stdout.write(action);
}

/**
* Checks if the environment is TTY
*/
private isTTY() {
return this.stdout.isTTY;
}
}

export enum LogLevel {
ERROR = 0,
INFO = 1,
DEBUG = 2,
}

enum EscapeSequence {
CLEAR_LINE = '\x1b[2K',
MOVE_CURSOR_TO_START = '\x1b[0G',
SHOW_CURSOR = '\x1b[?25h',
HIDE_CURSOR = '\x1b[?25l',
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import yargs, { CommandModule } from 'yargs';
import { TestCommandRunner } from '../../test-utils/command_runner.js';
import assert from 'node:assert';
import { ConfigureProfileCommand } from './configure_profile_command.js';
import { AmplifyPrompter, Printer } from '@aws-amplify/cli-core';
import { AmplifyPrompter } from '@aws-amplify/cli-core';
import { Open } from '../open/open.js';
import { ProfileController } from './profile_controller.js';
import { printer } from '../../printer.js';

const testAccessKeyId = 'testAccessKeyId';
const testSecretAccessKey = 'testSecretAccessKey';
Expand Down Expand Up @@ -35,7 +36,7 @@ void describe('configure command', () => {
'profileExists',
() => Promise.resolve(true)
);
const mockPrint = contextual.mock.method(Printer, 'print');
const mockPrint = contextual.mock.method(printer, 'print');

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

Expand Down
Loading

0 comments on commit fb07baf

Please sign in to comment.