tunnckoCore/find-callsite

View on GitHub
index.js

Summary

Maintainability
A
1 hr
Test Coverage
/*!
 * find-callsite <https://github.com/tunnckoCore/find-callsite>
 *
 * Copyright (c) Charlike Mike Reagent <@tunnckoCore> (https://i.am.charlike.online)
 * Released under the MIT license.
 */

'use strict'

var path = require('path')
var extend = require('extend-shallow')
var relativePaths = require('clean-stacktrace-relative-paths')

/**
 * > Find correct callsite where error is started. All that stuff
 * is because you not always need the first line of the stack
 * to understand where and what happened.
 *
 * In below example we use `rimraf.sync` to throw some error. That's
 * the case when we need to be informed where is the `rimraf.sync` call
 * not where it throws. In that case it is on line 6, column 12.
 *
 * **Example**
 *
 * ```js
 * var findCallsite = require('find-callsite')
 * var rimraf = require('rimraf')
 *
 * function fixture () {
 *   try {
 *     rimraf.sync(5555)
 *   } catch (err) {
 *     var callsiteLine = findCallsite(err)
 *     console.log(callsiteLine)
 *     // => 'at fixture (/home/charlike/apps/find-callsite/example.js:6:12)'
 *
 *     var relativeCallsiteLine = findCallsite(err, {
 *       relativePaths: true
 *     })
 *     console.log(relativeCallsiteLine)
 *     // => 'at fixture (example.js:6:12)'
 *   }
 * }
 *
 * fixture()
 * ```
 *
 * @param  {Error|Object|String} `error` plain or Error object with stack property, or string stack
 * @param  {Object} `opts` optional options object
 * @param  {String} `opts.cwd` give current working directory, default to `process.cwd()`
 * @param  {Boolean} `opts.relativePaths` make path relative to `opts.cwd`, default `false`
 * @return {String} single callsite from whole stack trace, e.g. `at foo (/home/bar/baz.js:33:4)`
 * @api public
 */

module.exports = function findCallsite (error, opts) {
  if (isString(error)) {
    error = { stack: error }
  }
  if (!isObject(error)) {
    throw new TypeError('find-callsite: expect `error` to be an object or string')
  }
  if (!isString(error.stack)) {
    throw new TypeError('find-callsite: expect `error.stack` to be non empty string')
  }
  opts = extend({ cwd: process.cwd(), relativePaths: false }, opts)

  // filepath of very parent
  var filepath = cameFrom(module.parent)

  // allow making path relative + ensurance
  filepath = opts.relativePaths
    ? path.relative(opts.cwd, filepath)
    : filepath

  // get the index where it's found in stack
  var foundIndex = error.stack.indexOf(String(filepath))

  // get whole stack from the begining to that index
  var toFoundPlace = error.stack.slice(0, foundIndex)

  // get the index of the last new line from that part of the stack
  var lastNewLine = toFoundPlace.lastIndexOf('\n')

  // get the rest of the stack, from the found index
  // to the very end of the stack
  var pathPlusRest = error.stack.slice(foundIndex)

  // get the index of first new line in that rest part of the stack
  var restFirstNewLine = pathPlusRest.indexOf('\n')

  // if stack is too short, we will get -1
  // so we should use as index the length of the stack
  restFirstNewLine = restFirstNewLine === -1
    ? error.stack.length
    : restFirstNewLine

  // if both indices are equal,
  // then we should use index of the first new line
  // otherwise we should use index of the end of the stack
  restFirstNewLine = restFirstNewLine === error.stack.length
    ? restFirstNewLine
    : restFirstNewLine + toFoundPlace.length

  // we cut exactly the callsite that we need and we trim it
  var callsiteLine = error.stack.slice(lastNewLine, restFirstNewLine).trim()
  return opts.relativePaths
    ? relativePaths(opts.cwd)(callsiteLine)
    : callsiteLine
}

function isObject (val) {
  return val && typeof val === 'object' && !Array.isArray(val)
}

function isString (val) {
  return val && typeof val === 'string' && val.length > 0
}

function cameFrom (mod) {
  /* istanbul ignore next */
  if (mod && mod.parent) {
    return cameFrom(mod.parent)
  }
  return (mod && mod.filename)
}