create-debugger.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import {Errorf} from '@e22m4u/js-format';
  2. import {format} from '@e22m4u/js-format';
  3. import {isNonArrayObject} from './utils/index.js';
  4. import {generateRandomHex} from './utils/index.js';
  5. import {createColorizedDump} from './create-colorized-dump.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. * Стандартное количество пробелов в одном шаге смещения.
  20. *
  21. * @type {number}
  22. */
  23. export const DEFAULT_OFFSET_STEP_SPACES = 2;
  24. /**
  25. * Подбор цвета для строки.
  26. *
  27. * @param {string} input
  28. * @returns {number}
  29. */
  30. function pickColorCode(input) {
  31. if (typeof input !== 'string')
  32. throw new Errorf(
  33. 'The parameter "input" of the function pickColorCode ' +
  34. 'must be a String, but %v given.',
  35. input,
  36. );
  37. let hash = 0;
  38. for (let i = 0; i < input.length; i++) {
  39. hash = (hash << 5) - hash + input.charCodeAt(i);
  40. hash |= 0;
  41. }
  42. return AVAILABLE_COLORS[Math.abs(hash) % AVAILABLE_COLORS.length];
  43. }
  44. /**
  45. * Оборачивает строку в цветовой код. Цвет определяется
  46. * по содержимому строки.
  47. *
  48. * @param {string} input
  49. * @param {number} color
  50. * @returns {string}
  51. */
  52. function wrapStringByColorCode(input, color) {
  53. if (typeof input !== 'string')
  54. throw new Errorf(
  55. 'The parameter "input" of the function wrapStringByColorCode ' +
  56. 'must be a String, but %v given.',
  57. input,
  58. );
  59. if (typeof color !== 'number')
  60. throw new Errorf(
  61. 'The parameter "color" of the function wrapStringByColorCode ' +
  62. 'must be a Number, but %v given.',
  63. color,
  64. );
  65. const colorCode = '\u001B[3' + (Number(color) < 8 ? color : '8;5;' + color);
  66. return `${colorCode};1m${input}\u001B[0m`;
  67. }
  68. /**
  69. * Проверка соответствия строки указанному шаблону.
  70. *
  71. * Примеры:
  72. * ```ts
  73. * console.log(matchPattern('app*', 'app:service')); // true
  74. * console.log(matchPattern('app:*', 'app:service')); // true
  75. * console.log(matchPattern('other*', 'app:service')); // false
  76. * console.log(matchPattern('app:service', 'app:service')); // true
  77. * console.log(matchPattern('app:other', 'app:service')); // false
  78. * ```
  79. *
  80. * @param {string} pattern
  81. * @param {string} input
  82. * @returns {boolean}
  83. */
  84. function matchPattern(pattern, input) {
  85. if (typeof pattern !== 'string')
  86. throw new Errorf(
  87. 'The parameter "pattern" of the function matchPattern ' +
  88. 'must be a String, but %v given.',
  89. pattern,
  90. );
  91. if (typeof input !== 'string')
  92. throw new Errorf(
  93. 'The parameter "input" of the function matchPattern ' +
  94. 'must be a String, but %v given.',
  95. input,
  96. );
  97. const regexpStr = pattern.replace(/\*/g, '.*?');
  98. const regexp = new RegExp('^' + regexpStr + '$');
  99. return regexp.test(input);
  100. }
  101. /**
  102. * Create debugger.
  103. *
  104. * @param {string} namespaceOrOptions
  105. * @param {string} namespaceSegments
  106. * @returns {Function}
  107. */
  108. export function createDebugger(
  109. namespaceOrOptions = undefined,
  110. ...namespaceSegments
  111. ) {
  112. // если первый аргумент не является строкой
  113. // и объектом, то выбрасывается ошибка
  114. if (
  115. namespaceOrOptions &&
  116. typeof namespaceOrOptions !== 'string' &&
  117. !isNonArrayObject(namespaceOrOptions)
  118. ) {
  119. throw new Errorf(
  120. 'The parameter "namespace" of the function createDebugger ' +
  121. 'must be a String or an Object, but %v given.',
  122. namespaceOrOptions,
  123. );
  124. }
  125. // формирование состояния отладчика
  126. // для хранения текущих настроек
  127. const withCustomState = isNonArrayObject(namespaceOrOptions);
  128. const state = withCustomState ? namespaceOrOptions : {};
  129. state.envNsSegments = Array.isArray(state.envNsSegments)
  130. ? state.envNsSegments
  131. : [];
  132. state.nsSegments = Array.isArray(state.nsSegments) ? state.nsSegments : [];
  133. state.pattern = typeof state.pattern === 'string' ? state.pattern : '';
  134. state.hash = typeof state.hash === 'string' ? state.hash : '';
  135. state.offsetSize =
  136. typeof state.offsetSize === 'number' ? state.offsetSize : 0;
  137. state.offsetStep =
  138. typeof state.offsetStep !== 'string'
  139. ? ' '.repeat(DEFAULT_OFFSET_STEP_SPACES)
  140. : state.offsetStep;
  141. state.delimiter =
  142. state.delimiter && typeof state.delimiter === 'string'
  143. ? state.delimiter
  144. : ':';
  145. // если первым аргументом не является объект состояния,
  146. // то дополнительно проверяется наличие сегмента пространства
  147. // имен в переменной окружения, и сегмент из первого аргумента
  148. if (!withCustomState) {
  149. // если переменная окружения DEBUGGER_NAMESPACE содержит
  150. // пространство имен, то значение переменной добавляется
  151. // в общий список
  152. if (
  153. typeof process !== 'undefined' &&
  154. process.env &&
  155. process.env['DEBUGGER_NAMESPACE']
  156. ) {
  157. state.envNsSegments.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.envNsSegments, ...state.nsSegments].join(
  192. state.delimiter,
  193. );
  194. const patterns = state.pattern.split(/[\s,]+/).filter(p => p.length > 0);
  195. if (patterns.length === 0 && state.pattern !== '*') return false;
  196. for (const singlePattern of patterns) {
  197. if (matchPattern(singlePattern, nsStr)) return true;
  198. }
  199. return false;
  200. };
  201. // формирование префикса
  202. // для сообщений отладки
  203. const getPrefix = () => {
  204. let tokens = [];
  205. [...state.envNsSegments, ...state.nsSegments, state.hash]
  206. .filter(Boolean)
  207. .forEach(token => {
  208. const extractedTokens = token.split(state.delimiter).filter(Boolean);
  209. tokens = [...tokens, ...extractedTokens];
  210. });
  211. let res = tokens.reduce((acc, token, index) => {
  212. const isLast = tokens.length - 1 === index;
  213. const tokenColor = pickColorCode(token);
  214. acc += wrapStringByColorCode(token, tokenColor);
  215. if (!isLast) acc += state.delimiter;
  216. return acc;
  217. }, '');
  218. if (state.offsetSize > 0) res += state.offsetStep.repeat(state.offsetSize);
  219. return res;
  220. };
  221. // формирование функции вывода
  222. // сообщений отладки
  223. function debugFn(messageOrData, ...args) {
  224. if (!isDebuggerEnabled()) return;
  225. const prefix = getPrefix();
  226. const multiString = format(messageOrData, ...args);
  227. const rows = multiString.split('\n');
  228. rows.forEach(message => {
  229. // (!) вывод сообщения отладки
  230. // для сообщения может быть определен префикс,
  231. // содержащий пространства имен и хэш операции
  232. prefix ? console.log(`${prefix} ${message}`) : console.log(message);
  233. });
  234. }
  235. // создание новой функции логирования
  236. // с дополнительным пространством имен
  237. debugFn.withNs = function (namespace, ...args) {
  238. const stateCopy = JSON.parse(JSON.stringify(state));
  239. [namespace, ...args].forEach(ns => {
  240. if (!ns || typeof ns !== 'string')
  241. throw new Errorf(
  242. 'Debugger namespace must be a non-empty String, but %v given.',
  243. ns,
  244. );
  245. stateCopy.nsSegments.push(ns);
  246. });
  247. return createDebugger(stateCopy);
  248. };
  249. // создание новой функции логирования
  250. // со статическим хэшем
  251. debugFn.withHash = function (hashLength = 4) {
  252. const stateCopy = JSON.parse(JSON.stringify(state));
  253. if (!hashLength || typeof hashLength !== 'number' || hashLength < 1) {
  254. throw new Errorf(
  255. 'Debugger hash must be a positive Number, but %v given.',
  256. hashLength,
  257. );
  258. }
  259. stateCopy.hash = generateRandomHex(hashLength);
  260. return createDebugger(stateCopy);
  261. };
  262. // создание новой функции логирования
  263. // со смещением сообщений отладки
  264. debugFn.withOffset = function (offsetSize) {
  265. const stateCopy = JSON.parse(JSON.stringify(state));
  266. if (!offsetSize || typeof offsetSize !== 'number' || offsetSize < 1) {
  267. throw new Errorf(
  268. 'Debugger offset must be a positive Number, but %v given.',
  269. offsetSize,
  270. );
  271. }
  272. stateCopy.offsetSize = offsetSize;
  273. return createDebugger(stateCopy);
  274. };
  275. // создание новой функции логирования
  276. // без пространства имен из переменной
  277. // окружения DEBUGGER_NAMESPACE
  278. debugFn.withoutEnvNs = function () {
  279. const stateCopy = JSON.parse(JSON.stringify(state));
  280. stateCopy.envNsSegments = [];
  281. return createDebugger(stateCopy);
  282. };
  283. // определение метода inspect, где значение первого аргумента будет
  284. // использовано для дампа, а если передано два аргумента, то первый
  285. // будет являться описанием для второго
  286. debugFn.inspect = function (valueOrDesc, ...args) {
  287. if (!isDebuggerEnabled()) return;
  288. const prefix = getPrefix();
  289. let multiString = '';
  290. // если первый аргумент является строкой, при условии наличия
  291. // других аргументов, то первое значение используется
  292. // как заголовок
  293. if (typeof valueOrDesc === 'string' && args.length) {
  294. multiString += `${valueOrDesc}\n`;
  295. // к дампу добавляется один шаг смещения,
  296. // чтобы визуально связать дамп с заголовком
  297. const multilineDump = args.map(v => createColorizedDump(v)).join('\n');
  298. const dumpRows = multilineDump.split('\n');
  299. multiString += dumpRows.map(v => `${state.offsetStep}${v}`).join('\n');
  300. }
  301. // если первый аргумент не является строкой,
  302. // то все аргументы будут использованы
  303. // как значения дампа
  304. else {
  305. multiString += [valueOrDesc, ...args]
  306. .map(v => createColorizedDump(v))
  307. .join('\n');
  308. }
  309. const rows = multiString.split('\n');
  310. rows.forEach(message => {
  311. // (!) вывод сообщения отладки
  312. // для сообщения может быть определен префикс,
  313. // содержащий пространства имен и хэш операции
  314. prefix ? console.log(`${prefix} ${message}`) : console.log(message);
  315. });
  316. };
  317. return debugFn;
  318. }