| var util = require('util'); |
| var colors = require('ansi-colors'); |
| var extend = require('extend-shallow'); |
| var differ = require('arr-diff'); |
| var union = require('arr-union'); |
| |
| var nonEnum = ['message', 'name', 'stack']; |
| var ignored = union(nonEnum, ['__safety', '_stack', 'plugin', 'showProperties', 'showStack']); |
| var props = ['fileName', 'lineNumber', 'message', 'name', 'plugin', 'showProperties', 'showStack', 'stack']; |
| |
| function PluginError(plugin, message, options) { |
| if (!(this instanceof PluginError)) { |
| throw new Error('Call PluginError using new'); |
| } |
| |
| Error.call(this); |
| var opts = setDefaults(plugin, message, options); |
| var self = this; |
| |
| // If opts has an error, get details from it |
| if (typeof opts.error === 'object') { |
| var keys = union(Object.keys(opts.error), nonEnum); |
| |
| // These properties are not enumerable, so we have to add them explicitly. |
| keys.forEach(function(prop) { |
| self[prop] = opts.error[prop]; |
| }); |
| } |
| |
| // Opts object can override |
| props.forEach(function(prop) { |
| if (prop in opts) { |
| this[prop] = opts[prop]; |
| } |
| }, this); |
| |
| // Defaults |
| if (!this.name) { |
| this.name = 'Error'; |
| } |
| if (!this.stack) { |
| |
| /** |
| * `Error.captureStackTrace` appends a stack property which |
| * relies on the toString method of the object it is applied to. |
| * |
| * Since we are using our own toString method which controls when |
| * to display the stack trace, if we don't go through this safety |
| * object we'll get stack overflow problems. |
| */ |
| |
| var safety = {}; |
| safety.toString = function() { |
| return this._messageWithDetails() + '\nStack:'; |
| }.bind(this); |
| |
| Error.captureStackTrace(safety, arguments.callee || this.constructor); |
| this.__safety = safety; |
| } |
| if (!this.plugin) { |
| throw new Error('Missing plugin name'); |
| } |
| if (!this.message) { |
| throw new Error('Missing error message'); |
| } |
| } |
| |
| util.inherits(PluginError, Error); |
| |
| /** |
| * Output a formatted message with details |
| */ |
| |
| PluginError.prototype._messageWithDetails = function() { |
| var msg = 'Message:\n ' + this.message; |
| var details = this._messageDetails(); |
| if (details !== '') { |
| msg += '\n' + details; |
| } |
| return msg; |
| }; |
| |
| /** |
| * Output actual message details |
| */ |
| |
| PluginError.prototype._messageDetails = function() { |
| if (!this.showProperties) { |
| return ''; |
| } |
| |
| var props = differ(Object.keys(this), ignored); |
| var len = props.length; |
| |
| if (len === 0) { |
| return ''; |
| } |
| |
| var res = ''; |
| var i = 0; |
| while (len--) { |
| var prop = props[i++]; |
| res += ' '; |
| res += prop + ': ' + this[prop]; |
| res += '\n'; |
| } |
| return 'Details:\n' + res; |
| }; |
| |
| /** |
| * Override the `toString` method |
| */ |
| |
| PluginError.prototype.toString = function() { |
| var detailsWithStack = function(stack) { |
| return this._messageWithDetails() + '\nStack:\n' + stack; |
| }.bind(this); |
| |
| var msg = ''; |
| if (this.showStack) { |
| // If there is no wrapped error, use the stack captured in the PluginError ctor |
| if (this.__safety) { |
| msg = this.__safety.stack; |
| |
| } else if (this._stack) { |
| msg = detailsWithStack(this._stack); |
| |
| } else { |
| // Stack from wrapped error |
| msg = detailsWithStack(this.stack); |
| } |
| return message(msg, this); |
| } |
| |
| msg = this._messageWithDetails(); |
| return message(msg, this); |
| }; |
| |
| // Format the output message |
| function message(msg, thisArg) { |
| var sig = colors.red(thisArg.name); |
| sig += ' in plugin '; |
| sig += '"' + colors.cyan(thisArg.plugin) + '"'; |
| sig += '\n'; |
| sig += msg; |
| return sig; |
| } |
| |
| /** |
| * Set default options based on arguments. |
| */ |
| |
| function setDefaults(plugin, message, opts) { |
| if (typeof plugin === 'object') { |
| return defaults(plugin); |
| } |
| opts = opts || {}; |
| if (message instanceof Error) { |
| opts.error = message; |
| } else if (typeof message === 'object') { |
| opts = message; |
| } else { |
| opts.message = message; |
| } |
| opts.plugin = plugin; |
| return defaults(opts); |
| } |
| |
| /** |
| * Extend default options with: |
| * |
| * - `showStack`: default=false |
| * - `showProperties`: default=true |
| * |
| * @param {Object} `opts` Options to extend |
| * @return {Object} |
| */ |
| |
| function defaults(opts) { |
| return extend({ |
| showStack: false, |
| showProperties: true, |
| }, opts); |
| } |
| |
| /** |
| * Expose `PluginError` |
| */ |
| |
| module.exports = PluginError; |