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