consola.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import Types from './types.js'
  2. import { isLogObj } from './utils/index.js'
  3. let paused = false
  4. const queue = []
  5. class Consola {
  6. constructor (options = {}) {
  7. this._reporters = options.reporters || []
  8. this._types = options.types || Types
  9. this.level = options.level !== undefined ? options.level : 3
  10. this._defaults = options.defaults || {}
  11. this._async = options.async !== undefined ? options.async : undefined
  12. this._stdout = options.stdout
  13. this._stderr = options.stderr
  14. this._mockFn = options.mockFn
  15. this._throttle = options.throttle || 1000
  16. this._throttleMin = options.throttleMin || 5
  17. // Create logger functions for current instance
  18. for (const type in this._types) {
  19. const defaults = {
  20. type,
  21. ...this._types[type],
  22. ...this._defaults
  23. }
  24. this[type] = this._wrapLogFn(defaults)
  25. this[type].raw = this._wrapLogFn(defaults, true)
  26. }
  27. // Use _mockFn if is set
  28. if (this._mockFn) {
  29. this.mockTypes()
  30. }
  31. // Keep serialized version of last log
  32. this._lastLogSerialized = undefined
  33. this._lastLog = undefined
  34. this._lastLogTime = undefined
  35. this._lastLogCount = 0
  36. this._throttleTimeout = undefined
  37. }
  38. get stdout () {
  39. return this._stdout || console._stdout // eslint-disable-line no-console
  40. }
  41. get stderr () {
  42. return this._stderr || console._stderr // eslint-disable-line no-console
  43. }
  44. create (options) {
  45. return new Consola(Object.assign({
  46. reporters: this._reporters,
  47. level: this.level,
  48. types: this._types,
  49. defaults: this._defaults,
  50. stdout: this._stdout,
  51. stderr: this._stderr,
  52. mockFn: this._mockFn
  53. }, options))
  54. }
  55. withDefaults (defaults) {
  56. return this.create({
  57. defaults: Object.assign({}, this._defaults, defaults)
  58. })
  59. }
  60. withTag (tag) {
  61. return this.withDefaults({
  62. tag: this._defaults.tag ? (this._defaults.tag + ':' + tag) : tag
  63. })
  64. }
  65. addReporter (reporter) {
  66. this._reporters.push(reporter)
  67. return this
  68. }
  69. removeReporter (reporter) {
  70. if (reporter) {
  71. const i = this._reporters.indexOf(reporter)
  72. if (i >= 0) {
  73. return this._reporters.splice(i, 1)
  74. }
  75. } else {
  76. this._reporters.splice(0)
  77. }
  78. return this
  79. }
  80. setReporters (reporters) {
  81. this._reporters = Array.isArray(reporters)
  82. ? reporters
  83. : [reporters]
  84. return this
  85. }
  86. wrapAll () {
  87. this.wrapConsole()
  88. this.wrapStd()
  89. }
  90. restoreAll () {
  91. this.restoreConsole()
  92. this.restoreStd()
  93. }
  94. wrapConsole () {
  95. for (const type in this._types) {
  96. // Backup original value
  97. if (!console['__' + type]) { // eslint-disable-line no-console
  98. console['__' + type] = console[type] // eslint-disable-line no-console
  99. }
  100. // Override
  101. console[type] = this[type].raw // eslint-disable-line no-console
  102. }
  103. }
  104. restoreConsole () {
  105. for (const type in this._types) {
  106. // Restore if backup is available
  107. if (console['__' + type]) { // eslint-disable-line no-console
  108. console[type] = console['__' + type] // eslint-disable-line no-console
  109. delete console['__' + type] // eslint-disable-line no-console
  110. }
  111. }
  112. }
  113. wrapStd () {
  114. this._wrapStream(this.stdout, 'log')
  115. this._wrapStream(this.stderr, 'log')
  116. }
  117. _wrapStream (stream, type) {
  118. if (!stream) {
  119. return
  120. }
  121. // Backup original value
  122. if (!stream.__write) {
  123. stream.__write = stream.write
  124. }
  125. // Override
  126. stream.write = (data) => {
  127. this[type].raw(String(data).trim())
  128. }
  129. }
  130. restoreStd () {
  131. this._restoreStream(this.stdout)
  132. this._restoreStream(this.stderr)
  133. }
  134. _restoreStream (stream) {
  135. if (!stream) {
  136. return
  137. }
  138. if (stream.__write) {
  139. stream.write = stream.__write
  140. delete stream.__write
  141. }
  142. }
  143. pauseLogs () {
  144. paused = true
  145. }
  146. resumeLogs () {
  147. paused = false
  148. // Process queue
  149. const _queue = queue.splice(0)
  150. for (const item of _queue) {
  151. item[0]._logFn(item[1], item[2])
  152. }
  153. }
  154. mockTypes (mockFn) {
  155. this._mockFn = mockFn || this._mockFn
  156. if (typeof this._mockFn !== 'function') {
  157. return
  158. }
  159. for (const type in this._types) {
  160. this[type] = this._mockFn(type, this._types[type]) || this[type]
  161. this[type].raw = this[type]
  162. }
  163. }
  164. _wrapLogFn (defaults, isRaw) {
  165. return (...args) => {
  166. if (paused) {
  167. queue.push([this, defaults, args, isRaw])
  168. return
  169. }
  170. return this._logFn(defaults, args, isRaw)
  171. }
  172. }
  173. _logFn (defaults, args, isRaw) {
  174. if (defaults.level > this.level) {
  175. return this._async ? Promise.resolve(false) : false
  176. }
  177. // Construct a new log object
  178. const logObj = Object.assign({
  179. date: new Date(),
  180. args: []
  181. }, defaults)
  182. // Consume arguments
  183. if (!isRaw && args.length === 1 && isLogObj(args[0])) {
  184. Object.assign(logObj, args[0])
  185. } else {
  186. logObj.args = Array.from(args)
  187. }
  188. // Aliases
  189. if (logObj.message) {
  190. logObj.args.unshift(logObj.message)
  191. delete logObj.message
  192. }
  193. if (logObj.additional) {
  194. if (!Array.isArray(logObj.additional)) {
  195. logObj.additional = logObj.additional.split('\n')
  196. }
  197. logObj.args.push('\n' + logObj.additional.join('\n'))
  198. delete logObj.additional
  199. }
  200. // Normalize type and tag to lowercase
  201. logObj.type = typeof logObj.type === 'string' ? logObj.type.toLowerCase() : ''
  202. logObj.tag = typeof logObj.tag === 'string' ? logObj.tag.toLowerCase() : ''
  203. // Resolve log
  204. /**
  205. * @param newLog false if the throttle expired and
  206. * we don't want to log a duplicate
  207. */
  208. const resolveLog = (newLog = false) => {
  209. const repeated = this._lastLogCount - this._throttleMin
  210. if (this._lastLog && repeated > 0) {
  211. const args = [...this._lastLog.args]
  212. if (repeated > 1) {
  213. args.push(`(repeated ${repeated} times)`)
  214. }
  215. this._log({ ...this._lastLog, args })
  216. this._lastLogCount = 1
  217. }
  218. // Log
  219. if (newLog) {
  220. this._lastLog = logObj
  221. if (this._async) {
  222. return this._logAsync(logObj)
  223. } else {
  224. this._log(logObj)
  225. }
  226. }
  227. }
  228. // Throttle
  229. clearTimeout(this._throttleTimeout)
  230. const diffTime = this._lastLogTime ? logObj.date - this._lastLogTime : 0
  231. this._lastLogTime = logObj.date
  232. if (diffTime < this._throttle) {
  233. try {
  234. const serializedLog = JSON.stringify([logObj.type, logObj.tag, logObj.args])
  235. const isSameLog = this._lastLogSerialized === serializedLog
  236. this._lastLogSerialized = serializedLog
  237. if (isSameLog) {
  238. this._lastLogCount++
  239. if (this._lastLogCount > this._throttleMin) {
  240. // Auto-resolve when throttle is timed out
  241. this._throttleTimeout = setTimeout(resolveLog, this._throttle)
  242. return // SPAM!
  243. }
  244. }
  245. } catch (_) {
  246. // Circular References
  247. }
  248. }
  249. resolveLog(true)
  250. }
  251. _log (logObj) {
  252. for (const reporter of this._reporters) {
  253. reporter.log(logObj, {
  254. async: false,
  255. stdout: this.stdout,
  256. stderr: this.stderr
  257. })
  258. }
  259. }
  260. _logAsync (logObj) {
  261. return Promise.all(
  262. this._reporters.map(reporter => reporter.log(logObj, {
  263. async: true,
  264. stdout: this.stdout,
  265. stderr: this.stderr
  266. }))
  267. )
  268. }
  269. }
  270. // Legacy support
  271. Consola.prototype.add = Consola.prototype.addReporter
  272. Consola.prototype.remove = Consola.prototype.removeReporter
  273. Consola.prototype.clear = Consola.prototype.removeReporter
  274. Consola.prototype.withScope = Consola.prototype.withTag
  275. Consola.prototype.mock = Consola.prototype.mockTypes
  276. Consola.prototype.pause = Consola.prototype.pauseLogs
  277. Consola.prototype.resume = Consola.prototype.resumeLogs
  278. // Export class
  279. export default Consola