@holz/ansi-terminal-backend 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# `@holz/ansi-terminal-backend`
|
|
2
|
+
|
|
3
|
+
Pretty-print logs to the terminal.
|
|
4
|
+
|
|
5
|
+
<img alt="Screenshot of logs printed to the terminal" src="https://user-images.githubusercontent.com/10053423/222926680-0a12da0c-5ff2-40a1-8759-5dca72eb89c3.png" width="600" />
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createAnsiTerminalBackend } from '@holz/ansi-terminal-backend';
|
|
11
|
+
|
|
12
|
+
const logger = createLogger(createAnsiTerminalBackend());
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> **Note**
|
|
16
|
+
> There is no way to disable colors. The output relies on colors to convey structure. To disable colors, use a different backend such as [`@holz/stream-backend`](https://github.com/PsychoLlama/holz/tree/main/packages/holz-stream-backend).
|
|
17
|
+
|
|
18
|
+
## Options
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
createAnsiTerminalBackend({
|
|
22
|
+
/**
|
|
23
|
+
* By default it prints to the global console, but you can override it.
|
|
24
|
+
* See: https://nodejs.org/api/console.html#new-consoleoptions
|
|
25
|
+
*/
|
|
26
|
+
console: new Console(custom.stdout, custom.stderr),
|
|
27
|
+
});
|
|
28
|
+
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("@holz/core");function o(e){return`\x1B[${e}`}const n=o("0m"),u=o("2m"),g=o("31m"),p=o("32m"),L=o("33m"),S=o("34m");function f(e={}){const r=e.console??console,i=" ".repeat(s.LogLevel.Error.length);return t=>{const a=v(new Date),m=`${n}${u}${a}${n}`,l=[{include:!0,command:"%s",content:m},{include:!0,command:"%s",content:b[t.level]},{include:!0,command:"%s",content:t.message.replace(/(\r?\n)/g,`$1${m} ${i} `)},{include:Object.keys(t.context).length>0,command:"%O",content:t.context},{include:t.origin.length>0,command:"%s",content:`${u}${t.origin.join(":")}${n}`}].filter(c=>c.include),d=l.map(c=>c.command).join(" "),$=l.map(c=>c.content);r.error(d,...$)}}function v(e){const r=e.getHours().toString().padStart(2,"0"),i=e.getMinutes().toString().padStart(2,"0"),t=e.getSeconds().toString().padStart(2,"0"),a=e.getMilliseconds().toString().padStart(3,"0");return`[${r}:${i}:${t}.${a}]`}const b={[s.LogLevel.Debug]:`${S}DEBUG${n}`,[s.LogLevel.Info]:`${p}INFO${n} `,[s.LogLevel.Warn]:`${L}WARN${n} `,[s.LogLevel.Error]:`${g}ERROR${n}`};exports.createAnsiTerminalBackend=f;
|
|
@@ -1,55 +1,54 @@
|
|
|
1
|
-
import { LogLevel as
|
|
2
|
-
function
|
|
3
|
-
return `\x1B[${
|
|
1
|
+
import { LogLevel as s } from "@holz/core";
|
|
2
|
+
function o(t) {
|
|
3
|
+
return `\x1B[${t}`;
|
|
4
4
|
}
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
processLog(t) {
|
|
11
|
-
const r = this.getTimestamp(new Date()), s = [
|
|
5
|
+
const e = o("0m"), u = o("2m"), g = o("31m"), p = o("32m"), S = o("33m"), f = o("34m");
|
|
6
|
+
function v(t = {}) {
|
|
7
|
+
const r = t.console ?? console, i = " ".repeat(s.Error.length);
|
|
8
|
+
return (n) => {
|
|
9
|
+
const m = b(new Date()), a = `${e}${u}${m}${e}`, l = [
|
|
12
10
|
{
|
|
13
11
|
include: !0,
|
|
14
12
|
command: "%s",
|
|
15
|
-
content:
|
|
13
|
+
content: a
|
|
16
14
|
},
|
|
17
15
|
{
|
|
18
16
|
include: !0,
|
|
19
17
|
command: "%s",
|
|
20
|
-
content:
|
|
18
|
+
content: h[n.level]
|
|
21
19
|
},
|
|
22
20
|
{
|
|
23
21
|
include: !0,
|
|
24
22
|
command: "%s",
|
|
25
|
-
content:
|
|
23
|
+
content: n.message.replace(
|
|
24
|
+
/(\r?\n)/g,
|
|
25
|
+
`$1${a} ${i} `
|
|
26
|
+
)
|
|
26
27
|
},
|
|
27
28
|
{
|
|
28
|
-
include:
|
|
29
|
-
command: "%
|
|
30
|
-
content:
|
|
29
|
+
include: Object.keys(n.context).length > 0,
|
|
30
|
+
command: "%O",
|
|
31
|
+
content: n.context
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
|
-
include:
|
|
34
|
-
command: "%
|
|
35
|
-
content:
|
|
34
|
+
include: n.origin.length > 0,
|
|
35
|
+
command: "%s",
|
|
36
|
+
content: `${u}${n.origin.join(":")}${e}`
|
|
36
37
|
}
|
|
37
|
-
].filter((
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
38
|
+
].filter((c) => c.include), $ = l.map((c) => c.command).join(" "), d = l.map((c) => c.content);
|
|
39
|
+
r.error($, ...d);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function b(t) {
|
|
43
|
+
const r = t.getHours().toString().padStart(2, "0"), i = t.getMinutes().toString().padStart(2, "0"), n = t.getSeconds().toString().padStart(2, "0"), m = t.getMilliseconds().toString().padStart(3, "0");
|
|
44
|
+
return `[${r}:${i}:${n}.${m}]`;
|
|
45
45
|
}
|
|
46
|
-
const
|
|
47
|
-
[
|
|
48
|
-
[
|
|
49
|
-
[
|
|
50
|
-
[
|
|
46
|
+
const h = {
|
|
47
|
+
[s.Debug]: `${f}DEBUG${e}`,
|
|
48
|
+
[s.Info]: `${p}INFO${e} `,
|
|
49
|
+
[s.Warn]: `${S}WARN${e} `,
|
|
50
|
+
[s.Error]: `${g}ERROR${e}`
|
|
51
51
|
};
|
|
52
52
|
export {
|
|
53
|
-
|
|
54
|
-
f as default
|
|
53
|
+
v as createAnsiTerminalBackend
|
|
55
54
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holz/ansi-terminal-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "An ANSI terminal backend for Holz",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/holz-ansi-terminal-backend.cjs",
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"test:types": "tsc"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@holz/core": "0.
|
|
41
|
+
"@holz/core": "^0.6.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@holz/core": "0.
|
|
44
|
+
"@holz/core": "^0.6.0",
|
|
45
45
|
"@types/node": "^18.14.0",
|
|
46
|
-
"@vitest/coverage-c8": "0.28.5",
|
|
47
|
-
"typescript": "4.9.5",
|
|
46
|
+
"@vitest/coverage-c8": "^0.28.5",
|
|
47
|
+
"typescript": "^4.9.5",
|
|
48
48
|
"vite": "^4.0.0",
|
|
49
49
|
"vitest": "^0.28.5"
|
|
50
50
|
}
|
|
@@ -1,24 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { Console } from 'node:console';
|
|
2
3
|
import { createLogger } from '@holz/core';
|
|
3
|
-
import
|
|
4
|
-
import AnsiTerminalBackend from '../ansi-terminal-backend';
|
|
4
|
+
import { createAnsiTerminalBackend } from '../ansi-terminal-backend';
|
|
5
5
|
|
|
6
6
|
const CURRENT_TIME = new Date('2020-06-15T03:05:07.010Z');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
describe('ANSI terminal backend', () => {
|
|
9
|
+
function createStream() {
|
|
10
|
+
let output = '';
|
|
11
|
+
const stream = new Writable({
|
|
12
|
+
write(chunk, _encoding, callback) {
|
|
13
|
+
output += String(chunk);
|
|
14
|
+
callback();
|
|
15
|
+
},
|
|
16
|
+
});
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
return {
|
|
19
|
+
getOutput: () => output,
|
|
20
|
+
stream,
|
|
21
|
+
};
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
// Named "terminal" to avoid conflict with `global.console`.
|
|
25
|
+
function createTerminal() {
|
|
26
|
+
const stdout = createStream();
|
|
27
|
+
const stderr = createStream();
|
|
28
|
+
const terminal = new Console({
|
|
29
|
+
stdout: stdout.stream,
|
|
30
|
+
stderr: stderr.stream,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
stdout,
|
|
35
|
+
stderr,
|
|
36
|
+
terminal,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
20
39
|
|
|
21
|
-
describe('ANSI terminal backend', () => {
|
|
22
40
|
beforeEach(() => {
|
|
23
41
|
vi.useFakeTimers({
|
|
24
42
|
now: CURRENT_TIME,
|
|
@@ -30,20 +48,18 @@ describe('ANSI terminal backend', () => {
|
|
|
30
48
|
});
|
|
31
49
|
|
|
32
50
|
it('prints the message to the terminal', () => {
|
|
33
|
-
const terminal =
|
|
34
|
-
const backend =
|
|
51
|
+
const { terminal, stderr } = createTerminal();
|
|
52
|
+
const backend = createAnsiTerminalBackend({ console: terminal });
|
|
35
53
|
|
|
36
54
|
const logger = createLogger(backend);
|
|
37
55
|
logger.info('hello world');
|
|
38
56
|
|
|
39
|
-
expect(
|
|
40
|
-
expect.stringContaining('hello world')
|
|
41
|
-
);
|
|
57
|
+
expect(stderr.getOutput()).toContain('hello world');
|
|
42
58
|
});
|
|
43
59
|
|
|
44
60
|
it('includes the log level', () => {
|
|
45
|
-
const terminal =
|
|
46
|
-
const backend =
|
|
61
|
+
const { terminal, stderr } = createTerminal();
|
|
62
|
+
const backend = createAnsiTerminalBackend({ console: terminal });
|
|
47
63
|
|
|
48
64
|
const logger = createLogger(backend);
|
|
49
65
|
logger.debug('shout');
|
|
@@ -51,68 +67,56 @@ describe('ANSI terminal backend', () => {
|
|
|
51
67
|
logger.warn('hmmmm');
|
|
52
68
|
logger.error('oh no');
|
|
53
69
|
|
|
54
|
-
expect(
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
expect(terminal.stderr).toHaveBeenCalledWith(
|
|
59
|
-
expect.stringContaining('INFO')
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
expect(terminal.stderr).toHaveBeenCalledWith(
|
|
63
|
-
expect.stringContaining('WARN')
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
expect(terminal.stderr).toHaveBeenCalledWith(
|
|
67
|
-
expect.stringContaining('ERROR')
|
|
68
|
-
);
|
|
70
|
+
expect(stderr.getOutput()).toContain('DEBUG');
|
|
71
|
+
expect(stderr.getOutput()).toContain('INFO');
|
|
72
|
+
expect(stderr.getOutput()).toContain('WARN');
|
|
73
|
+
expect(stderr.getOutput()).toContain('ERROR');
|
|
69
74
|
});
|
|
70
75
|
|
|
71
76
|
it('includes the log namespace', () => {
|
|
72
|
-
const terminal =
|
|
73
|
-
const backend =
|
|
77
|
+
const { terminal, stderr } = createTerminal();
|
|
78
|
+
const backend = createAnsiTerminalBackend({ console: terminal });
|
|
74
79
|
const logger = createLogger(backend)
|
|
75
80
|
.namespace('my-lib')
|
|
76
81
|
.namespace('MyClass');
|
|
77
82
|
|
|
78
83
|
logger.debug('initialized');
|
|
79
84
|
|
|
80
|
-
expect(
|
|
81
|
-
expect.stringContaining('my-lib:MyClass')
|
|
82
|
-
);
|
|
85
|
+
expect(stderr.getOutput()).toContain('my-lib:MyClass');
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
it('includes the log context', () => {
|
|
86
|
-
const terminal =
|
|
87
|
-
const backend =
|
|
89
|
+
const { terminal, stderr } = createTerminal();
|
|
90
|
+
const backend = createAnsiTerminalBackend({ console: terminal });
|
|
88
91
|
const logger = createLogger(backend);
|
|
89
92
|
|
|
90
93
|
logger.info('creating session', { sessionId: 3109 });
|
|
91
94
|
|
|
92
95
|
// Hard to test without replicating the implementation.
|
|
93
|
-
expect(
|
|
94
|
-
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
expect(terminal.stderr).toHaveBeenCalledWith(
|
|
98
|
-
expect.stringContaining('3109')
|
|
99
|
-
);
|
|
96
|
+
expect(stderr.getOutput()).toContain('sessionId');
|
|
97
|
+
expect(stderr.getOutput()).toContain('3109');
|
|
100
98
|
});
|
|
101
99
|
|
|
102
100
|
it('does not include the log context if it is empty', () => {
|
|
103
|
-
const terminal =
|
|
104
|
-
const backend =
|
|
101
|
+
const { terminal, stderr } = createTerminal();
|
|
102
|
+
const backend = createAnsiTerminalBackend({ console: terminal });
|
|
105
103
|
const logger = createLogger(backend);
|
|
106
104
|
|
|
107
105
|
logger.warn('activating death ray', {});
|
|
108
106
|
|
|
109
107
|
// Hard to test without replicating the implementation.
|
|
110
|
-
expect(
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
expect(stderr.getOutput()).not.toContain('{');
|
|
109
|
+
expect(stderr.getOutput()).not.toContain('}');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('indents multiline strings', () => {
|
|
113
|
+
const { terminal, stderr } = createTerminal();
|
|
114
|
+
const backend = createAnsiTerminalBackend({ console: terminal });
|
|
115
|
+
const logger = createLogger(backend);
|
|
116
|
+
|
|
117
|
+
logger.info('This is a multiline string.\nIt has two lines.');
|
|
113
118
|
|
|
114
|
-
expect(
|
|
115
|
-
|
|
116
|
-
);
|
|
119
|
+
expect(stderr.getOutput()).toContain('This is a multiline string.');
|
|
120
|
+
expect(stderr.getOutput()).toContain(' It has two lines.');
|
|
117
121
|
});
|
|
118
122
|
});
|
|
@@ -11,20 +11,18 @@ import * as ansi from './ansi-codes';
|
|
|
11
11
|
* ques, the printed text is much less understandable. It is better to check
|
|
12
12
|
* when constructing the logger instead.
|
|
13
13
|
*/
|
|
14
|
-
export
|
|
15
|
-
|
|
14
|
+
export function createAnsiTerminalBackend(options: Options = {}): LogProcessor {
|
|
15
|
+
const output = options.console ?? console;
|
|
16
|
+
const labelSizeInWhitespace = ' '.repeat(LogLevel.Error.length);
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
processLog(log: Log) {
|
|
22
|
-
const timestamp = this.getTimestamp(new Date());
|
|
18
|
+
return (log: Log) => {
|
|
19
|
+
const timestamp = formatAsTimestamp(new Date());
|
|
20
|
+
const timestampPrefix = `${ansi.reset}${ansi.dim}${timestamp}${ansi.reset}`;
|
|
23
21
|
const segments = [
|
|
24
22
|
{
|
|
25
23
|
include: true,
|
|
26
24
|
command: '%s',
|
|
27
|
-
content:
|
|
25
|
+
content: timestampPrefix,
|
|
28
26
|
},
|
|
29
27
|
{
|
|
30
28
|
include: true,
|
|
@@ -34,36 +32,39 @@ export default class AnsiTerminalBackend implements LogProcessor {
|
|
|
34
32
|
{
|
|
35
33
|
include: true,
|
|
36
34
|
command: '%s',
|
|
37
|
-
content: log.message
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
command: '%s',
|
|
42
|
-
content: `${ansi.dim}${log.origin.join(':')}${ansi.reset}`,
|
|
35
|
+
content: log.message.replace(
|
|
36
|
+
/(\r?\n)/g,
|
|
37
|
+
`$1${timestampPrefix} ${labelSizeInWhitespace} `
|
|
38
|
+
),
|
|
43
39
|
},
|
|
44
40
|
{
|
|
45
41
|
include: Object.keys(log.context).length > 0,
|
|
46
42
|
command: '%O',
|
|
47
43
|
content: log.context,
|
|
48
44
|
},
|
|
45
|
+
{
|
|
46
|
+
include: log.origin.length > 0,
|
|
47
|
+
command: '%s',
|
|
48
|
+
content: `${ansi.dim}${log.origin.join(':')}${ansi.reset}`,
|
|
49
|
+
},
|
|
49
50
|
].filter((segment) => segment.include);
|
|
50
51
|
|
|
51
52
|
const format = segments.map((segment) => segment.command).join(' ');
|
|
52
53
|
const values = segments.map((segment) => segment.content);
|
|
53
54
|
|
|
54
55
|
// CLIs typically print interactive messages to stdout and logs to stderr.
|
|
55
|
-
|
|
56
|
-
}
|
|
56
|
+
output.error(format, ...values);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// ISO-8601 timestamp with milliseconds.
|
|
61
|
+
function formatAsTimestamp(date: Date) {
|
|
62
|
+
const hours = date.getHours().toString().padStart(2, '0');
|
|
63
|
+
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
64
|
+
const seconds = date.getSeconds().toString().padStart(2, '0');
|
|
65
|
+
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
+
return `[${hours}:${minutes}:${seconds}.${milliseconds}]`;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// Trailing whitespace is important for alignment.
|
|
@@ -75,11 +76,11 @@ const logLevelLabel: Record<LogLevel, string> = {
|
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
interface Options {
|
|
78
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Defaults the global `console`, but in NodeJS you can create a console
|
|
81
|
+
* over any writable stream. It could be a file or a network socket.
|
|
82
|
+
*
|
|
83
|
+
* @see https://nodejs.org/api/console.html#new-consoleoptions
|
|
84
|
+
*/
|
|
85
|
+
console?: Console;
|
|
79
86
|
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* A subset of the Console interface. Must support printf-style interpolation.
|
|
83
|
-
* @see https://console.spec.whatwg.org/#formatting-specifiers
|
|
84
|
-
*/
|
|
85
|
-
export type MinimalConsole = Pick<Console, 'log' | 'error'>;
|
package/src/index.ts
CHANGED