console.js

  1. /**
  2. * Console APIs
  3. *
  4. * The Console APIs provide functionality to allow developers to perform debugging tasks,
  5. * such as logging messages or the values of variables at set points in your code.
  6. *
  7. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/console}
  8. *
  9. * @module Console
  10. */
  11. /* eslint-disable no-control-regex */
  12. import { performance } from 'perf_hooks';
  13. import { green, yellow, cyan, red, bright_black } from 'colors';
  14. const { callConsole } = process.binding('stdio');
  15. // Returns a string with as many spaces as the parameter specified.
  16. function pre(amount) {
  17. return ' '.repeat(amount);
  18. }
  19. // Small util for objects that might not have a `.toString` method.
  20. function objectToString(value) {
  21. return Object.prototype.toString.call(value);
  22. }
  23. /**
  24. * Stringifies almost all JavaScript built-in types.
  25. *
  26. * @ignore
  27. * @param {*} value
  28. * @param {WeakSet} seen
  29. * @param {number} depth
  30. * @returns {string}
  31. */
  32. function stringify(value, seen, depth = 0) {
  33. switch (typeof value) {
  34. case 'string':
  35. return depth > 0 ? stringifyText(value) : value;
  36. case 'number':
  37. case 'boolean':
  38. return yellow(String(value));
  39. case 'undefined':
  40. return bright_black(String(value));
  41. case 'symbol':
  42. return green(String(value));
  43. case 'bigint':
  44. return yellow(String(value) + 'n');
  45. case 'object':
  46. return !value ? 'null' : stringifyObject(value, seen, ++depth);
  47. case 'function':
  48. return !value.name
  49. ? cyan('[Function (anonymous)]')
  50. : cyan(`[Function: ${value.name}]`);
  51. default:
  52. return '[Unknown]';
  53. }
  54. }
  55. function stringifyText(value) {
  56. const text = value.length > 100 ? `${value.slice(0, 100)}...` : value;
  57. const textEscaped = JSON.stringify(text);
  58. return green(textEscaped);
  59. }
  60. function isArray(value) {
  61. return Array.isArray(value);
  62. }
  63. function stringifyArray(arr, seen, depth) {
  64. // Special formatting required if array has only numbers.
  65. const hasOnlyNumbers = arr.every((elem) => typeof elem === 'number');
  66. const entries = [];
  67. for (const elem of arr) {
  68. entries.push(stringify(elem, seen, depth));
  69. }
  70. // Remove the color characters to get the proper length.
  71. const uncoloredEntries = entries.join('').replace(/\u001b\[[0-9;]*m/g, '');
  72. // Multiline formatting.
  73. if (uncoloredEntries.length > 60) {
  74. const start = '[\n';
  75. const end = `\n${pre((depth - 1) * 2)}]`;
  76. const entriesPretty = prettifyArray(entries, depth, hasOnlyNumbers);
  77. return `${start}${entriesPretty}${end}`;
  78. }
  79. // Inline formatting.
  80. return entries.length > 0 ? `[ ${entries.join(', ')} ]` : `[]`;
  81. }
  82. function isTypedArray(value) {
  83. switch (Object.prototype.toString.call(value)) {
  84. case '[object Int8Array]':
  85. case '[object Uint8Array]':
  86. case '[object Uint8ClampedArray]':
  87. case '[object Uint16Array]':
  88. case '[object Int32Array]':
  89. case '[object Uint32Array]':
  90. case '[object Float32Array]':
  91. case '[object Float64Array]':
  92. return true;
  93. default:
  94. return false;
  95. }
  96. }
  97. // Calculate the grid size (trying to make perfect squares and minimizing empty space).
  98. // 1. Max out at 12xN.
  99. // 2. Max out at 01xN (if the lengthier element is too big).
  100. function getMaxElementsPerRow(arr, avgElementLength, maxElementLength) {
  101. if (maxElementLength > 30) return 1;
  102. return Math.min(
  103. Math.max(
  104. Math.floor((Math.sqrt(arr.length) * avgElementLength) / maxElementLength),
  105. 1
  106. ),
  107. 12
  108. );
  109. }
  110. function prettifyArray(arr, depth = 0, hasOnlyNumbers) {
  111. // Remove the color characters so we can calculate the AVG and MAX correctly.
  112. const uncolored = arr.map(
  113. (elem) => elem.replace(/\u001b\[[0-9;]*m/g, '').length
  114. );
  115. const maxElementLength = Math.max(...uncolored);
  116. const avgElementLength = uncolored.reduce((a, b) => a + b) / uncolored.length;
  117. // Calculate the grid size.
  118. const maxElementsPerRow = getMaxElementsPerRow(
  119. arr,
  120. avgElementLength,
  121. maxElementLength
  122. );
  123. // Tries to align the columns.
  124. const alignColumn = (elem, i) => {
  125. const length = elem.replace(/\u001b\[[0-9;]*m/g, '').length;
  126. const shift = maxElementsPerRow === 1 ? 0 : maxElementLength - length;
  127. if (hasOnlyNumbers) {
  128. return i === arr.length - 1
  129. ? pre(shift) + elem
  130. : pre(shift) + elem + ', ';
  131. } else {
  132. return i === arr.length - 1
  133. ? elem + pre(shift)
  134. : elem + ', ' + pre(shift);
  135. }
  136. };
  137. // Creates rows of length `maxElementsPerRow`.
  138. const groupRows = (acc, elem, i) => {
  139. if (acc.atRow === maxElementsPerRow || i === 0) {
  140. acc.list.push([elem]);
  141. acc.atRow = 1;
  142. } else {
  143. acc.list[acc.list.length - 1].push(elem);
  144. acc.atRow++;
  145. }
  146. return acc;
  147. };
  148. // Indents row based on the depth we're currently in.
  149. const indentRow = (row) => pre(depth * 2) + row.join('');
  150. let output = arr.map(alignColumn);
  151. output = output.reduce(groupRows, { atRow: 0, list: [] });
  152. output = output.list.map(indentRow).join('\n');
  153. return output;
  154. }
  155. function stringifyTypedArray(arr, depth = 0) {
  156. // Colorize internal values.
  157. let pretty = arr
  158. .toString()
  159. .split(',')
  160. .map((elem) => yellow(elem));
  161. // Get typed-array's specific type.
  162. const type = Object.prototype.toString
  163. .call(arr)
  164. .replace('[object ', '')
  165. .replace(']', '');
  166. if (pretty.length > 50) {
  167. pretty = prettifyArray(pretty, depth, true);
  168. return `${type}(${arr.length}) [\n${pretty}\n${pre((depth - 1) * 2)}]`;
  169. }
  170. return `${type}(${arr.length}) [ ${pretty.join(', ')} ]`;
  171. }
  172. function isDate(value) {
  173. return Object.prototype.toString.call(value) === '[object Date]';
  174. }
  175. function stringifyDate(date) {
  176. return date.toISOString();
  177. }
  178. function isRexExp(value) {
  179. return Object.prototype.toString.call(value) === '[object RegExp]';
  180. }
  181. function stringifyRexExp(exp) {
  182. return exp.toString();
  183. }
  184. function isError(value) {
  185. return Object.prototype.toString.call(value) === '[object Error]';
  186. }
  187. function stringifyError(error) {
  188. return error.stack;
  189. }
  190. function isArrayBuffer(value) {
  191. return value instanceof ArrayBuffer;
  192. }
  193. function stringifyArrayBuffer(value) {
  194. return `ArrayBuffer { byteLength: ${stringify(value.byteLength)} }`;
  195. }
  196. function isPromise(value) {
  197. return value instanceof Promise;
  198. }
  199. function stringifyPromise(value) {
  200. // We have to use a Rust binding to inspect the contents of a promise
  201. // object because JS doesn't expose that kind of functionality.
  202. const binding = process.binding('promise');
  203. const { state, value: promiseValue } = binding.peek(value);
  204. if (state === 'PENDING') {
  205. return `Promise { ${cyan('<pending>')} }`;
  206. }
  207. const output = stringify(promiseValue, undefined, 1);
  208. const end = `${output.length > 50 ? '\n' : ' '}}`;
  209. const prefix =
  210. state === 'FULFILLED'
  211. ? `${output.length > 50 ? '\n ' : ''}`
  212. : `${output.length > 50 ? '\n ' : ''}${red('<rejected>')} `;
  213. return 'Promise { ' + prefix + output + end;
  214. }
  215. const specialCharsRegex = new RegExp('[^A-Za-z0-9|_]+');
  216. /**
  217. * Specifically stringifies JavaScript objects.
  218. *
  219. * @ignore
  220. * @param {*} value
  221. * @param {WeakSet} seen
  222. * @param {number} depth
  223. * @returns {string}
  224. */
  225. function stringifyObject(value, seen = new WeakSet(), depth) {
  226. // We have to check the type of the value parameter to decide which stringify
  227. // transformer we should use.
  228. if (isArray(value)) {
  229. return stringifyArray(value, seen, depth);
  230. }
  231. if (isArrayBuffer(value)) {
  232. return stringifyArrayBuffer(value);
  233. }
  234. if (isTypedArray(value)) {
  235. return stringifyTypedArray(value, depth);
  236. }
  237. if (isDate(value)) {
  238. return stringifyDate(value);
  239. }
  240. if (isRexExp(value)) {
  241. return stringifyRexExp(value);
  242. }
  243. if (isError(value)) {
  244. return stringifyError(value);
  245. }
  246. if (isPromise(value)) {
  247. return stringifyPromise(value);
  248. }
  249. // It's an object type that console does not support.
  250. if (objectToString(value) !== '[object Object]') {
  251. const type = objectToString(value).replace('[object ', '').replace(']', '');
  252. return `${type} {}`;
  253. }
  254. // Looks like it's a regular object.
  255. const entries = [];
  256. for (const key of Object.keys(value)) {
  257. if (seen.has(value[key])) {
  258. entries.push(`${pre(depth * 2)}${key}: [Circular]`);
  259. continue;
  260. }
  261. // The following wraps in quotes object keys that contain special
  262. // characters like { "Foo-Bar": 123 }.
  263. const keyValue = specialCharsRegex.test(key) ? `"${key}"` : key;
  264. seen.add(value);
  265. entries.push(
  266. `${pre(depth * 2)}${keyValue}: ${stringify(value[key], seen, depth)}`
  267. );
  268. }
  269. // Output the class name if the object is a class instance.
  270. const className = value?.constructor?.name;
  271. const prefix = !className || className === 'Object' ? '' : className + ' ';
  272. // Apply multi-line formatting on long properties.
  273. if (entries.map((v) => v.trim()).join('').length > 50) {
  274. const start = `${prefix}{\n`;
  275. const end = `\n${pre((depth - 1) * 2)}}`;
  276. return `${start}${entries.join(',\n')}${end}`;
  277. }
  278. // Inline formatting.
  279. const entriesPretty = entries.map((v) => v.trim());
  280. const content = entries.length > 0 ? `{ ${entriesPretty.join(', ')} }` : `{}`;
  281. return `${prefix}${content}`;
  282. }
  283. /**
  284. * Shows the given message and waits for the user's input.
  285. *
  286. * @param {String} [message] - A string of text to display to the user.
  287. * @param {String} [defaultValue] - A string containing the default value displayed in the text input field.
  288. */
  289. export function prompt(message = 'Prompt', defaultValue = null) {
  290. // Write prompt message to stdout.
  291. process.stdout.write(
  292. `${message} ${defaultValue ? `[${defaultValue}] ` : ''}`
  293. );
  294. // Read and return user's input.
  295. return process.stdin.read() || defaultValue;
  296. }
  297. /**
  298. * Console is a subset implementation of MDN's Console API.
  299. */
  300. export class Console {
  301. // Holds timers initialized by console.
  302. // https://developer.mozilla.org/en-US/docs/Web/API/Console/time
  303. #timers = new Map();
  304. /**
  305. * Outputs data to the `stdout` stream.
  306. *
  307. * @param {...*} args - Prints to stdout with newline.
  308. */
  309. log(...args) {
  310. const output = args.map((arg) => stringify(arg)).join(' ');
  311. process.stdout.write(`${output}\n`);
  312. }
  313. /**
  314. * An alias to `console.log()`.
  315. *
  316. * @param {...*} args - Prints to stdout with newline.
  317. */
  318. info(...args) {
  319. const output = args.map((arg) => stringify(arg)).join(' ');
  320. process.stdout.write(`${output}\n`);
  321. }
  322. /**
  323. * An alias to `console.log()`.
  324. *
  325. * @param {...*} args - Prints to stdout with newline.
  326. */
  327. debug(...args) {
  328. const output = args.map((arg) => stringify(arg)).join(' ');
  329. process.stdout.write(`${output}\n`);
  330. }
  331. /**
  332. * Same as `console.log` but prepends the output with "WARNING".
  333. *
  334. * @param {...*} args - Prints to stdout with newline.
  335. */
  336. warn(...args) {
  337. const output = args.map((arg) => stringify(arg)).join(' ');
  338. process.stderr.write(`WARNING: ${output}\n`);
  339. }
  340. /**
  341. * Same as `console.log` but prepends the output with "WARNING".
  342. *
  343. * @param {...*} args - Prints to stdout with newline.
  344. */
  345. error(...args) {
  346. const output = args.map((arg) => stringify(arg)).join(' ');
  347. process.stderr.write(`WARNING: ${output}\n`);
  348. }
  349. /**
  350. * Clears the console if the environment allows it.
  351. */
  352. clear() {
  353. try {
  354. process.binding('stdio').clear();
  355. } catch (e) {
  356. this.warn('This environment does not support console clearing');
  357. }
  358. }
  359. /**
  360. * Starts a timer you can use to track how long an operation takes.
  361. *
  362. * @param {String} [label] - A string representing the name to give the new timer.
  363. */
  364. time(label = 'default') {
  365. if (this.#timers.has(label)) {
  366. this.warn(`Timer '${label}' already exists`);
  367. return;
  368. }
  369. this.#timers.set(label, performance.now());
  370. }
  371. /**
  372. * Logs the current value of a timer that was previously started by calling
  373. * console.time() to the console.
  374. *
  375. * @param {String} [label] - The name of the timer to log to the console.
  376. */
  377. timeLog(label = 'default') {
  378. if (!this.#timers.has(label)) {
  379. this.warn(`Timer '${label}' does not exist`);
  380. return;
  381. }
  382. const difference = performance.now() - this.#timers.get(label);
  383. this.log(`${label}: ${difference} ms`);
  384. }
  385. /**
  386. * Stops a timer that was previously started by calling console.time().
  387. *
  388. * @param {String} [label] - A string representing the name of the timer to stop.
  389. */
  390. timeEnd(label = 'default') {
  391. if (!this.#timers.has(label)) {
  392. this.warn(`Timer '${label}' does not exist`);
  393. return;
  394. }
  395. this.timeLog(label);
  396. this.#timers.delete(label);
  397. }
  398. }
  399. // This wrapper forwards console messages to V8's internal console implementation,
  400. // triggering the `Runtime.consoleAPICalled` event. This ensures that the
  401. // attached debugger (if exists) is notified about the console call.
  402. //
  403. // https://github.com/v8/v8/blob/master/src/inspector/v8-console.cc
  404. // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#event-consoleAPICalled
  405. //
  406. export function wrapConsole(console, consoleFromV8) {
  407. // Get the property names of the console prototype.
  408. const prototype = Object.getPrototypeOf(console);
  409. const propertyNames = Object.getOwnPropertyNames(prototype);
  410. for (const key of Object.keys(consoleFromV8)) {
  411. // If global console has the same method as inspector console,
  412. // then wrap these two methods into one.
  413. if (propertyNames.includes(key)) {
  414. console[key] = callConsole.bind(
  415. console,
  416. consoleFromV8[key],
  417. console[key]
  418. );
  419. } else {
  420. // Add additional console APIs from the inspector.
  421. console[key] = consoleFromV8[key];
  422. }
  423. }
  424. }
  425. export default { Console, prompt, wrapConsole };