summaryrefslogtreecommitdiff
path: root/src/logger.ts
blob: 7ffc2da5aee6148447b3744fe1015cb6acebf49c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import type { CompileError } from './parser/utils/error.js';
import { bold, blue, red, grey, underline } from 'kleur/colors';
import { Writable } from 'stream';
import { format as utilFormat } from 'util';

type ConsoleStream = Writable & {
  fd: 1 | 2;
};

export const defaultLogDestination = new Writable({
  objectMode: true,
  write(event: LogMessage, _, callback) {
    let dest: ConsoleStream = process.stderr;
    if (levels[event.level] < levels['error']) {
      dest = process.stdout;
    }
    let type = event.type;
    if (event.level === 'info') {
      type = bold(blue(type));
    } else if (event.level === 'error') {
      type = bold(red(type));
    }

    dest.write(`[${type}] `);
    dest.write(utilFormat(...event.args));
    dest.write('\n');

    callback();
  },
});

interface LogWritable<T> extends Writable {
  write: (chunk: T) => boolean;
}

export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino
export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error';

export interface LogOptions {
  dest: LogWritable<LogMessage>;
  level: LoggerLevel;
}

export const defaultLogOptions: LogOptions = {
  dest: defaultLogDestination,
  level: 'info',
};

export interface LogMessage {
  type: string;
  level: LoggerLevel;
  message: string;
  args: Array<any>;
}

const levels: Record<LoggerLevel, number> = {
  debug: 20,
  info: 30,
  warn: 40,
  error: 50,
  silent: 90,
};

/** Full logging API */
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...args: Array<any>) {
  const event: LogMessage = {
    type,
    level,
    args,
    message: '',
  };

  // test if this level is enabled or not
  if (levels[opts.level] > levels[level]) {
    return; // do nothing
  }

  opts.dest.write(event);
}

/** Emit a message only shown in debug mode */
export function debug(opts: LogOptions, type: string, ...messages: Array<any>) {
  return log(opts, 'debug', type, ...messages);
}

/** Emit a general info message (be careful using this too much!) */
export function info(opts: LogOptions, type: string, ...messages: Array<any>) {
  return log(opts, 'info', type, ...messages);
}

/** Emit a warning a user should be aware of */
export function warn(opts: LogOptions, type: string, ...messages: Array<any>) {
  return log(opts, 'warn', type, ...messages);
}

/** Emit a fatal error message the user should address. */
export function error(opts: LogOptions, type: string, ...messages: Array<any>) {
  return log(opts, 'error', type, ...messages);
}

/** Pretty format error for display */
export function parseError(opts: LogOptions, err: CompileError) {
  let frame = err.frame
    // Switch colons for pipes
    .replace(/^([0-9]+)(:)/gm, `${bold('$1')} │`)
    // Make the caret red.
    .replace(/(?<=^\s+)(\^)/gm, bold(red(' ^')))
    // Add identation
    .replace(/^/gm, '   ');

  error(
    opts,
    'parse-error',
    `

 ${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))}

 ${bold(red(`𝘅 ${err.message}`))}

${frame}
`
  );
}

// A default logger for when too lazy to pass LogOptions around.
export const logger = {
  debug: debug.bind(null, defaultLogOptions),
  info: info.bind(null, defaultLogOptions),
  warn: warn.bind(null, defaultLogOptions),
  error: error.bind(null, defaultLogOptions),
};