@holz/ansi-terminal-backend 0.2.0 → 0.5.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.
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var s=(o=>(o.Error="error",o.Warn="warn",o.Info="info",o.Debug="debug",o))(s||{});function t(o){return`\x1B[${o}`}const i={black:t("30m"),red:t("31m"),green:t("32m"),yellow:t("33m"),blue:t("34m"),magenta:t("35m"),cyan:t("36m"),white:t("37m")},n={reset:t("0m"),bold:t("1m"),dim:t("2m")};class d{constructor(e={}){this.console=e.console??console}processLog(e){const m=this.getTimestamp(new Date),c=[{include:!0,command:"%s",content:`${n.reset}${n.dim}${m}${n.reset}`},{include:!0,command:"%s",content:u[e.level]},{include:!0,command:"%s",content:e.message},{include:e.origin.length>0,command:"%s",content:`${n.dim}${e.origin.join(":")}${n.reset}`},{include:Object.keys(e.context).length>0,command:"%O",content:e.context}].filter(r=>r.include),a=c.map(r=>r.command).join(" "),l=c.map(r=>r.content);this.console.error(a,...l)}getTimestamp(e){const m=e.getHours().toString().padStart(2,"0"),c=e.getMinutes().toString().padStart(2,"0"),a=e.getSeconds().toString().padStart(2,"0"),l=e.getMilliseconds().toString().padStart(3,"0");return`[${m}:${c}:${a}.${l}]`}}const u={[s.Debug]:`${i.blue}DEBUG${n.reset}`,[s.Info]:`${i.green}INFO${n.reset} `,[s.Warn]:`${i.yellow}WARN${n.reset} `,[s.Error]:`${i.red}ERROR${n.reset}`};exports.AnsiTerminalBackend=d;exports.default=d;
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,68 +1,54 @@
1
- var s = /* @__PURE__ */ ((o) => (o.Error = "error", o.Warn = "warn", o.Info = "info", o.Debug = "debug", o))(s || {});
2
- function t(o) {
3
- return `\x1B[${o}`;
1
+ import { LogLevel as s } from "@holz/core";
2
+ function o(t) {
3
+ return `\x1B[${t}`;
4
4
  }
5
- const m = {
6
- black: t("30m"),
7
- red: t("31m"),
8
- green: t("32m"),
9
- yellow: t("33m"),
10
- blue: t("34m"),
11
- magenta: t("35m"),
12
- cyan: t("36m"),
13
- white: t("37m")
14
- }, n = {
15
- reset: t("0m"),
16
- bold: t("1m"),
17
- dim: t("2m")
18
- };
19
- class u {
20
- constructor(e = {}) {
21
- this.console = e.console ?? console;
22
- }
23
- processLog(e) {
24
- const i = this.getTimestamp(new Date()), c = [
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 = [
25
10
  {
26
11
  include: !0,
27
12
  command: "%s",
28
- content: `${n.reset}${n.dim}${i}${n.reset}`
13
+ content: a
29
14
  },
30
15
  {
31
16
  include: !0,
32
17
  command: "%s",
33
- content: d[e.level]
18
+ content: h[n.level]
34
19
  },
35
20
  {
36
21
  include: !0,
37
22
  command: "%s",
38
- content: e.message
23
+ content: n.message.replace(
24
+ /(\r?\n)/g,
25
+ `$1${a} ${i} `
26
+ )
39
27
  },
40
28
  {
41
- include: e.origin.length > 0,
42
- command: "%s",
43
- content: `${n.dim}${e.origin.join(":")}${n.reset}`
29
+ include: Object.keys(n.context).length > 0,
30
+ command: "%O",
31
+ content: n.context
44
32
  },
45
33
  {
46
- include: Object.keys(e.context).length > 0,
47
- command: "%O",
48
- content: e.context
34
+ include: n.origin.length > 0,
35
+ command: "%s",
36
+ content: `${u}${n.origin.join(":")}${e}`
49
37
  }
50
- ].filter((r) => r.include), a = c.map((r) => r.command).join(" "), l = c.map((r) => r.content);
51
- this.console.error(a, ...l);
52
- }
53
- // ISO-8601 timestamp with milliseconds.
54
- getTimestamp(e) {
55
- const i = e.getHours().toString().padStart(2, "0"), c = e.getMinutes().toString().padStart(2, "0"), a = e.getSeconds().toString().padStart(2, "0"), l = e.getMilliseconds().toString().padStart(3, "0");
56
- return `[${i}:${c}:${a}.${l}]`;
57
- }
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}]`;
58
45
  }
59
- const d = {
60
- [s.Debug]: `${m.blue}DEBUG${n.reset}`,
61
- [s.Info]: `${m.green}INFO${n.reset} `,
62
- [s.Warn]: `${m.yellow}WARN${n.reset} `,
63
- [s.Error]: `${m.red}ERROR${n.reset}`
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}`
64
51
  };
