index.js
1 /*! 2 * depd 3 * Copyright(c) 2014-2018 Douglas Christopher Wilson 4 * MIT Licensed 5 */ 6 7 /** 8 * Module dependencies. 9 */ 10 11 var relative = require('path').relative 12 13 /** 14 * Module exports. 15 */ 16 17 module.exports = depd 18 19 /** 20 * Get the path to base files on. 21 */ 22 23 var basePath = process.cwd() 24 25 /** 26 * Determine if namespace is contained in the string. 27 */ 28 29 function containsNamespace (str, namespace) { 30 var vals = str.split(/[ ,]+/) 31 var ns = String(namespace).toLowerCase() 32 33 for (var i = 0; i < vals.length; i++) { 34 var val = vals[i] 35 36 // namespace contained 37 if (val && (val === '*' || val.toLowerCase() === ns)) { 38 return true 39 } 40 } 41 42 return false 43 } 44 45 /** 46 * Convert a data descriptor to accessor descriptor. 47 */ 48 49 function convertDataDescriptorToAccessor (obj, prop, message) { 50 var descriptor = Object.getOwnPropertyDescriptor(obj, prop) 51 var value = descriptor.value 52 53 descriptor.get = function getter () { return value } 54 55 if (descriptor.writable) { 56 descriptor.set = function setter (val) { return (value = val) } 57 } 58 59 delete descriptor.value 60 delete descriptor.writable 61 62 Object.defineProperty(obj, prop, descriptor) 63 64 return descriptor 65 } 66 67 /** 68 * Create arguments string to keep arity. 69 */ 70 71 function createArgumentsString (arity) { 72 var str = '' 73 74 for (var i = 0; i < arity; i++) { 75 str += ', arg' + i 76 } 77 78 return str.substr(2) 79 } 80 81 /** 82 * Create stack string from stack. 83 */ 84 85 function createStackString (stack) { 86 var str = this.name + ': ' + this.namespace 87 88 if (this.message) { 89 str += ' deprecated ' + this.message 90 } 91 92 for (var i = 0; i < stack.length; i++) { 93 str += '\n at ' + stack[i].toString() 94 } 95 96 return str 97 } 98 99 /** 100 * Create deprecate for namespace in caller. 101 */ 102 103 function depd (namespace) { 104 if (!namespace) { 105 throw new TypeError('argument namespace is required') 106 } 107 108 var stack = getStack() 109 var site = callSiteLocation(stack[1]) 110 var file = site[0] 111 112 function deprecate (message) { 113 // call to self as log 114 log.call(deprecate, message) 115 } 116 117 deprecate._file = file 118 deprecate._ignored = isignored(namespace) 119 deprecate._namespace = namespace 120 deprecate._traced = istraced(namespace) 121 deprecate._warned = Object.create(null) 122 123 deprecate.function = wrapfunction 124 deprecate.property = wrapproperty 125 126 return deprecate 127 } 128 129 /** 130 * Determine if event emitter has listeners of a given type. 131 * 132 * The way to do this check is done three different ways in Node.js >= 0.8 133 * so this consolidates them into a minimal set using instance methods. 134 * 135 * @param {EventEmitter} emitter 136 * @param {string} type 137 * @returns {boolean} 138 * @private 139 */ 140 141 function eehaslisteners (emitter, type) { 142 var count = typeof emitter.listenerCount !== 'function' 143 ? emitter.listeners(type).length 144 : emitter.listenerCount(type) 145 146 return count > 0 147 } 148 149 /** 150 * Determine if namespace is ignored. 151 */ 152 153 function isignored (namespace) { 154 if (process.noDeprecation) { 155 // --no-deprecation support 156 return true 157 } 158 159 var str = process.env.NO_DEPRECATION || '' 160 161 // namespace ignored 162 return containsNamespace(str, namespace) 163 } 164 165 /** 166 * Determine if namespace is traced. 167 */ 168 169 function istraced (namespace) { 170 if (process.traceDeprecation) { 171 // --trace-deprecation support 172 return true 173 } 174 175 var str = process.env.TRACE_DEPRECATION || '' 176 177 // namespace traced 178 return containsNamespace(str, namespace) 179 } 180 181 /** 182 * Display deprecation message. 183 */ 184 185 function log (message, site) { 186 var haslisteners = eehaslisteners(process, 'deprecation') 187 188 // abort early if no destination 189 if (!haslisteners && this._ignored) { 190 return 191 } 192 193 var caller 194 var callFile 195 var callSite 196 var depSite 197 var i = 0 198 var seen = false 199 var stack = getStack() 200 var file = this._file 201 202 if (site) { 203 // provided site 204 depSite = site 205 callSite = callSiteLocation(stack[1]) 206 callSite.name = depSite.name 207 file = callSite[0] 208 } else { 209 // get call site 210 i = 2 211 depSite = callSiteLocation(stack[i]) 212 callSite = depSite 213 } 214 215 // get caller of deprecated thing in relation to file 216 for (; i < stack.length; i++) { 217 caller = callSiteLocation(stack[i]) 218 callFile = caller[0] 219 220 if (callFile === file) { 221 seen = true 222 } else if (callFile === this._file) { 223 file = this._file 224 } else if (seen) { 225 break 226 } 227 } 228 229 var key = caller 230 ? depSite.join(':') + '__' + caller.join(':') 231 : undefined 232 233 if (key !== undefined && key in this._warned) { 234 // already warned 235 return 236 } 237 238 this._warned[key] = true 239 240 // generate automatic message from call site 241 var msg = message 242 if (!msg) { 243 msg = callSite === depSite || !callSite.name 244 ? defaultMessage(depSite) 245 : defaultMessage(callSite) 246 } 247 248 // emit deprecation if listeners exist 249 if (haslisteners) { 250 var err = DeprecationError(this._namespace, msg, stack.slice(i)) 251 process.emit('deprecation', err) 252 return 253 } 254 255 // format and write message 256 var format = process.stderr.isTTY 257 ? formatColor 258 : formatPlain 259 var output = format.call(this, msg, caller, stack.slice(i)) 260 process.stderr.write(output + '\n', 'utf8') 261 } 262 263 /** 264 * Get call site location as array. 265 */ 266 267 function callSiteLocation (callSite) { 268 var file = callSite.getFileName() || '<anonymous>' 269 var line = callSite.getLineNumber() 270 var colm = callSite.getColumnNumber() 271 272 if (callSite.isEval()) { 273 file = callSite.getEvalOrigin() + ', ' + file 274 } 275 276 var site = [file, line, colm] 277 278 site.callSite = callSite 279 site.name = callSite.getFunctionName() 280 281 return site 282 } 283 284 /** 285 * Generate a default message from the site. 286 */ 287 288 function defaultMessage (site) { 289 var callSite = site.callSite 290 var funcName = site.name 291 292 // make useful anonymous name 293 if (!funcName) { 294 funcName = '<anonymous@' + formatLocation(site) + '>' 295 } 296 297 var context = callSite.getThis() 298 var typeName = context && callSite.getTypeName() 299 300 // ignore useless type name 301 if (typeName === 'Object') { 302 typeName = undefined 303 } 304 305 // make useful type name 306 if (typeName === 'Function') { 307 typeName = context.name || typeName 308 } 309 310 return typeName && callSite.getMethodName() 311 ? typeName + '.' + funcName 312 : funcName 313 } 314 315 /** 316 * Format deprecation message without color. 317 */ 318 319 function formatPlain (msg, caller, stack) { 320 var timestamp = new Date().toUTCString() 321 322 var formatted = timestamp + 323 ' ' + this._namespace + 324 ' deprecated ' + msg 325 326 // add stack trace 327 if (this._traced) { 328 for (var i = 0; i < stack.length; i++) { 329 formatted += '\n at ' + stack[i].toString() 330 } 331 332 return formatted 333 } 334 335 if (caller) { 336 formatted += ' at ' + formatLocation(caller) 337 } 338 339 return formatted 340 } 341 342 /** 343 * Format deprecation message with color. 344 */ 345 346 function formatColor (msg, caller, stack) { 347 var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' + // bold cyan 348 ' \x1b[33;1mdeprecated\x1b[22;39m' + // bold yellow 349 ' \x1b[0m' + msg + '\x1b[39m' // reset 350 351 // add stack trace 352 if (this._traced) { 353 for (var i = 0; i < stack.length; i++) { 354 formatted += '\n \x1b[36mat ' + stack[i].toString() + '\x1b[39m' // cyan 355 } 356 357 return formatted 358 } 359 360 if (caller) { 361 formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m' // cyan 362 } 363 364 return formatted 365 } 366 367 /** 368 * Format call site location. 369 */ 370 371 function formatLocation (callSite) { 372 return relative(basePath, callSite[0]) + 373 ':' + callSite[1] + 374 ':' + callSite[2] 375 } 376 377 /** 378 * Get the stack as array of call sites. 379 */ 380 381 function getStack () { 382 var limit = Error.stackTraceLimit 383 var obj = {} 384 var prep = Error.prepareStackTrace 385 386 Error.prepareStackTrace = prepareObjectStackTrace 387 Error.stackTraceLimit = Math.max(10, limit) 388 389 // capture the stack 390 Error.captureStackTrace(obj) 391 392 // slice this function off the top 393 var stack = obj.stack.slice(1) 394 395 Error.prepareStackTrace = prep 396 Error.stackTraceLimit = limit 397 398 return stack 399 } 400 401 /** 402 * Capture call site stack from v8. 403 */ 404 405 function prepareObjectStackTrace (obj, stack) { 406 return stack 407 } 408 409 /** 410 * Return a wrapped function in a deprecation message. 411 */ 412 413 function wrapfunction (fn, message) { 414 if (typeof fn !== 'function') { 415 throw new TypeError('argument fn must be a function') 416 } 417 418 var args = createArgumentsString(fn.length) 419 var stack = getStack() 420 var site = callSiteLocation(stack[1]) 421 422 site.name = fn.name 423 424 // eslint-disable-next-line no-new-func 425 var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site', 426 '"use strict"\n' + 427 'return function (' + args + ') {' + 428 'log.call(deprecate, message, site)\n' + 429 'return fn.apply(this, arguments)\n' + 430 '}')(fn, log, this, message, site) 431 432 return deprecatedfn 433 } 434 435 /** 436 * Wrap property in a deprecation message. 437 */ 438 439 function wrapproperty (obj, prop, message) { 440 if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { 441 throw new TypeError('argument obj must be object') 442 } 443 444 var descriptor = Object.getOwnPropertyDescriptor(obj, prop) 445 446 if (!descriptor) { 447 throw new TypeError('must call property on owner object') 448 } 449 450 if (!descriptor.configurable) { 451 throw new TypeError('property must be configurable') 452 } 453 454 var deprecate = this 455 var stack = getStack() 456 var site = callSiteLocation(stack[1]) 457 458 // set site name 459 site.name = prop 460 461 // convert data descriptor 462 if ('value' in descriptor) { 463 descriptor = convertDataDescriptorToAccessor(obj, prop, message) 464 } 465 466 var get = descriptor.get 467 var set = descriptor.set 468 469 // wrap getter 470 if (typeof get === 'function') { 471 descriptor.get = function getter () { 472 log.call(deprecate, message, site) 473 return get.apply(this, arguments) 474 } 475 } 476 477 // wrap setter 478 if (typeof set === 'function') { 479 descriptor.set = function setter () { 480 log.call(deprecate, message, site) 481 return set.apply(this, arguments) 482 } 483 } 484 485 Object.defineProperty(obj, prop, descriptor) 486 } 487 488 /** 489 * Create DeprecationError for deprecation 490 */ 491 492 function DeprecationError (namespace, message, stack) { 493 var error = new Error() 494 var stackString 495 496 Object.defineProperty(error, 'constructor', { 497 value: DeprecationError 498 }) 499 500 Object.defineProperty(error, 'message', { 501 configurable: true, 502 enumerable: false, 503 value: message, 504 writable: true 505 }) 506 507 Object.defineProperty(error, 'name', { 508 enumerable: false, 509 configurable: true, 510 value: 'DeprecationError', 511 writable: true 512 }) 513 514 Object.defineProperty(error, 'namespace', { 515 configurable: true, 516 enumerable: false, 517 value: namespace, 518 writable: true 519 }) 520 521 Object.defineProperty(error, 'stack', { 522 configurable: true, 523 enumerable: false, 524 get: function () { 525 if (stackString !== undefined) { 526 return stackString 527 } 528 529 // prepare stack trace 530 return (stackString = createStackString.call(this, stack)) 531 }, 532 set: function setter (val) { 533 stackString = val 534 } 535 }) 536 537 return error 538 }