UNPKG

nor-debug

Version:
713 lines (587 loc) 19.6 kB
/** Helpers for JavaScript debuging * Copyright (c) 2013-2014 Sendanor <info@sendanor.fi> * Copyright (c) 2013-2014 Jaakko-Heikki Heusala <jheusala@iki.fi> * * @FIXME: The ARRAY()'s should be converted to for-loops, etc to improve performance. */ var DEBUG_LINE_LIMIT = parseInt(process.env.DEBUG_LINE_LIMIT || 500, 10); var NODE_ENV = process.env.NODE_ENV || 'development'; var debug = module.exports = require('./core.js'); var util = require("util"); var FS = require("fs"); var PATH = require("path"); var is = require("nor-is"); var ARRAY = require("nor-array"); var FUNCTION = require("nor-function"); var NorAssert = require('./NorAssert.js'); var DummyAssert = require('./DummyAssert.js'); var node_0_11_or_newer = (process.versions && is.string(process.versions.node) && parseFloat(process.versions.node.split('.').slice(0, 2).join('.')) >= 0.11 ) ? true : false; var disable_util = node_0_11_or_newer; /** Returns `true` if value is true value, otherwise `false` */ function parse_env_boolean(value, def) { if( (arguments.length === 2) && (value === undefined) ) { return def; } if(!value) { return false; } if(value === true) { return true; } value = ('' + value).toLowerCase().trim(); if(value === "true") { return true; } if(value === "on") { return true; } if(value === "yes") { return true; } if(value === "y") { return true; } if(value === "1") { return true; } return false; } var DEBUG_ENABLE_COLORS = parse_env_boolean(process.env.DEBUG_ENABLE_COLORS, true); var ansi, stdout_cursor, stderr_cursor; // FIXME: `process.browser` does not seem to work on newer browserify if( (!process.browser) && (DEBUG_ENABLE_COLORS) ) { ansi = require('ansi'); if(ansi && (typeof ansi === 'function')) { stdout_cursor = ansi(process.stdout, {enabled:true}); stderr_cursor = ansi(process.stderr, {enabled:true}); } else { ansi = undefined; } } /* Defaults */ debug.defaults = {}; debug.defaults.cursors = {}; debug.defaults.cursors.error = function(cursor) { return cursor.brightRed(); }; debug.defaults.cursors.warning = function(cursor) { return cursor.brightYellow(); }; debug.defaults.cursors.log = function(cursor) { return cursor.magenta(); }; debug.defaults.cursors.info = function(cursor) { return cursor.green(); }; debug.defaults.production_enable_log = parse_env_boolean(process.env.DEBUG_ENABLE_LOG_IN_PRODUCTION, false); debug.defaults.use_util_error = parse_env_boolean(process.env.DEBUG_USE_UTIL_ERROR, true); debug.defaults.use_util_debug = parse_env_boolean(process.env.DEBUG_USE_UTIL_DEBUG, true); debug.defaults.use_console_log = parse_env_boolean(process.env.DEBUG_USE_CONSOLE_LOG, true); debug.defaults.use_console_info = parse_env_boolean(process.env.DEBUG_USE_CONSOLE_INFO, debug.defaults.use_console_log); /* Features */ var features = {}; // Error.captureStackTrace if(typeof Error.captureStackTrace === 'function') { features.Error_captureStackTrace = true; } // Object.defineProperty if(typeof Object.defineProperty === 'function') { features.Object_defineProperty = true; } /* Pretty print paths */ var print_path = require('./print-path.js'); /* */ debug.setProjectRoot = function(value) { debug.assert(value).is('string'); debug.defaults.project_root = value; debug.log('Paths are now relative to ', debug.defaults.project_root); return debug.defaults.project_root; }; debug.setNodeENV = function(value) { NODE_ENV = (value === 'production') ? 'production' : 'development'; return NODE_ENV; }; /** Set a prefix * @param value {String|Function} The prefix as a string or a function to build it. The function will get the default prefix as first argument. */ debug.setPrefix = function(value) { if(!is.func(value)) { debug.assert(value).is('string'); } debug.defaults.prefix = value; return debug.defaults.prefix; }; /** Get prefix */ function get_prefix(value) { var has_prefix = debug.defaults.hasOwnProperty('prefix'); if(!has_prefix) { return value; } var prefix = has_prefix ? debug.defaults.prefix : ''; if(is.func(prefix)) { return prefix(value); } return ''+ value + ' ' + prefix; } /* Compatibility hacks */ function _setup_property(obj, prop, opts) { if(!features.Object_defineProperty) { return; } Object.defineProperty(obj, prop, opts); } /** For optimal (v8) JIT compilation we use try-catch in own block. * Try-catch is also required for feature testing of imcompatible Object.defineProperty() (IE8). */ function setup_property(obj, prop, opts, failsafe) { try { _setup_property(obj, prop, opts); } catch (err) { obj[prop] = failsafe; } } /** Setup `debig.__stack` */ setup_property(debug, '__stack', { get: function stack_getter(){ if(!features.Error_captureStackTrace) { return []; } var orig, err, stack; try { orig = Error.prepareStackTrace; Error.prepareStackTrace = function(_, stack){ return stack; }; err = new Error(); Error.captureStackTrace(err, stack_getter); stack = err.stack; } finally { Error.prepareStackTrace = orig; } return stack; } }, []); setup_property(debug, '__line', { get: function(){ var stack = debug.__stack; var tmp = stack[1]; if(!(tmp && (typeof tmp.getLineNumber === 'function'))) { return; } return tmp.getLineNumber(); } }, -1); /** Returns true if the app is running in production mode */ debug.isProduction = function () { return (NODE_ENV === "production"); }; /** Returns true if the app is running in development mode */ debug.isDevelopment = function () { return debug.isProduction() ? false : true; }; /** Returns value converted to string and trimmed from white spaces around it */ function inspect_values(x) { if(typeof x === "string") { return x; } return util.inspect(x); } /** Returns value with special chars converted to "\n", "\r" or "\t" */ function convert_specials(x) { return (''+x).replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"); } /** Returns value trimmed from white spaces around it */ function trim_values(x) { return (''+x).replace(/ +$/, "").replace(/^ +/, ""); } /** Chop too long values to specified limit */ function chop_long_values(limit) { if(limit-3 < 1) { throw new TypeError("limit must be at least four (4) characters!"); } return function(x) { x = ''+x; if(x.length > limit) { return x.substr(0, limit-3) + '...'; } return x; }; } /** Replace full path names */ function chop_long_paths(str) { str = ''+str; str = str.replace(/(\/[^/:\)\(]+)+/gi, function(path) { if(FS && is.func(FS.existsSync) && FS.existsSync(path)) { return print_path(path); } return path; }); return str; } /** Helper function that can be called but does nothing */ function do_nothing() { } /** Get timestamp */ function get_timestamp() { function dd(x) { x = ''+x; return (x.length === 1) ? '0'+x : x; } var n = new Date(); return n.getFullYear() + '-' + dd(n.getMonth()+1) + '-' + dd(n.getDate()) + ' ' + dd(n.getHours()) + ':' + dd(n.getMinutes()) + ':' + dd(n.getSeconds()); } /** Write debug log message */ function _print_error(line) { if( (typeof debug === 'object') && (typeof debug._log_error === 'function') ) { debug._log_error( line ); return; } if( debug.defaults.use_util_error && (!disable_util) && (typeof util === 'object') && (typeof util.error === 'function') ) { util.error( 'ERROR: '+ line ); return; } if( (typeof console === 'object') && (typeof console.error === 'function') ) { console.error( line ); return; } if( debug.defaults.use_console_log && (typeof console === 'object') && (typeof console.log === 'function') ) { console.log( line ); return; } if( (typeof debug === 'object') && (typeof debug._log === 'function') ) { debug._log( line ); return; } if( (typeof debug === 'object') && (typeof debug._failover_log === 'function') ) { debug._failover_log( line ); return; } } /** Write debug log message */ function print_error(line) { try { if(ansi) { debug.defaults.cursors.error(stderr_cursor); } _print_error(line); } finally { if(ansi) { stderr_cursor.reset(); } } } /** Write warning message */ function _print_warning(line) { if( (typeof debug === 'object') && (typeof debug._log_warn === 'function') ) { debug._log_warn( line ); return; } if( debug.defaults.use_util_error && (!disable_util) && (typeof util === 'object') && (typeof util.error === 'function') ) { util.error( 'WARNING: '+ line ); return; } if( (typeof console === 'object') && (typeof console.warn === 'function') ) { console.warn( line ); return; } if( debug.defaults.use_console_log && (typeof console === 'object') && (typeof console.log === 'function') ) { console.log( line ); return; } if( (typeof debug === 'object') && (typeof debug._log === 'function') ) { debug._log( line ); return; } if( (typeof debug === 'object') && (typeof debug._failover_log === 'function') ) { debug._failover_log( line ); return; } } /** Write warning message */ function print_warning(line) { try { if(ansi) { debug.defaults.cursors.warning(stderr_cursor); } _print_warning(line); } finally { if(ansi) { stderr_cursor.reset(); } } } /** Print informative log messages */ function _print_info(line) { if( (typeof debug === 'object') && (typeof debug._log_info === 'function') ) { debug._log_info( line ); return; } if( debug.defaults.use_util_error && (!disable_util) && (typeof util === 'object') && (typeof util.error === 'function') ) { util.error( line ); return; } if( debug.defaults.use_console_info && (typeof console === 'object') && (typeof console.info === 'function') ) { console.info( line ); return; } if( debug.defaults.use_console_log && (typeof console === 'object') && (typeof console.log === 'function') ) { console.log( line ); return; } if( (typeof console === 'object') && (typeof console.warn === 'function') ) { console.warn( line ); return; } if( (typeof console === 'object') && (typeof console.error === 'function') ) { console.error( line ); return; } if( (typeof debug === 'object') && (typeof debug._log === 'function') ) { debug._log( line ); return; } if( (typeof debug === 'object') && (typeof debug._failover_log === 'function') ) { debug._failover_log( line ); return; } } /** Print informative log messages */ function print_info(line) { try { if(ansi) { debug.defaults.cursors.info(stderr_cursor); } _print_info(line); } finally { if(ansi) { stderr_cursor.reset(); } } } /** */ function _print_log(line) { if( (typeof debug === 'object') && (typeof debug._log === 'function') ) { debug._log( line ); return; } if( debug.defaults.use_util_debug && (!disable_util) && (typeof util === 'object') && (typeof util.debug === 'function') ) { util.debug( line ); return; } if( debug.defaults.use_console_log && (typeof console === 'object') && (typeof console.log === 'function') ) { console.log( line ); return; } if( (typeof console === 'object') && (typeof console.warn === 'function') ) { console.warn( line ); return; } if( (typeof console === 'object') && (typeof console.error === 'function') ) { console.error( line ); return; } if( (typeof debug === 'object') && (typeof debug._failover_log === 'function') ) { debug._failover_log( line ); return; } } /** */ function inspect_and_trim(v) { return trim_values(inspect_values(v)); } /** */ function chop_and_convert(v) { return chop_long_values(DEBUG_LINE_LIMIT)(convert_specials(v)); } /** Returns the stack property of `x` if it exists, otherwise `x` itself. */ function get_stack(x) { if(!(x && x.stack)) { return x; } var buf = ''+x.stack; var message = buf.split('\n')[0]; var extra = Object.keys(x).filter(function(key) { return (key !== 'stack') && (x[key] !== undefined); }).map(function(key) { return '> ' + key + ' = ' + inspect_and_trim(x[key]); }).join('\n'); if(message === ''+x) { return ''+x.stack + '\n' + extra; } return '' + x + '\n' + x.stack + '\n' + extra; } /** */ function failover_logger(fun) { function logger() { var args = Array.prototype.slice.call(arguments); return fun( ARRAY(args).map(get_stack).map(inspect_and_trim).join(' ') ); } return logger; } /** Writes debug log messages with timestamp, file locations, and function * names. The usage is `debug.log('foo =', foo);`. Any non-string variable will * be passed on to `util.inspect()`. */ setup_property(debug, 'log', { get: function(){ // Disable in production if( (!debug.defaults.production_enable_log) && debug.isProduction()) { return do_nothing; } var stack = debug.__stack; var timestamp = get_timestamp(); var prefix = timestamp; var line, func; if( stack && (stack.length >= 2) ) { prefix += ' ' + print_path(stack[1].getFileName()) || 'unknown'; line = stack[1].getLineNumber(); if(line) { prefix += ':' + line; } func = stack[1].getFunctionName(); if(func) { prefix += ' @' + func+'()'; } } return function () { try { if(ansi) { debug.defaults.cursors.log(stdout_cursor); } var args = Array.prototype.slice.call(arguments); _print_log( chop_long_paths(get_prefix(prefix)) + ': '); ARRAY( ARRAY(args).map(inspect_and_trim).join(' ').split("\n") ).map(chop_and_convert).forEach(function(line) { _print_log( ''+ timestamp + ' > ' + chop_long_paths(line) ); }); } finally { if(ansi) { stdout_cursor.reset(); } } }; } }, failover_logger(_print_log) ); /** Writes debug log messages with timestamp, file locations, and function * names. The usage is `debug.log('foo =', foo);`. Any non-string variable will * be passed on to `util.inspect()`. */ setup_property(debug, 'error', { get: function(){ // Disable in production //if(debug.isProduction()) { // return do_nothing; //} var stack = debug.__stack; var timestamp = get_timestamp(); var prefix = timestamp; var line, func; if(stack && (stack.length >= 2)) { prefix += ' ' + print_path(stack[1].getFileName()) || 'unknown'; line = stack[1].getLineNumber(); if(line) { prefix += ':' + line; } func = stack[1].getFunctionName(); if(func) { prefix += ' @' + func+'()'; } } return function () { var args = Array.prototype.slice.call(arguments); //var cols = []; print_error( chop_long_paths(get_prefix(prefix)) + ': ' ); ARRAY( ARRAY(args).map(get_stack).map(inspect_and_trim).join(' ').split("\n") ).map(chop_and_convert).forEach(function(line) { print_error( ''+timestamp + ' > ' + chop_long_paths(line) ); }); }; } }, failover_logger(_print_error) ); /** Writes debug log messages with timestamp, file locations, and function * names. The usage is `debug.log('foo =', foo);`. Any non-string variable will * be passed on to `util.inspect()`. */ setup_property(debug, 'warn', { get: function(){ // Disable in production //if(debug.isProduction()) { // return do_nothing; //} var stack = debug.__stack; var timestamp = get_timestamp(); var prefix = timestamp; var line, func; if(stack && (stack.length >= 2)) { prefix += ' ' + print_path(stack[1].getFileName()) || 'unknown'; line = stack[1].getLineNumber(); if(line) { prefix += ':' + line; } func = stack[1].getFunctionName(); if(func) { prefix += ' @' + func+'()'; } } return function () { var args = Array.prototype.slice.call(arguments); //var cols = []; print_warning( chop_long_paths(get_prefix(prefix)) + ': ' ); ARRAY( ARRAY(args).map(get_stack).map(inspect_and_trim).join(' ').split("\n") ).map(chop_and_convert).forEach(function(line) { print_warning( ''+timestamp + ' > ' + chop_long_paths(line) ); }); }; } }, failover_logger(_print_warning) ); /** Writes debug log messages with timestamp, file locations, and function * names. The usage is `debug.log('foo =', foo);`. Any non-string variable will * be passed on to `util.inspect()`. */ setup_property(debug, 'info', { get: function(){ // Disable in production //if(debug.isProduction()) { // return do_nothing; //} var stack = debug.__stack; var timestamp = get_timestamp(); var prefix = timestamp; var line, func; if(stack && (stack.length >= 2)) { prefix += ' ' + print_path(stack[1].getFileName()) || 'unknown'; line = stack[1].getLineNumber(); if(line) { prefix += ':' + line; } func = stack[1].getFunctionName(); if(func) { prefix += ' @' + func+'()'; } } return function () { var args = Array.prototype.slice.call(arguments); //var cols = []; print_info( chop_long_paths(get_prefix(prefix)) + ': ' ); ARRAY( ARRAY(args).map(get_stack).map(inspect_and_trim).join(' ').split("\n") ).map(chop_and_convert).forEach(function(line) { print_info( ''+ timestamp + ' > ' + chop_long_paths(line) ); }); }; } }, failover_logger(_print_info) ); function debug_assert(value) { return new NorAssert(value); } // debug_assert function dummy_assert() { return new DummyAssert(); } function assert_getter(){ return debug_assert; } // assert_getter /** Assert some things about a variable, otherwise throws an exception. */ setup_property(debug, 'assert', { get: assert_getter }, dummy_assert); // debug.assert /** Hijacks 3rd party method call to print debug information when it is called. * Use it like `debug.inspectMethod(res, 'write');` to hijack `res.write()`. */ debug.inspectMethod = function hijack_method(obj, method) { var orig = obj[method]; if(!debug.inspectMethod._id) { debug.inspectMethod._id = 0; } obj[method] = function() { var x = (debug.inspectMethod._id += 1); var args = Array.prototype.slice.call(arguments); var stack = [].concat(debug.__stack); debug.log('#' + x + ': Call to ' + method + ' (' + ARRAY(args).map(inspect_values).join(', ') + ') ...'); // FIXME: files could be printed relative to previous stack item, so it would not take that much space. debug.log('#' + x + ": stack = ", ARRAY(stack).map(function(x) { return print_path(x.getFileName()) + ':' + x.getLineNumber(); }).join(' -> ') ); var ret = FUNCTION(orig).apply(obj, args); debug.log('#' + x + ': returned: ', ret); return ret; }; }; /** */ function get_location(x, short) { var file = (x && x.getFileName && print_path(x.getFileName())) || ''; var line = (x && x.getLineNumber && x.getLineNumber()) || ''; var func = (x && x.getFunctionName && x.getFunctionName()) || ''; if(short && file) { file = PATH.basename(file); } var out = file || 'unknown'; if(line) { out += ':' + line; } if(func) { out += ' @' + func + '()'; } return out; } /** Builds a wrapper function that will print warning when an obsolete method is called * @params self {object} The `this` element of the method/function * @params obsolete_func {string} The method name that was obsolete * @params new_func {string} The new method name that should be used * @returns {function} The wrapped function that will print a warning and otherwise behave as the method would normally. */ debug.obsoleteMethod = function(self, obsolete_func, func) { if(typeof self !== 'function') { debug.assert(self).typeOf('object'); } debug.assert(obsolete_func).typeOf('string'); debug.assert(func).typeOf('string'); debug.assert(self[func]).typeOf('function'); return function() { var args = Array.prototype.slice.call(arguments); var stack = [].concat(debug.__stack); debug.log('Warning! An obsolete method .'+ obsolete_func + '() used at ' + get_location(stack[1]) + ' -- use .' + func + '() instead.' ); return FUNCTION(self[func]).apply(this, args); }; }; /* EOF */