65
52
  export {
66
- u as AnsiTerminalBackend,
67
- u as default
53
+ v as createAnsiTerminalBackend
68
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holz/ansi-terminal-backend",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "An ANSI terminal backend for Holz",
5
5
  "type": "module",
6
6
  "main": "./dist/holz-ansi-terminal-backend.cjs",
@@ -37,11 +37,14 @@
37
37
  "test:unit": "vitest --color --passWithNoTests",
38
38
  "test:types": "tsc"
39
39
  },
40
+ "peerDependencies": {
41
+ "@holz/core": "^0.5.0"
42
+ },
40
43
  "devDependencies": {
41
- "@holz/core": "0.2.0",
44
+ "@holz/core": "^0.5.0",
42
45
  "@types/node": "^18.14.0",
43
- "@vitest/coverage-c8": "0.28.5",
44
- "typescript": "4.9.5",
46
+ "@vitest/coverage-c8": "^0.28.5",
47
+ "typescript": "^4.9.5",
45
48
  "vite": "^4.0.0",
46
49
  "vitest": "^0.28.5"
47
50
  }
@@ -1,24 +1,42 @@
1
- import { format } from 'util';
1
+ import { Writable } from 'node:stream';
2
+ import { Console } from 'node:console';
2
3
  import { createLogger } from '@holz/core';
