1const { inspect } = require('util') 2const npmlog = require('npmlog') 3const log = require('./log-shim.js') 4const { explain } = require('./explain-eresolve.js') 5 6const originalCustomInspect = Symbol('npm.display.original.util.inspect.custom') 7 8// These are most assuredly not a mistake 9// https://eslint.org/docs/latest/rules/no-control-regex 10/* eslint-disable no-control-regex */ 11// \x00 through \x1f, \x7f through \x9f, not including \x09 \x0a \x0b \x0d 12const hasC01 = /[\x00-\x08\x0c\x0e-\x1f\x7f-\x9f]/ 13// Allows everything up to '[38;5;255m' in 8 bit notation 14const allowedSGR = /^\[[0-9;]{0,8}m/ 15// '[38;5;255m'.length 16const sgrMaxLen = 10 17 18// Strips all ANSI C0 and C1 control characters (except for SGR up to 8 bit) 19function stripC01 (str) { 20 if (!hasC01.test(str)) { 21 return str 22 } 23 let result = '' 24 for (let i = 0; i < str.length; i++) { 25 const char = str[i] 26 const code = char.charCodeAt(0) 27 if (!hasC01.test(char)) { 28 // Most characters are in this set so continue early if we can 29 result = `${result}${char}` 30 } else if (code === 27 && allowedSGR.test(str.slice(i + 1, i + sgrMaxLen + 1))) { 31 // \x1b with allowed SGR 32 result = `${result}\x1b` 33 } else if (code <= 31) { 34 // escape all other C0 control characters besides \x7f 35 result = `${result}^${String.fromCharCode(code + 64)}` 36 } else { 37 // hasC01 ensures this is now a C1 control character or \x7f 38 result = `${result}^${String.fromCharCode(code - 64)}` 39 } 40 } 41 return result 42} 43 44class Display { 45 #chalk = null 46 47 constructor () { 48 // pause by default until config is loaded 49 this.on() 50 log.pause() 51 } 52 53 static clean (output) { 54 if (typeof output === 'string') { 55 // Strings are cleaned inline 56 return stripC01(output) 57 } 58 if (!output || typeof output !== 'object') { 59 // Numbers, booleans, null all end up here and don't need cleaning 60 return output 61 } 62 // output && typeof output === 'object' 63 // We can't use hasOwn et al for detecting the original but we can use it 64 // for detecting the properties we set via defineProperty 65 if ( 66 output[inspect.custom] && 67 (!Object.hasOwn(output, originalCustomInspect)) 68 ) { 69 // Save the old one if we didn't already do it. 70 Object.defineProperty(output, originalCustomInspect, { 71 value: output[inspect.custom], 72 writable: true, 73 }) 74 } 75 if (!Object.hasOwn(output, originalCustomInspect)) { 76 // Put a dummy one in for when we run multiple times on the same object 77 Object.defineProperty(output, originalCustomInspect, { 78 value: function () { 79 return this 80 }, 81 writable: true, 82 }) 83 } 84 // Set the custom inspect to our own function 85 Object.defineProperty(output, inspect.custom, { 86 value: function () { 87 const toClean = this[originalCustomInspect]() 88 // Custom inspect can return things other than objects, check type again 89 if (typeof toClean === 'string') { 90 // Strings are cleaned inline 91 return stripC01(toClean) 92 } 93 if (!toClean || typeof toClean !== 'object') { 94 // Numbers, booleans, null all end up here and don't need cleaning 95 return toClean 96 } 97 return stripC01(inspect(toClean, { customInspect: false })) 98 }, 99 writable: true, 100 }) 101 return output 102 } 103 104 on () { 105 process.on('log', this.#logHandler) 106 } 107 108 off () { 109 process.off('log', this.#logHandler) 110 // Unbalanced calls to enable/disable progress 111 // will leave change listeners on the tracker 112 // This pretty much only happens in tests but 113 // this removes the event emitter listener warnings 114 log.tracker.removeAllListeners() 115 } 116 117 load (config) { 118 const { 119 color, 120 chalk, 121 timing, 122 loglevel, 123 unicode, 124 progress, 125 silent, 126 heading = 'npm', 127 } = config 128 129 this.#chalk = chalk 130 131 // npmlog is still going away someday, so this is a hack to dynamically 132 // set the loglevel of timing based on the timing flag, instead of making 133 // a breaking change to npmlog. The result is that timing logs are never 134 // shown except when the --timing flag is set. We also need to change 135 // the index of the silly level since otherwise it is set to -Infinity 136 // and we can't go any lower than that. silent is still set to Infinify 137 // because we DO want silent to hide timing levels. This allows for the 138 // special case of getting timing information while hiding all CLI output 139 // in order to get perf information that might be affected by writing to 140 // a terminal. XXX(npmlog): this will be removed along with npmlog 141 log.levels.silly = -10000 142 log.levels.timing = log.levels[loglevel] + (timing ? 1 : -1) 143 144 log.level = loglevel 145 log.heading = heading 146 147 if (color) { 148 log.enableColor() 149 } else { 150 log.disableColor() 151 } 152 153 if (unicode) { 154 log.enableUnicode() 155 } else { 156 log.disableUnicode() 157 } 158 159 // if it's silent, don't show progress 160 if (progress && !silent) { 161 log.enableProgress() 162 } else { 163 log.disableProgress() 164 } 165 166 // Resume displaying logs now that we have config 167 log.resume() 168 } 169 170 log (...args) { 171 this.#logHandler(...args) 172 } 173 174 #logHandler = (level, ...args) => { 175 try { 176 this.#log(level, ...args) 177 } catch (ex) { 178 try { 179 // if it crashed once, it might again! 180 this.#npmlog('verbose', `attempt to log ${inspect(args)} crashed`, ex) 181 } catch (ex2) { 182 // eslint-disable-next-line no-console 183 console.error(`attempt to log ${inspect(args)} crashed`, ex, ex2) 184 } 185 } 186 } 187 188 #log (...args) { 189 return this.#eresolveWarn(...args) || this.#npmlog(...args) 190 } 191 192 // Explicitly call these on npmlog and not log shim 193 // This is the final place we should call npmlog before removing it. 194 #npmlog (level, ...args) { 195 npmlog[level](...args.map(Display.clean)) 196 } 197 198 // Also (and this is a really inexcusable kludge), we patch the 199 // log.warn() method so that when we see a peerDep override 200 // explanation from Arborist, we can replace the object with a 201 // highly abbreviated explanation of what's being overridden. 202 #eresolveWarn (level, heading, message, expl) { 203 if (level === 'warn' && 204 heading === 'ERESOLVE' && 205 expl && typeof expl === 'object' 206 ) { 207 this.#npmlog(level, heading, message) 208 this.#npmlog(level, '', explain(expl, this.#chalk, 2)) 209 // Return true to short circuit other log in chain 210 return true 211 } 212 } 213} 214 215module.exports = Display 216