1'use strict'
2
3const procLog = require('proc-log')
4const { format } = require('util')
5
6// helper to emit log messages with a predefined prefix
7const logLevels = Object.keys(procLog).filter((k) => typeof procLog[k] === 'function')
8const withPrefix = (prefix) => logLevels.reduce((acc, level) => {
9  acc[level] = (...args) => procLog[level](prefix, ...args)
10  return acc
11}, {})
12
13// very basic ansi color generator
14const COLORS = {
15  wrap: (str, colors) => {
16    const codes = colors.filter(c => typeof c === 'number')
17    return `\x1b[${codes.join(';')}m${str}\x1b[0m`
18  },
19  inverse: 7,
20  fg: {
21    black: 30,
22    red: 31,
23    green: 32,
24    yellow: 33,
25    blue: 34,
26    magenta: 35,
27    cyan: 36,
28    white: 37
29  },
30  bg: {
31    black: 40,
32    red: 41,
33    green: 42,
34    yellow: 43,
35    blue: 44,
36    magenta: 45,
37    cyan: 46,
38    white: 47
39  }
40}
41
42class Logger {
43  #buffer = []
44  #paused = null
45  #level = null
46  #stream = null
47
48  // ordered from loudest to quietest
49  #levels = [{
50    id: 'silly',
51    display: 'sill',
52    style: { inverse: true }
53  }, {
54    id: 'verbose',
55    display: 'verb',
56    style: { fg: 'cyan', bg: 'black' }
57  }, {
58    id: 'info',
59    style: { fg: 'green' }
60  }, {
61    id: 'http',
62    style: { fg: 'green', bg: 'black' }
63  }, {
64    id: 'notice',
65    style: { fg: 'cyan', bg: 'black' }
66  }, {
67    id: 'warn',
68    display: 'WARN',
69    style: { fg: 'black', bg: 'yellow' }
70  }, {
71    id: 'error',
72    display: 'ERR!',
73    style: { fg: 'red', bg: 'black' }
74  }]
75
76  constructor (stream) {
77    process.on('log', (...args) => this.#onLog(...args))
78    this.#levels = new Map(this.#levels.map((level, index) => [level.id, { ...level, index }]))
79    this.level = 'info'
80    this.stream = stream
81    procLog.pause()
82  }
83
84  get stream () {
85    return this.#stream
86  }
87
88  set stream (stream) {
89    this.#stream = stream
90  }
91
92  get level () {
93    return this.#levels.get(this.#level) ?? null
94  }
95
96  set level (level) {
97    this.#level = this.#levels.get(level)?.id ?? null
98  }
99
100  isVisible (level) {
101    return this.level?.index <= this.#levels.get(level)?.index ?? -1
102  }
103
104  #onLog (...args) {
105    const [level] = args
106
107    if (level === 'pause') {
108      this.#paused = true
109      return
110    }
111
112    if (level === 'resume') {
113      this.#paused = false
114      this.#buffer.forEach((b) => this.#log(...b))
115      this.#buffer.length = 0
116      return
117    }
118
119    if (this.#paused) {
120      this.#buffer.push(args)
121      return
122    }
123
124    this.#log(...args)
125  }
126
127  #color (str, { fg, bg, inverse }) {
128    if (!this.#stream?.isTTY) {
129      return str
130    }
131
132    return COLORS.wrap(str, [
133      COLORS.fg[fg],
134      COLORS.bg[bg],
135      inverse && COLORS.inverse
136    ])
137  }
138
139  #log (levelId, msgPrefix, ...args) {
140    if (!this.isVisible(levelId) || typeof this.#stream?.write !== 'function') {
141      return
142    }
143
144    const level = this.#levels.get(levelId)
145
146    const prefixParts = [
147      this.#color('gyp', { fg: 'white', bg: 'black' }),
148      this.#color(level.display ?? level.id, level.style)
149    ]
150    if (msgPrefix) {
151      prefixParts.push(this.#color(msgPrefix, { fg: 'magenta' }))
152    }
153
154    const prefix = prefixParts.join(' ').trim() + ' '
155    const lines = format(...args).split(/\r?\n/).map(l => prefix + l.trim())
156
157    this.#stream.write(lines.join('\n') + '\n')
158  }
159}
160
161// used to suppress logs in tests
162const NULL_LOGGER = !!process.env.NODE_GYP_NULL_LOGGER
163
164module.exports = {
165  logger: new Logger(NULL_LOGGER ? null : process.stderr),
166  stdout: NULL_LOGGER ? () => {} : (...args) => console.log(...args),
167  withPrefix,
168  ...procLog
169}
170