create-debugger.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import {inspect} from 'util';
  2. import {Errorf} from '@e22m4u/js-format';
  3. import {format} from '@e22m4u/js-format';
  4. import {isNonArrayObject} from './utils/index.js';
  5. import {generateRandomHex} from './utils/index.js';
  6. /**
  7. * Доступные цвета.
  8. *
  9. * @type {number[]}
  10. */
  11. const AVAILABLE_COLORS = [
  12. 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68,
  13. 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 134,
  14. 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171,
  15. 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, 201, 202, 203, 204,
  16. 205, 206, 207, 208, 209, 214, 215, 220, 221,
  17. ];
  18. /**
  19. * Опции утилиты inspect для дампа объектов.
  20. *
  21. * @type {object}
  22. */
  23. const INSPECT_OPTIONS = {
  24. showHidden: false,
  25. depth: null,
  26. colors: true,
  27. compact: false,
  28. };
  29. /**
  30. * Подбор цвета для строки.
  31. *
  32. * @param {string} input
  33. * @returns {number}
  34. */
  35. function pickColorCode(input) {
  36. if (typeof input !== 'string')
  37. throw new Errorf(
  38. 'The parameter "input" of the function pickColorCode ' +
  39. 'must be a String, but %v given.',
  40. input,
  41. );
  42. let hash = 0;
  43. for (let i = 0; i < input.length; i++) {
  44. hash = (hash << 5) - hash + input.charCodeAt(i);
  45. hash |= 0;
  46. }
  47. return AVAILABLE_COLORS[Math.abs(hash) % AVAILABLE_COLORS.length];
  48. }
  49. /**
  50. * Оборачивает строку в цветовой код. Цвет определяется
  51. * по содержимому строки.
  52. *
  53. * @param {string} input
  54. * @param {number} color
  55. * @returns {string}
  56. */
  57. function wrapStringByColorCode(input, color) {
  58. if (typeof input !== 'string')
  59. throw new Errorf(
  60. 'The parameter "input" of the function wrapStringByColorCode ' +
  61. 'must be a String, but %v given.',
  62. input,
  63. );
  64. if (typeof color !== 'number')
  65. throw new Errorf(
  66. 'The parameter "color" of the function wrapStringByColorCode ' +
  67. 'must be a Number, but %v given.',
  68. color,
  69. );
  70. const colorCode = '\u001B[3' + (Number(color) < 8 ? color : '8;5;' + color);
  71. return `${colorCode};1m${input}\u001B[0m`;
  72. }
  73. /**
  74. * Проверка соответствия строки указанному шаблону.
  75. *
  76. * Примеры:
  77. * ```ts
  78. * console.log(matchPattern('app*', 'app:service')); // true
  79. * console.log(matchPattern('app:*', 'app:service')); // true
  80. * console.log(matchPattern('other*', 'app:service')); // false
  81. * console.log(matchPattern('app:service', 'app:service')); // true
  82. * console.log(matchPattern('app:other', 'app:service')); // false
  83. * ```
  84. *
  85. * @param {string} pattern
  86. * @param {string} input
  87. * @returns {boolean}
  88. */
  89. function matchPattern(pattern, input) {
  90. if (typeof pattern !== 'string')
  91. throw new Errorf(
  92. 'The parameter "pattern" of the function matchPattern ' +
  93. 'must be a String, but %v given.',
  94. pattern,
  95. );
  96. if (typeof input !== 'string')
  97. throw new Errorf(
  98. 'The parameter "input" of the function matchPattern ' +
  99. 'must be a String, but %v given.',
  100. input,
  101. );
  102. const regexpStr = pattern.replace(/\*/g, '.*?');
  103. const regexp = new RegExp('^' + regexpStr + '$');
  104. return regexp.test(input);
  105. }
  106. /**
  107. * Create debugger.
  108. *
  109. * @param {string} namespaceOrOptions
  110. * @param {string[]} namespaceSegments
  111. * @returns {Function}
  112. */
  113. export function createDebugger(
  114. namespaceOrOptions = undefined,
  115. ...namespaceSegments
  116. ) {
  117. // если первый аргумент не является строкой
  118. // и объектом, то выбрасывается ошибка
  119. if (
  120. namespaceOrOptions &&
  121. typeof namespaceOrOptions !== 'string' &&
  122. !isNonArrayObject(namespaceOrOptions)
  123. ) {
  124. throw new Errorf(
  125. 'The parameter "namespace" of the function createDebugger ' +
  126. 'must be a String or an Object, but %v given.',
  127. namespaceOrOptions,
  128. );
  129. }
  130. // формирование состояния отладчика
  131. // для хранения текущих настроек
  132. const withCustomState = isNonArrayObject(namespaceOrOptions);
  133. const state = withCustomState ? namespaceOrOptions : {};
  134. state.nsSegments = Array.isArray(state.nsSegments) ? state.nsSegments : [];
  135. state.pattern = typeof state.pattern === 'string' ? state.pattern : '';
  136. state.hash = typeof state.hash === 'string' ? state.hash : '';
  137. state.offsetSize =
  138. typeof state.offsetSize === 'number' ? state.offsetSize : 0;
  139. state.offsetStep =
  140. typeof state.offsetStep === 'string' ? state.offsetStep : ' ';
  141. state.delimiter =
  142. state.delimiter && typeof state.delimiter === 'string'
  143. ? state.delimiter
  144. : ':';
  145. // если первым аргументом не является объект состояния,
  146. // то дополнительно проверяется наличие сегмента пространства
  147. // имен в переменной окружения, и сегмент из первого аргумента
  148. if (!withCustomState) {
  149. // если переменная окружения содержит пространство
  150. // имен, то значение переменной добавляется
  151. // в общий список
  152. if (
  153. typeof process !== 'undefined' &&
  154. process.env &&
  155. process.env['DEBUGGER_NAMESPACE']
  156. ) {
  157. state.nsSegments.push(process.env.DEBUGGER_NAMESPACE);
  158. }
  159. // если первый аргумент содержит значение,
  160. // то оно используется как пространство имен
  161. if (typeof namespaceOrOptions === 'string')
  162. state.nsSegments.push(namespaceOrOptions);
  163. }
  164. // проверка типа дополнительных сегментов пространства
  165. // имен, и добавление их в общий набор сегментов
  166. namespaceSegments.forEach(segment => {
  167. if (!segment || typeof segment !== 'string')
  168. throw new Errorf(
  169. 'Namespace segment must be a non-empty String, but %v given.',
  170. segment,
  171. );
  172. state.nsSegments.push(segment);
  173. });
  174. // если переменная окружения DEBUG содержит
  175. // значение, то оно используется как шаблон
  176. if (typeof process !== 'undefined' && process.env && process.env['DEBUG']) {
  177. state.pattern = process.env['DEBUG'];
  178. }
  179. // если локальное хранилище браузера содержит
  180. // значение по ключу "debug", то оно используется
  181. // как шаблон вывода
  182. else if (
  183. typeof localStorage !== 'undefined' &&
  184. typeof localStorage.getItem('debug') === 'string'
  185. ) {
  186. state.pattern = localStorage.getItem('debug');
  187. }
  188. // формирование функции для проверки
  189. // активности текущего отладчика
  190. const isDebuggerEnabled = () => {
  191. const nsStr = state.nsSegments.join(state.delimiter);
  192. const patterns = state.pattern.split(/[\s,]+/).filter(p => p.length > 0);
  193. if (patterns.length === 0 && state.pattern !== '*') return false;
  194. for (const singlePattern of patterns) {
  195. if (matchPattern(singlePattern, nsStr)) return true;
  196. }
  197. return false;
  198. };
  199. // формирование префикса
  200. // для сообщений отладки
  201. const getPrefix = () => {
  202. let tokens = [];
  203. [...state.nsSegments, state.hash].filter(Boolean).forEach(token => {
  204. const extractedTokens = token.split(state.delimiter).filter(Boolean);
  205. tokens = [...tokens, ...extractedTokens];
  206. });
  207. let res = tokens.reduce((acc, token, index) => {
  208. const isLast = tokens.length - 1 === index;
  209. const tokenColor = pickColorCode(token);
  210. acc += wrapStringByColorCode(token, tokenColor);
  211. if (!isLast) acc += state.delimiter;
  212. return acc;
  213. }, '');
  214. if (state.offsetSize > 0) res += state.offsetStep.repeat(state.offsetSize);
  215. return res;
  216. };
  217. // формирование функции вывода
  218. // сообщений отладки
  219. function debugFn(messageOrData, ...args) {
  220. if (!isDebuggerEnabled()) return;
  221. const prefix = getPrefix();
  222. if (typeof messageOrData === 'string') {
  223. const message = format(messageOrData, ...args);
  224. prefix ? console.log(`${prefix} ${message}`) : console.log(message);
  225. return;
  226. }
  227. const multiString = inspect(messageOrData, INSPECT_OPTIONS);
  228. const rows = multiString.split('\n');
  229. [...args, ...rows].forEach(message => {
  230. prefix ? console.log(`${prefix} ${message}`) : console.log(message);
  231. });
  232. }
  233. // создание новой функции логирования
  234. // с дополнительным пространством имен
  235. debugFn.withNs = function (namespace, ...args) {
  236. const stateCopy = JSON.parse(JSON.stringify(state));
  237. [namespace, ...args].forEach(ns => {
  238. if (!ns || typeof ns !== 'string')
  239. throw new Errorf(
  240. 'Debugger namespace must be a non-empty String, but %v given.',
  241. ns,
  242. );
  243. stateCopy.nsSegments.push(ns);
  244. });
  245. return createDebugger(stateCopy);
  246. };
  247. // создание новой функции логирования
  248. // со статическим хэшем
  249. debugFn.withHash = function (hashLength = 4) {
  250. const stateCopy = JSON.parse(JSON.stringify(state));
  251. if (!hashLength || typeof hashLength !== 'number' || hashLength < 1) {
  252. throw new Errorf(
  253. 'Debugger hash must be a positive Number, but %v given.',
  254. hashLength,
  255. );
  256. }
  257. stateCopy.hash = generateRandomHex(hashLength);
  258. return createDebugger(stateCopy);
  259. };
  260. // создание новой функции логирования
  261. // со смещением сообщений отладки
  262. debugFn.withOffset = function (offsetSize) {
  263. const stateCopy = JSON.parse(JSON.stringify(state));
  264. if (!offsetSize || typeof offsetSize !== 'number' || offsetSize < 1) {
  265. throw new Errorf(
  266. 'Debugger offset must be a positive Number, but %v given.',
  267. offsetSize,
  268. );
  269. }
  270. stateCopy.offsetSize = offsetSize;
  271. return createDebugger(stateCopy);
  272. };
  273. return debugFn;
  274. }