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;