3
- import type { MinimalConsole } from '../ansi-terminal-backend';
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
- class MockConsole implements MinimalConsole {
9
- log(...strings: Array<unknown>) {
10
- this.stdout(format(...strings));
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
- error(...strings: Array<unknown>) {
14
- this.stderr(format(...strings));
18
+ return {
19
+ getOutput: () => output,
20
+ stream,
21
+ };
15
22
  }
16
23
 
17
- stdout = vi.fn();
18
- stderr = vi.fn();
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 = new MockConsole();
34
- const backend = new AnsiTerminalBackend({ console: terminal });
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(terminal.stderr).toHaveBeenCalledWith(
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 = new MockConsole();
46
- const backend = new AnsiTerminalBackend({ console: terminal });
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(terminal.stderr).toHaveBeenCalledWith(
55
- expect.stringContaining('DEBUG')
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 = new MockConsole();
73
- const backend = new AnsiTerminalBackend({ console: terminal });
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(terminal.stderr).toHaveBeenCalledWith(
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 = new MockConsole();
87
- const backend = new AnsiTerminalBackend({ console: terminal });
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(terminal.stderr).toHaveBeenCalledWith(
94
- expect.stringContaining('sessionId')
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 = new MockConsole();
104
- const backend = new AnsiTerminalBackend({ console: terminal });
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(terminal.stderr).not.toHaveBeenCalledWith(
111
- expect.stringContaining('{')
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(terminal.stderr).not.toHaveBeenCalledWith(
115
- expect.stringContaining('}')
116
- );
119
+ expect(stderr.getOutput()).toContain('This is a multiline string.');
120
+ expect(stderr.getOutput()).toContain(' It has two lines.');
117
121
  });
118
122
  });
package/src/ansi-codes.ts CHANGED
@@ -3,25 +3,19 @@ function ansiCode(code: string) {
3
3
  }
4
4
 
5
5
  /**
6
- * Unix only. This will break for win32 terminals and may spew garbage on
7
- * terminals without 4-bit color support.
8
- *
9
- * TODO: Strip colors on non-interactive TTYs.
6
+ * Assumes 3-bit color support for an ANSI terminal. This should work on most
7
+ * modern terminals, including on Windows.
10
8
  */
11
9
 
12
- export const color = {
13
- black: ansiCode('30m'),
14
- red: ansiCode('31m'),
15
- green: ansiCode('32m'),
16
- yellow: ansiCode('33m'),
17
- blue: ansiCode('34m'),
18
- magenta: ansiCode('35m'),
19
- cyan: ansiCode('36m'),
20
- white: ansiCode('37m'),
21
- };
10
+ export const reset = ansiCode('0m');
11
+ export const bold = ansiCode('1m');
12
+ export const dim = ansiCode('2m');
22
13
 
23
- export const code = {
24
- reset: ansiCode('0m'),
25
- bold: ansiCode('1m'),
26
- dim: ansiCode('2m'),
27
- };
14
+ export const black = ansiCode('30m');
15
+ export const red = ansiCode('31m');
16
+ export const green = ansiCode('32m');
17
+ export const yellow = ansiCode('33m');
18
+ export const blue = ansiCode('34m');
19
+ export const magenta = ansiCode('35m');
20
+ export const cyan = ansiCode('36m');
21
+ export const white = ansiCode('37m');
@@ -1,6 +1,6 @@
1
1
  import type { Log, LogProcessor } from '@holz/core';
2
2
  import { LogLevel } from '@holz/core';
3
- import { color, code } from './ansi-codes';
3
+ import * as ansi from './ansi-codes';
4
4
 
5
5
  /**
6
6
  * A backend that prints logs to a 3-bit ansi terminal. This should work on
@@ -11,20 +11,18 @@ import { color, code } 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 default class AnsiTerminalBackend implements LogProcessor {
15
- private console: MinimalConsole;
14
+ export function createAnsiTerminalBackend(options: Options = {}): LogProcessor {
15
+ const output = options.console ?? console;
16
+ const labelSizeInWhitespace = ' '.repeat(LogLevel.Error.length);
16
17
 
17
- constructor(options: Options = {}) {
18
- this.console = options.console ?? console;
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: `${code.reset}${code.dim}${timestamp}${code.reset}`,
25
+ content: timestampPrefix,
28
26
  },
29
27
  {
30
28
  include: true,
@@ -34,52 +32,55 @@ export default class AnsiTerminalBackend implements LogProcessor {
34
32
  {
35
33
  include: true,
36
34
  command: '%s',
37
- content: log.message,
38
- },
39
- {
40
- include: log.origin.length > 0,
41
- command: '%s',
42
- content: `${code.dim}${log.origin.join(':')}${code.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
- this.console.error(format, ...values);
56
- }
56
+ output.error(format, ...values);
57
+ };
58
+ }
57
59
 
58
- // ISO-8601 timestamp with milliseconds.
59
- private getTimestamp(date: Date) {
60
- const hours = date.getHours().toString().padStart(2, '0');
61
- const minutes = date.getMinutes().toString().padStart(2, '0');
62
- const seconds = date.getSeconds().toString().padStart(2, '0');
63
- const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
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
- return `[${hours}:${minutes}:${seconds}.${milliseconds}]`;
66
- }
67
+ return `[${hours}:${minutes}:${seconds}.${milliseconds}]`;
67
68
  }
68
69
 
69
70
  // Trailing whitespace is important for alignment.
70
71
  const logLevelLabel: Record<LogLevel, string> = {
71
- [LogLevel.Debug]: `${color.blue}DEBUG${code.reset}`,
72
- [LogLevel.Info]: `${color.green}INFO${code.reset} `,
73
- [LogLevel.Warn]: `${color.yellow}WARN${code.reset} `,
74
- [LogLevel.Error]: `${color.red}ERROR${code.reset}`,
72
+ [LogLevel.Debug]: `${ansi.blue}DEBUG${ansi.reset}`,
73
+ [LogLevel.Info]: `${ansi.green}INFO${ansi.reset} `,
74
+ [LogLevel.Warn]: `${ansi.yellow}WARN${ansi.reset} `,
75
+ [LogLevel.Error]: `${ansi.red}ERROR${ansi.reset}`,
75
76
  };
76
77
 
77
78
  interface Options {
78
- console?: MinimalConsole;
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
@@ -1,4 +1 @@
1
- export {
2
- default,
3
- default as AnsiTerminalBackend,
4
- } from './ansi-terminal-backend';
1
+ export { createAnsiTerminalBackend } from './ansi-terminal-backend';