diff --git a/packages/logger/package.json b/packages/logger/package.json index f6b7c68968..4af457c7fc 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -56,16 +56,22 @@ "dependencies": { "@libp2p/interface": "^1.6.2", "@multiformats/multiaddr": "^12.2.3", - "debug": "^4.3.4", "interface-datastore": "^8.2.11", - "multiformats": "^13.1.0" + "ms": "^3.0.0-canary.1", + "multiformats": "^13.1.0", + "supports-color": "^9.4.0" }, "devDependencies": { "@libp2p/peer-id": "^4.2.2", - "@types/debug": "^4.1.12", "aegir": "^44.0.1", "sinon": "^18.0.0", "uint8arrays": "^5.1.0" }, + "browser": { + "./dist/src/debug/node.js": "./dist/src/debug/browser.js" + }, + "react-native": { + "./dist/src/debug/node.js": "./dist/src/debug/browser.js" + }, "sideEffects": false } diff --git a/packages/logger/src/debug/browser.ts b/packages/logger/src/debug/browser.ts new file mode 100644 index 0000000000..9f1d59eb3e --- /dev/null +++ b/packages/logger/src/debug/browser.ts @@ -0,0 +1,250 @@ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +/* eslint-env browser */ + +/** + * This is the web browser implementation of `debug()`. + */ +import humanize from 'ms' +import setup from './common.js' + +const storage = localstorage() + +/** + * Colors. + */ +const colors = [ + '#0000CC', + '#0000FF', + '#0033CC', + '#0033FF', + '#0066CC', + '#0066FF', + '#0099CC', + '#0099FF', + '#00CC00', + '#00CC33', + '#00CC66', + '#00CC99', + '#00CCCC', + '#00CCFF', + '#3300CC', + '#3300FF', + '#3333CC', + '#3333FF', + '#3366CC', + '#3366FF', + '#3399CC', + '#3399FF', + '#33CC00', + '#33CC33', + '#33CC66', + '#33CC99', + '#33CCCC', + '#33CCFF', + '#6600CC', + '#6600FF', + '#6633CC', + '#6633FF', + '#66CC00', + '#66CC33', + '#9900CC', + '#9900FF', + '#9933CC', + '#9933FF', + '#99CC00', + '#99CC33', + '#CC0000', + '#CC0033', + '#CC0066', + '#CC0099', + '#CC00CC', + '#CC00FF', + '#CC3300', + '#CC3333', + '#CC3366', + '#CC3399', + '#CC33CC', + '#CC33FF', + '#CC6600', + '#CC6633', + '#CC9900', + '#CC9933', + '#CCCC00', + '#CCCC33', + '#FF0000', + '#FF0033', + '#FF0066', + '#FF0099', + '#FF00CC', + '#FF00FF', + '#FF3300', + '#FF3333', + '#FF3366', + '#FF3399', + '#FF33CC', + '#FF33FF', + '#FF6600', + '#FF6633', + '#FF9900', + '#FF9933', + '#FFCC00', + '#FFCC33' +] + +/** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +// eslint-disable-next-line complexity +function useColors (): boolean { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + // @ts-expect-error window.process.type and window.process.__nwjs are not in the types + if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) { + return true + } + + // Internet Explorer and Edge do not support colors. + if (typeof navigator !== 'undefined' && navigator.userAgent && (navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/) != null)) { + return false + } + + // Is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + // @ts-expect-error document.documentElement.style.WebkitAppearance is not in the types + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // Is firebug? http://stackoverflow.com/a/398120/376773 + // @ts-expect-error window.console.firebug and window.console.exception are not in the types + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // Is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && (navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) != null) && parseInt(RegExp.$1, 10) >= 31) || + // Double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)) +} + +/** + * Colorize log arguments if enabled. + */ +function formatArgs (this: any, args: any[]): void { + args[0] = (this.useColors ? '%c' : '') + + this.namespace + + (this.useColors ? ' %c' : ' ') + + args[0] + + (this.useColors ? '%c ' : ' ') + + '+' + humanize(this.diff) + + if (!this.useColors) { + return + } + + const c = 'color: ' + this.color + args.splice(1, 0, c, 'color: inherit') + + // The final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + let index = 0 + let lastC = 0 + args[0].replace(/%[a-zA-Z%]/g, (match: string) => { + if (match === '%%') { + return + } + index++ + if (match === '%c') { + // We only are interested in the *last* %c + // (the user may have provided their own) + lastC = index + } + }) + + args.splice(lastC, 0, c) +} + +/** + * Invokes `console.debug()` when available. + * No-op when `console.debug` is not a "function". + * If `console.debug` is not available, falls back + * to `console.log`. + */ +const log = console.debug ?? console.log ?? (() => { }) + +/** + * Save `namespaces`. + * + * @param {string} namespaces + */ +function save (namespaces: string): void { + try { + if (namespaces) { + storage?.setItem('debug', namespaces) + } else { + storage?.removeItem('debug') + } + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } +} + +/** + * Load `namespaces`. + * + * @returns {string} returns the previously persisted debug modes + */ +function load (): string | null | undefined { + let r + try { + r = storage?.getItem('debug') + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG + } + + return r +} + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + */ +function localstorage (): Storage | undefined { + try { + // TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context + // The Browser also has localStorage in the global context. + return localStorage + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } +} + +function setupFormatters (formatters: any): void { + /** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + formatters.j = function (v: any) { + try { + return JSON.stringify(v) + } catch (error: any) { + return '[UnexpectedJSONParseError]: ' + error.message + } + } +} + +export default setup({ formatArgs, save, load, useColors, setupFormatters, colors, storage, log }) diff --git a/packages/logger/src/debug/common.ts b/packages/logger/src/debug/common.ts new file mode 100644 index 0000000000..ee7a0ebdda --- /dev/null +++ b/packages/logger/src/debug/common.ts @@ -0,0 +1,286 @@ +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + */ +import humanize from 'ms' + +export default function setup (env: any): any { + createDebug.debug = createDebug + createDebug.default = createDebug + createDebug.coerce = coerce + createDebug.disable = disable + createDebug.enable = enable + createDebug.enabled = enabled + createDebug.humanize = humanize + createDebug.destroy = destroy + + Object.keys(env).forEach(key => { + // @ts-expect-error cannot use string to index type + createDebug[key] = env[key] + }) + + /** + * The currently active debug mode names, and names to skip. + */ + + createDebug.names = [] as any[] + createDebug.skips = [] as any[] + + /** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + createDebug.formatters = {} as Record + + /** + * Selects a color for a debug namespace + * + * @param {string} namespace - The namespace string for the debug instance to be colored + * @returns {number | string} An ANSI color code for the given namespace + * @api private + */ + function selectColor (namespace: string): number | string { + let hash = 0 + + for (let i = 0; i < namespace.length; i++) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i) + hash |= 0 // Convert to 32bit integer + } + + // @ts-expect-error colors is not in the types + return createDebug.colors[Math.abs(hash) % createDebug.colors.length] + } + createDebug.selectColor = selectColor + + /** + * Create a debugger with the given `namespace`. + * + * @param {string} namespace + * @returns {Function} + */ + function createDebug (namespace: string): any { + let prevTime: any + let enableOverride: any = null + let namespacesCache: any + let enabledCache: any + + function debug (...args: any[]): void { + // Disabled? + // @ts-expect-error enabled is not in the types + if (!debug.enabled) { + return + } + + const self: any = debug + + // Set `diff` timestamp + const curr = Number(new Date()) + const ms = curr - (prevTime || curr) + self.diff = ms + self.prev = prevTime + self.curr = curr + prevTime = curr + + args[0] = createDebug.coerce(args[0]) + + if (typeof args[0] !== 'string') { + // Anything else let's inspect with %O + args.unshift('%O') + } + + // Apply any `formatters` transformations + let index = 0 + args[0] = args[0].replace(/%([a-zA-Z%])/g, (match: any, format: any): any => { + // If we encounter an escaped % then don't increase the array index + if (match === '%%') { + return '%' + } + index++ + const formatter = createDebug.formatters[format] + if (typeof formatter === 'function') { + const val = args[index] + match = formatter.call(self, val) + + // Now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1) + index-- + } + return match + }) + + // Apply env-specific formatting (colors, etc.) + // @ts-expect-error formatArgs is not in the types + createDebug.formatArgs.call(self, args) + + // @ts-expect-error log is not in the types + const logFn = self.log || createDebug.log + logFn.apply(self, args) + } + + debug.namespace = namespace + // @ts-expect-error useColors is not in the types + debug.useColors = createDebug.useColors() + debug.color = createDebug.selectColor(namespace) + debug.extend = extend + debug.destroy = createDebug.destroy // XXX Temporary. Will be removed in the next major release. + + Object.defineProperty(debug, 'enabled', { + enumerable: true, + configurable: false, + get: () => { + if (enableOverride !== null) { + return enableOverride + } + // @ts-expect-error namespaces is not in the types + if (namespacesCache !== createDebug.namespaces) { + // @ts-expect-error namespaces is not in the types + namespacesCache = createDebug.namespaces + enabledCache = createDebug.enabled(namespace) + } + + return enabledCache + }, + set: v => { + enableOverride = v + } + }) + + // Env-specific initialization logic for debug instances + // @ts-expect-error init is not in the types + if (typeof createDebug.init === 'function') { + // @ts-expect-error init is not in the types + createDebug.init(debug) + } + + return debug + } + + function extend (this: any, namespace: string, delimiter: string): any { + const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace) + newDebug.log = this.log + return newDebug + } + + /** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {string} namespaces + */ + function enable (namespaces: string): void { + // @ts-expect-error save is not in the types + createDebug.save(namespaces) + // @ts-expect-error namespaces is not in the types + createDebug.namespaces = namespaces + + createDebug.names = [] + createDebug.skips = [] + + let i + const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/) + const len = split.length + + for (i = 0; i < len; i++) { + if (!split[i]) { + // ignore empty strings + continue + } + + namespaces = split[i].replace(/\*/g, '.*?') + + if (namespaces[0] === '-') { + createDebug.skips.push(new RegExp('^' + namespaces.substr(1) + '$')) + } else { + createDebug.names.push(new RegExp('^' + namespaces + '$')) + } + } + } + + /** + * Disable debug output. + * + * @returns {string} namespaces + * @api public + */ + function disable () { + const namespaces = [ + ...createDebug.names.map(toNamespace), + ...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace) + ].join(',') + createDebug.enable('') + return namespaces + } + + /** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {string} name + * @returns {boolean} + * @api public + */ + function enabled (name: string): boolean { + if (name[name.length - 1] === '*') { + return true + } + + let i + let len + + for (i = 0, len = createDebug.skips.length; i < len; i++) { + if (createDebug.skips[i].test(name)) { + return false + } + } + + for (i = 0, len = createDebug.names.length; i < len; i++) { + if (createDebug.names[i].test(name)) { + return true + } + } + + return false + } + + /** + * Convert regexp to namespace + * + * @param {RegExp} regxep + * @returns {string} namespace + */ + function toNamespace (regexp: RegExp): string { + return regexp.toString() + .substring(2, regexp.toString().length - 2) + .replace(/\.\*\?$/, '*') + } + + /** + * Coerce `val`. + * + * @param {Mixed} val + * @returns {Mixed} + * @api private + */ + function coerce (val: any): any { + if (val instanceof Error) { + return val.stack || val.message + } + return val + } + + /** + * XXX DO NOT USE. This is a temporary stub function. + * XXX It WILL be removed in the next major release. + */ + function destroy () { + console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.') + } + + // @ts-expect-error setupFormatters is not in the types + createDebug.setupFormatters(createDebug.formatters) + + // @ts-expect-error load is not in the types + createDebug.enable(createDebug.load()) + + return createDebug +} diff --git a/packages/logger/src/debug/node.ts b/packages/logger/src/debug/node.ts new file mode 100644 index 0000000000..c3c0571205 --- /dev/null +++ b/packages/logger/src/debug/node.ts @@ -0,0 +1,243 @@ +/** + * Module dependencies. + */ + +import tty from 'tty' +import util from 'util' +import humanize from 'ms' +import setup from './common.js' +import supportsColor from 'supports-color' + +/** + * This is the Node.js implementation of `debug()`. + */ + +/** + * Colors. + */ + +let colors = [6, 2, 3, 4, 5, 1] + +if (supportsColor.stderr !== false && (supportsColor.stderr ?? supportsColor).level >= 2) { + colors = [ + 20, + 21, + 26, + 27, + 32, + 33, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 56, + 57, + 62, + 63, + 68, + 69, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 92, + 93, + 98, + 99, + 112, + 113, + 128, + 129, + 134, + 135, + 148, + 149, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 178, + 179, + 184, + 185, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 214, + 215, + 220, + 221 + ] +} + +/** + * Build up the default `inspectOpts` object from the environment variables. + * + * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js + */ + +const inspectOpts = Object.keys(process.env).filter(key => { + return /^debug_/i.test(key) +}).reduce((obj, key) => { + // Camel-case + const prop = key + .substring(6) + .toLowerCase() + .replace(/_([a-z])/g, (_, k) => { + return k.toUpperCase() + }) + + // Coerce string value into JS value + let val: any = process.env[key] + if (/^(yes|on|true|enabled)$/i.test(val)) { + val = true + } else if (/^(no|off|false|disabled)$/i.test(val)) { + val = false + } else if (val === 'null') { + val = null + } else { + val = Number(val) + } + + obj[prop] = val + return obj +}, {} as Record) + +/** + * Is stdout a TTY? Colored output is enabled when `true`. + */ + +function useColors () { + return 'colors' in inspectOpts + ? Boolean(inspectOpts.colors) + : tty.isatty(process.stderr.fd) +} + +/** + * Adds ANSI color escape codes if enabled. + * + * @api public + */ + +function formatArgs (this: any, args: any[]): void { + const { + namespace: name, useColors + } = this + + if (useColors) { + const c = this.color + const colorCode = '\u001B[3' + (c < 8 ? c : '8;5;' + c) + const prefix = ` ${colorCode};1m${name} \u001B[0m` + + args[0] = prefix + args[0].split('\n').join('\n' + prefix) + args.push(colorCode + 'm+' + humanize(this.diff) + '\u001B[0m') + } else { + args[0] = getDate() + name + ' ' + args[0] + } +} + +function getDate (): string { + if (inspectOpts.hideDate) { + return '' + } + return new Date().toISOString() + ' ' +} + +/** + * Invokes `util.format()` with the specified arguments and writes to stderr. + */ +function log (...args: any[]): boolean { + return process.stderr.write(util.format(...args) + '\n') +} + +/** + * Save `namespaces`. + * + * @param {string} namespaces + */ +function save (namespaces: string): void { + if (namespaces) { + process.env.DEBUG = namespaces + } else { + // If you set a process.env field to null or undefined, it gets cast to the + // string 'null' or 'undefined'. Just delete instead. + delete process.env.DEBUG + } +} + +/** + * Load `namespaces`. + * + * @returns {string} returns the previously persisted debug modes + */ +function load (): string | undefined { + return process.env.DEBUG +} + +/** + * Init logic for `debug` instances. + * + * Create a new `inspectOpts` object in case `useColors` is set + * differently for a particular `debug` instance. + */ + +function init (debug: any): void { + debug.inspectOpts = {} + + const keys = Object.keys(inspectOpts) + for (let i = 0; i < keys.length; i++) { + debug.inspectOpts[keys[i]] = inspectOpts[keys[i]] + } +} + +function setupFormatters (formatters: any): void { + /** + * Map %o to `util.inspect()`, all on a single line. + */ + formatters.o = function (v: any): string { + this.inspectOpts.colors = this.useColors + return util.inspect(v, this.inspectOpts) + .split('\n') + .map(str => str.trim()) + .join(' ') + } + + /** + * Map %O to `util.inspect()`, allowing multiple lines if needed. + */ + formatters.O = function (v: any): string { + this.inspectOpts.colors = this.useColors + return util.inspect(v, this.inspectOpts) + } +} + +export default setup({ init, log, formatArgs, save, load, useColors, setupFormatters, colors, inspectOpts }) diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 75e5ea0291..b5009c537e 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -32,7 +32,7 @@ * ``` */ -import debug from 'debug' +import debug from './debug/node.js' import { base32 } from 'multiformats/bases/base32' import { base58btc } from 'multiformats/bases/base58' import { base64 } from 'multiformats/bases/base64' @@ -194,7 +194,7 @@ export function logger (name: string): Logger { let trace: debug.Debugger = createDisabledLogger(`${name}:trace`) // look at all the debug names and see if trace logging has explicitly been enabled - if (debug.enabled(`${name}:trace`) && debug.names.map(r => r.toString()).find(n => n.includes(':trace')) != null) { + if (debug.enabled(`${name}:trace`) && debug.names.map((r: any) => r.toString()).find((n: string) => n.includes(':trace')) != null) { trace = debug(`${name}:trace`) } diff --git a/packages/logger/test/index.spec.ts b/packages/logger/test/index.spec.ts index 5c1c9dccd6..a68ed29ed4 100644 --- a/packages/logger/test/index.spec.ts +++ b/packages/logger/test/index.spec.ts @@ -1,7 +1,7 @@ import { peerIdFromString } from '@libp2p/peer-id' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' -import debug from 'debug' +import debug from '../src/debug/node.js' import { Key } from 'interface-datastore' import { base32 } from 'multiformats/bases/base32' import { base58btc } from 'multiformats/bases/base58' @@ -16,7 +16,7 @@ describe('logger', () => { const log = logger('hello') expect(log).to.be.a('function') - expect(log).to.a.property('enabled').that.is.not.true() + expect(log).to.have.property('enabled').that.is.not.true() expect(log).to.have.property('error').that.is.a('function') expect(log).to.have.nested.property('error.enabled').that.is.not.true() expect(log).to.have.property('trace').that.is.a('function') @@ -29,7 +29,7 @@ describe('logger', () => { const log = logger.forComponent('hello') expect(log).to.be.a('function') - expect(log).to.a.property('enabled').that.is.not.true() + expect(log).to.have.property('enabled').that.is.not.true() expect(log).to.have.property('error').that.is.a('function') expect(log).to.have.nested.property('error.enabled').that.is.not.true() expect(log).to.have.property('trace').that.is.a('function') @@ -42,7 +42,7 @@ describe('logger', () => { const log = logger('enabled-logger') expect(log).to.be.a('function') - expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('enabled').that.is.true() expect(log).to.have.property('error').that.is.a('function') expect(log).to.have.nested.property('error.enabled').that.is.not.true() expect(log).to.have.property('trace').that.is.a('function') @@ -55,7 +55,7 @@ describe('logger', () => { const log = logger('enabled-with-error-logger') expect(log).to.be.a('function') - expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('enabled').that.is.true() expect(log).to.have.property('error').that.is.a('function') expect(log).to.have.nested.property('error.enabled').that.is.true() expect(log).to.have.property('trace').that.is.a('function') @@ -68,7 +68,7 @@ describe('logger', () => { const log = logger('enabled-with-trace-logger') expect(log).to.be.a('function') - expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('enabled').that.is.true() expect(log).to.have.property('error').that.is.a('function') expect(log).to.have.nested.property('error.enabled').that.is.true() expect(log).to.have.property('trace').that.is.a('function')