log.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. /** @module env/log */
  2. 'use strict';
  3. const util = require('util');
  4. const EventEmitter = require('events');
  5. const _ = require('lodash');
  6. const table = require('text-table');
  7. const chalk = require('chalk');
  8. const logSymbols = require('log-symbols');
  9. // Padding step
  10. const step = ' ';
  11. let padding = ' ';
  12. function pad(status) {
  13. const max = 'identical'.length;
  14. const delta = max - status.length;
  15. return delta ? ' '.repeat(delta) + status : status;
  16. }
  17. // Borrowed from https://github.com/mikeal/logref/blob/master/main.js#L6-15
  18. function formatter(msg, ctx) {
  19. while (msg.indexOf('%') !== -1) {
  20. const start = msg.indexOf('%');
  21. let end = msg.indexOf(' ', start);
  22. if (end === -1) {
  23. end = msg.length;
  24. }
  25. msg = msg.slice(0, start) + ctx[msg.slice(start + 1, end)] + msg.slice(end);
  26. }
  27. return msg;
  28. }
  29. const getDefaultColors = () => ({
  30. skip: 'yellow',
  31. force: 'yellow',
  32. create: 'green',
  33. invoke: 'bold',
  34. conflict: 'red',
  35. identical: 'cyan',
  36. info: 'gray'
  37. });
  38. const initParams = params => {
  39. params = params || {};
  40. return Object.assign(
  41. {}, params, {
  42. colors: Object.assign(getDefaultColors(), params.colors || {})});
  43. };
  44. module.exports = params => {
  45. // `this.log` is a [logref](https://github.com/mikeal/logref)
  46. // compatible logger, with an enhanced API.
  47. //
  48. // It also has EventEmitter like capabilities, so you can call on / emit
  49. // on it, namely used to increase or decrease the padding.
  50. //
  51. // All logs are done against STDERR, letting you stdout for meaningfull
  52. // value and redirection, should you need to generate output this way.
  53. //
  54. // Log functions take two arguments, a message and a context. For any
  55. // other kind of paramters, `console.error` is used, so all of the
  56. // console format string goodies you're used to work fine.
  57. //
  58. // - msg - The message to show up
  59. // - context - The optional context to escape the message against
  60. //
  61. // @param {Object} params
  62. // @param {Object} params.colors status mappings
  63. //
  64. // Returns the logger
  65. function log(msg, ctx) {
  66. msg = msg || '';
  67. if (typeof ctx === 'object' && !Array.isArray(ctx)) {
  68. console.error(formatter(msg, ctx));
  69. } else {
  70. console.error.apply(console, arguments);
  71. }
  72. return log;
  73. }
  74. _.extend(log, EventEmitter.prototype);
  75. params = initParams(params);
  76. // A simple write method, with formatted message.
  77. //
  78. // Returns the logger
  79. log.write = function () {
  80. process.stderr.write(util.format.apply(util, arguments));
  81. return this;
  82. };
  83. // Same as `log.write()` but automatically appends a `\n` at the end
  84. // of the message.
  85. log.writeln = function () {
  86. this.write.apply(this, arguments);
  87. this.write('\n');
  88. return this;
  89. };
  90. // Convenience helper to write sucess status, this simply prepends the
  91. // message with a gren `✔`.
  92. log.ok = function () {
  93. this.write(logSymbols.success + ' ' + util.format.apply(util, arguments) + '\n');
  94. return this;
  95. };
  96. log.error = function () {
  97. this.write(logSymbols.error + ' ' + util.format.apply(util, arguments) + '\n');
  98. return this;
  99. };
  100. log.on('up', () => {
  101. padding += step;
  102. });
  103. log.on('down', () => {
  104. padding = padding.replace(step, '');
  105. });
  106. /* eslint-disable no-loop-func */
  107. // TODO: Fix this ESLint warning
  108. for (const status of Object.keys(params.colors)) {
  109. // Each predefined status has its logging method utility, handling
  110. // status color and padding before the usual `.write()`
  111. //
  112. // Example
  113. //
  114. // this.log
  115. // .write()
  116. // .info('Doing something')
  117. // .force('Forcing filepath %s, 'some path')
  118. // .conflict('on %s' 'model.js')
  119. // .write()
  120. // .ok('This is ok');
  121. //
  122. // The list of default status and mapping colors
  123. //
  124. // skip yellow
  125. // force yellow
  126. // create green
  127. // invoke bold
  128. // conflict red
  129. // identical cyan
  130. // info grey
  131. //
  132. // Returns the logger
  133. log[status] = function () {
  134. const color = params.colors[status];
  135. this.write(chalk[color](pad(status))).write(padding);
  136. this.write(util.format.apply(util, arguments) + '\n');
  137. return this;
  138. };
  139. }
  140. /* eslint-enable no-loop-func */
  141. // A basic wrapper around `cli-table` package, resetting any single
  142. // char to empty strings, this is used for aligning options and
  143. // arguments without too much Math on our side.
  144. //
  145. // - opts - A list of rows or an Hash of options to pass through cli
  146. // table.
  147. //
  148. // Returns the table reprensetation
  149. log.table = opts => {
  150. const tableData = [];
  151. opts = Array.isArray(opts) ? {rows: opts} : opts;
  152. opts.rows = opts.rows || [];
  153. for (const row of opts.rows) {
  154. tableData.push(row);
  155. }
  156. return table(tableData);
  157. };
  158. return log;
  159. };