├── .gitignore ├── Readme.md ├── cmd.js ├── examples ├── arrow-function.js ├── bad-line.js ├── default.js ├── json.js ├── modules-throw-table.js ├── modules-throw.js ├── no-module.js ├── no-stack.js ├── pretty-json.js ├── pretty.js ├── table.js └── tree.js ├── images ├── cute.gif ├── filter.png ├── pretty.png ├── table.png └── tree.png ├── index.js ├── package.json ├── postinstall.js ├── set-flags.js └── usage.txt /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Cute-stack 2 | 3 | ## Cute up your Node stack traces 4 | 5 | 6 | ```sh 7 | 8 | npm install -g cute-stack # cli usage 9 | npm install cute-stack --save # programmatic usage 10 | 11 | ``` 12 | 13 | ![demo](images/cute.gif) 14 | 15 | ## Usage 16 | 17 | ### CLI 18 | 19 | New in version 1.3.x cute-stack has an executable, named `cute`. 20 | Simply use `cute` instead of `node` to get cute-stack traces: 21 | 22 | ```sh 23 | cute myApp.js 24 | ``` 25 | 26 | ### Programmatic 27 | 28 | Require, then call, `cute-stack` in your entry point file 29 | 30 | ```javascript 31 | require('cute-stack')(); 32 | //...all your app code 33 | ``` 34 | 35 | ## File Path Transforms 36 | 37 | cute-stack converts paths in some cases. 38 | 39 | Any frames that contain paths matching the 40 | current working directory, those parts of 41 | the paths matching the CWD will be replaced with `./`. 42 | For instance, if the CWD is `/user/dave/projects/`, 43 | and the file that's caused a trace is 44 | `/user/dave/projects/foo/bar.js`, the path will be 45 | rewritten as `./foo/bar.js`. 46 | 47 | The other, more esoteric transform concerns dependencies. 48 | If a dependency causes a stack trace, it might 49 | look something like `./node_modules/foo/node_modules/bar/node_modules/baz/index.js`. Quite lengthy. So `cute-stack` replaces node_modules 50 | directories with a diamond character, so it becomes 51 | `.♦foo♦bar♦baz/index.js`. 52 | 53 | ## API 54 | 55 | ### Type 56 | ```javascript 57 | require('cute-stack')(type) 58 | ``` 59 | 60 | #### Supported types 61 | 62 | * pretty (default) 63 | * table 64 | * json 65 | * json-pretty 66 | * tree 67 | 68 | 69 | ![pretty](images/pretty.png) 70 | ![table](images/table.png) 71 | ![tree](images/tree.png) 72 | 73 | 74 | ### stackSize 75 | 76 | ```javascript 77 | require('cute-stack')(stackSize) 78 | require('cute-stack')(type, stackSize) 79 | ``` 80 | 81 | The `stackSize` parameter can be used to alter 82 | how many frames a stack trace generates. 83 | 84 | The default is 10, setting this to a high number 85 | could have an impact on performance. 86 | 87 | During development this could be set to `Infinity` 88 | for unlimited stack traces 89 | 90 | ```javascript 91 | require('cute-stack')('pretty', Infinity) 92 | ``` 93 | 94 | ### uncute 95 | 96 | ```javascript 97 | var cute = require('cute-stack')(); 98 | cute.uncute(); 99 | ``` 100 | Removes alterations to stack traces 101 | 102 | 103 | ### noStack 104 | ```javascript 105 | require('cute-stack').noStack(); 106 | ``` 107 | 108 | Disables stack traces 109 | 110 | ## Plugins 111 | 112 | ```javascript 113 | var cute = require('cute-stack'); 114 | 115 | function myPlugin(frame) { 116 | return 'result of processing a stack frame' 117 | } 118 | cute.ui.myPlugin = myPlugin; 119 | cute('myPlugin') 120 | ``` 121 | 122 | Add a method to `cute.ui` to create a new plugin. 123 | The method will be given each frame, and should 124 | return something based on that frame. 125 | 126 | ### init and print 127 | ```javascript 128 | var cute = require('cute-stack'); 129 | 130 | function myPlugin(frame) { 131 | return 'result of processing a stack frame' 132 | } 133 | myPlugin.init = function () { 134 | //do some initialisation 135 | } 136 | myPlugin.print = function (stack) { 137 | return '\n=====\n' + stack.join('\n') + '\n=====\n' 138 | } 139 | cute.ui.myPlugin = myPlugin; 140 | 141 | cute('myPlugin') 142 | ``` 143 | 144 | If a plugin has an `init` method attached to it, 145 | this will be called prior to processing all frames 146 | 147 | If it has a `print` method attached this will be 148 | used to display all frames once they have been 149 | processed. 150 | 151 | ## Plugin function 152 | 153 | Instead of defining the plugin and passing its name, you can pass the plugin function 154 | directly. 155 | 156 | ```js 157 | function myPlugin(frame) { 158 | return 'result of processing a stack frame' 159 | } 160 | myPlugin.init = function () { 161 | //do some initialisation 162 | } 163 | myPlugin.print = function (stack) { 164 | return '\n=====\n' + stack.join('\n') + '\n=====\n' 165 | } 166 | 167 | require('cute-stack')(myPlugin); 168 | ``` 169 | 170 | See a plugin example [bad-line](https://github.com/bahmutov/bad-line) that prints 171 | actual source line where the crash happens for each local file in the stack. 172 | 173 | ## Filtering stack frames 174 | 175 | You can limit printed stack frames using `plugin.filter` method. Just return truthy value 176 | for frames to be included in the stack. For example to skip printing `module.js` calls 177 | when printing using default formatter 178 | 179 | ```js 180 | var cute = require('cute-stack'); 181 | cute.ui.default.filter = function isNotModuleJs(frame) { 182 | return !/^module\.js$/.test(frame.file); 183 | } 184 | cute(); 185 | throw x; 186 | ``` 187 | 188 | ![no modules](images/filter.png) 189 | 190 | ## Kudos 191 | 192 | ### Collaborators 193 | 194 | * Gleb Bahmutov (@bahmutov) 195 | 196 | ### Sponsorship 197 | 198 | Sponsored by nearForm 199 | -------------------------------------------------------------------------------- /cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.argv.splice(1, 2, process.argv[2]); 4 | var argv = require('minimist')( 5 | process.argv.slice(1).filter(Boolean), { 6 | alias: { 7 | t: 'type', 8 | s: ['stacksize', 'stackSize'] 9 | } 10 | } 11 | ); 12 | 13 | var fs = require('fs'); 14 | var path = require('path'); 15 | var Module = require('module'); 16 | var spawn = require('child_process').spawn; 17 | var setFlagsFromString = require('./set-flags'); 18 | var pack = require('./package.json'); 19 | 20 | var type = argv.type; 21 | var stackSize = argv.stacksize; 22 | strip('-t'); 23 | strip('--type'); 24 | strip('-s'); 25 | strip('-stacksize'); 26 | strip('-stackSize'); 27 | 28 | function strip(flag) { 29 | var n = 2; 30 | var i = process.argv.indexOf(flag); 31 | if (i < 0) return 32 | if (process.argv[i + 1][0] === '-') n = 1 33 | process.argv.splice(i, n); 34 | } 35 | 36 | function delegate() { 37 | return spawn('node', process.argv 38 | .slice(1) 39 | .filter(Boolean), { 40 | stdio: 'inherit', 41 | env: process.env, 42 | cwd: process.cwd() 43 | }) 44 | } 45 | 46 | function prefixEval(letter) { 47 | argv[letter] = 'require("' + __dirname + '/")("'+type+'",' + stackSize + ');\n' + argv[letter]; 48 | process.argv[process.argv.indexOf('-'+letter)+1] = argv[letter]; 49 | } 50 | 51 | function evalArg(arg) { 52 | prefixEval(arg); 53 | delegate(); 54 | } 55 | 56 | function version () { 57 | process.stdout.write('Cute: ' + pack.version); 58 | process.stdout.write('\nNode: ') 59 | delegate(); 60 | } 61 | 62 | function help() { 63 | delegate(); 64 | setTimeout(function () { 65 | console.log('\n=============================\n') 66 | fs.createReadStream(path.join(__dirname, 'usage.txt')).pipe(process.stdout); 67 | }, 100); 68 | } 69 | 70 | function unsupported(flag) { 71 | console.log('--' + flag + ' is currently unsupported. PR\'s welcome!'); 72 | } 73 | 74 | function load() { 75 | setFlagsFromString(process.argv.slice(2).join(' ')); 76 | require('./')(type, stackSize, {emulated: true}); 77 | 78 | process.argv[1] = require.resolve(path.resolve(process.cwd(), process.argv[1])); 79 | 80 | Module.runMain(); 81 | } 82 | 83 | switch (true) { 84 | 85 | default: return load(); 86 | 87 | case 88 | !! 89 | argv.p: return evalArg('p'); 90 | 91 | case 92 | !! 93 | argv.e: return evalArg('e'); 94 | 95 | case 96 | !! 97 | (argv.v || argv.version): return version(); 98 | 99 | case 100 | !! 101 | (argv.help || argv.h): return help(); 102 | 103 | case 104 | !! 105 | argv['no-deprecation']: 106 | return unsupported('no-deprecation'); 107 | 108 | case 109 | !! 110 | argv['trace-deprecation']: 111 | return unsupported('trace-deprecation'); 112 | 113 | case 114 | !! 115 | argv['max-stack-size']: 116 | return unsupported('max-stack-size'); 117 | 118 | case 119 | !! 120 | argv['enable-ssl2']: 121 | return unsupported('enable-ssl2'); 122 | 123 | case 124 | !! 125 | argv['enable-ssl3']: 126 | return unsupported('enable-ssl3'); 127 | 128 | case 129 | !! 130 | (argv.i || argv['v8-options'] || !argv._.length): 131 | return delegate(); 132 | } 133 | 134 | -------------------------------------------------------------------------------- /examples/arrow-function.js: -------------------------------------------------------------------------------- 1 | require('../')() 2 | 3 | var l = (fn) => fn() 4 | 5 | l(()=>{ throw Error() }) 6 | 7 | -------------------------------------------------------------------------------- /examples/bad-line.js: -------------------------------------------------------------------------------- 1 | require('../')(require('bad-line')); 2 | function foo() { 3 | throw new Error('bad error in foo'); 4 | } 5 | foo(); 6 | -------------------------------------------------------------------------------- /examples/default.js: -------------------------------------------------------------------------------- 1 | require('../')() 2 | throw x; -------------------------------------------------------------------------------- /examples/json.js: -------------------------------------------------------------------------------- 1 | require('../')('json') 2 | throw x; -------------------------------------------------------------------------------- /examples/modules-throw-table.js: -------------------------------------------------------------------------------- 1 | require('../')('table', 50) 2 | 3 | var mkdirp = require('mkdirp'); 4 | var fs = require('fs'); 5 | var foo = 'node_modules/foo/' 6 | var bar = 'node_modules/bar/' 7 | var baz = 'node_modules/baz/' 8 | var foobar = foo+bar; 9 | var foobarbaz = foobar+baz; 10 | 11 | mkdirp.sync(foobarbaz); 12 | 13 | fs.writeFileSync(foo + '/index.js', 'require("bar")'); 14 | fs.writeFileSync(foobar + '/index.js', 'require("baz")'); 15 | fs.writeFileSync(foobarbaz + '/index.js', 'throw x'); 16 | 17 | require('foo'); 18 | 19 | -------------------------------------------------------------------------------- /examples/modules-throw.js: -------------------------------------------------------------------------------- 1 | require('../')('pretty', 50) 2 | 3 | var mkdirp = require('mkdirp'); 4 | var fs = require('fs'); 5 | var foo = 'node_modules/foo/' 6 | var bar = 'node_modules/bar/' 7 | var baz = 'node_modules/baz/' 8 | var foobar = foo+bar; 9 | var foobarbaz = foobar+baz; 10 | 11 | mkdirp.sync(foobarbaz); 12 | 13 | fs.writeFileSync(foo + '/index.js', 'require("bar")'); 14 | fs.writeFileSync(foobar + '/index.js', 'require("baz")'); 15 | fs.writeFileSync(foobarbaz + '/index.js', 'throw x'); 16 | 17 | require('foo'); 18 | 19 | -------------------------------------------------------------------------------- /examples/no-module.js: -------------------------------------------------------------------------------- 1 | var cute = require('../'); 2 | 3 | cute.ui.default.filter = function isNotModuleJs(frame) { 4 | return !/^module\.js$/.test(frame.file); 5 | } 6 | 7 | cute(); 8 | throw x; 9 | -------------------------------------------------------------------------------- /examples/no-stack.js: -------------------------------------------------------------------------------- 1 | require('../').noStack(); 2 | throw x; -------------------------------------------------------------------------------- /examples/pretty-json.js: -------------------------------------------------------------------------------- 1 | require('../')('pretty-json') 2 | throw x; -------------------------------------------------------------------------------- /examples/pretty.js: -------------------------------------------------------------------------------- 1 | require('../')('pretty') 2 | throw x; -------------------------------------------------------------------------------- /examples/table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('../')('table') 4 | throw x; -------------------------------------------------------------------------------- /examples/tree.js: -------------------------------------------------------------------------------- 1 | require('../')('tree', 4); 2 | throw x; 3 | -------------------------------------------------------------------------------- /images/cute.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidmarkclements/cute-stack/7975b60f6f17b8bf82666b17ec88daec5c945276/images/cute.gif -------------------------------------------------------------------------------- /images/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidmarkclements/cute-stack/7975b60f6f17b8bf82666b17ec88daec5c945276/images/filter.png -------------------------------------------------------------------------------- /images/pretty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidmarkclements/cute-stack/7975b60f6f17b8bf82666b17ec88daec5c945276/images/pretty.png -------------------------------------------------------------------------------- /images/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidmarkclements/cute-stack/7975b60f6f17b8bf82666b17ec88daec5c945276/images/table.png -------------------------------------------------------------------------------- /images/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidmarkclements/cute-stack/7975b60f6f17b8bf82666b17ec88daec5c945276/images/tree.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var colors = require('colors'); 3 | var Table = require('cli-table'); 4 | var treeify = require('treeify'); 5 | 6 | module.exports = cute; 7 | 8 | cute.uncute = function () { 9 | Error.stackTraceLimit = 10; 10 | Error.prepareStackTrace = null; 11 | } 12 | cute.noStack = function () { cute(null, 0); } 13 | 14 | cute.ui = { 15 | default: pretty, 16 | pretty: pretty, 17 | table: table, 18 | json: JSON.stringify.bind(JSON), 19 | tree: tree 20 | } 21 | 22 | cute.ui['pretty-json'] = function (frame) { 23 | return JSON.stringify(frame, 0, 2); 24 | } 25 | 26 | cute.ui['pretty-json'].print = cute.ui.json.print = function (data) { 27 | return '['+data+']' 28 | } 29 | 30 | function cute(type, stackSize, opts) { 31 | opts = opts || {} 32 | if (typeof type === 'number') { 33 | stackSize = type; 34 | type = null; 35 | } 36 | if (typeof stackSize === 'number') { 37 | Error.stackTraceLimit = stackSize; 38 | } 39 | if (opts.emulated) { 40 | Error.stackTraceLimit += 7; 41 | } 42 | if (typeof type !== 'function') { 43 | type = cute.ui[type] || cute.ui.default; 44 | } 45 | if (type.init) { type.init(); } 46 | 47 | var filter = type.filter ? type.filter : function (x) { return x; }; 48 | 49 | function details(frame) { 50 | var fn = frame.getFunction(); 51 | var sig = fn ? 52 | ((fn+'').split('{')[0].trim() + ' { [body] }') : 53 | findSig(frame); 54 | 55 | return { 56 | file: frame.getFileName() 57 | .replace(process.cwd(), '.') 58 | .replace(/\/node_modules\//g, '♦'), 59 | line: frame.getLineNumber(), 60 | column: frame.getColumnNumber(), 61 | name: frame.getFunctionName(), 62 | meth: frame.getMethodName(), 63 | sig: sig, 64 | id: function () { 65 | return this.name || this.meth || this.sig; 66 | } 67 | } 68 | } 69 | 70 | // https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi 71 | Error.prepareStackTrace = function (error, stack) { 72 | if (opts.emulated) { stack = strip(stack); } 73 | 74 | var text = (error+'').bgRed.white 75 | + '\n\n' + (type.print||join)( 76 | stack.map(details).filter(filter).map(type) 77 | ); 78 | 79 | return text; 80 | } 81 | return cute; 82 | } 83 | 84 | function pretty(frame) { 85 | return [ 86 | frame.file.cyan, 87 | (' ' + frame.line + ',' + frame.column + ' ').bgYellow.black, 88 | (' ' + (frame.id()) + ' ').gray 89 | ].join(' ') + '\n'; 90 | } 91 | 92 | function join(a) { 93 | return Array.prototype.join.call(a, ''); 94 | } 95 | 96 | table.init = function () { 97 | 98 | table._ = new Table({ 99 | head: ['file', 'line', 'col', 'name/sig'], 100 | colWidths: [20, 8, 6, 46] 101 | 102 | }) 103 | 104 | 105 | } 106 | 107 | table.print = function () { 108 | var t = table._ + ''; 109 | table._ = null; 110 | return t; 111 | } 112 | 113 | function table(frame) { 114 | var file = (frame.file.length > 18) ? 115 | '…' + frame.file.substring(-16) : 116 | frame.file; 117 | 118 | table._.push([ 119 | file, frame.line, frame.column, frame.id() 120 | ]); 121 | } 122 | 123 | function tree(frame) { 124 | return { 125 | file: frame.file, 126 | line: frame.line, 127 | column: frame.column, 128 | id: frame.id() 129 | }; 130 | } 131 | 132 | tree.print = function treePrint(frames) { 133 | if (!frames.length) { 134 | return; 135 | } 136 | 137 | frames.reduce(function (prev, curr) { 138 | if (prev !== curr) { 139 | prev.caller = curr; 140 | } 141 | return curr; 142 | }, frames[0]); 143 | 144 | return treeify.asTree(frames[0], true); 145 | }; 146 | 147 | function findSig(frame) { 148 | var path = require('path'); 149 | var mod = require('module'); 150 | var name = frame.getFileName(); 151 | var line = frame.getLineNumber(); 152 | var col = frame.getColumnNumber(); 153 | var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; 154 | var sig = ''; 155 | mod.wrap = (function (wrap) { 156 | return function (script) { 157 | script = mod.wrapper[0] + script + mod.wrapper[1]; 158 | 159 | var lines = script.split('\n'); 160 | 161 | var match = lines[line] 162 | .substr(0, col) 163 | .replace(comments, '') 164 | .match(/function.+{/); 165 | 166 | if (match) { 167 | sig = match[0] + ' [body] }'; 168 | return; 169 | } 170 | 171 | lines.slice(0, line).reverse().some(function (l, ix) { 172 | var match = l 173 | .replace(comments, '') 174 | .match(/function\s*\(.+\)/m); 175 | 176 | if (match) { 177 | sig = lines[lines.indexOf(l)] 178 | .replace(comments, '') 179 | .match(/function.+{/)[0] + ' [body] }'; 180 | } 181 | return match; 182 | }); 183 | 184 | mod.wrap = wrap; 185 | return script; 186 | }; 187 | }(mod.wrap)); 188 | 189 | try { 190 | if (path.join(__dirname + '/cmd.js') !== name) { 191 | require(require.resolve(name.replace(path.extname(name), ''))); 192 | } 193 | 194 | } catch (e) {} 195 | 196 | return sig; 197 | } 198 | 199 | function strip(stack) { 200 | var loadfn = stack.filter(function (frame) { 201 | return /cmd\.js/.test(frame.getFileName()) && frame.getFunctionName() === 'load' 202 | })[0]; 203 | var startFrame = stack.indexOf(loadfn); 204 | var endFrame = stack.indexOf(stack.slice(startFrame, stack.length).filter(function (frame) { 205 | return /module\.js/.test(frame.getFileName()) && frame.getFunctionName() === 'Module.runMain' 206 | })[0]); 207 | return stack.slice(0, startFrame).concat(stack.slice(endFrame, stack.length)) 208 | } 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cute-stack", 3 | "version": "1.4.3", 4 | "bin": { 5 | "cute": "./cmd.js" 6 | }, 7 | "description": "", 8 | "main": "index.js", 9 | "scripts": { 10 | "postinstall": "node ./postinstall.js" 11 | }, 12 | "author": "David Mark Clements", 13 | "license": "MIT", 14 | "dependencies": { 15 | "cli-table": "^0.3.1", 16 | "colors": "^1.0.3", 17 | "minimist": "^1.1.0", 18 | "treeify": "1.0.1" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/davidmarkclements/cute-stack.git" 23 | }, 24 | "devDependencies": { 25 | "bad-line": "0.1.0", 26 | "mkdirp": "^0.5.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | 3 | if (!+process.versions.node[0]) { 4 | console.log('\u001b[33minstall setflags on 0.10 and 0.12\u001b[0m'); 5 | var npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; 6 | spawn(npm, ['i', 'setflags'], {stdio: 'inherit'}); 7 | } 8 | -------------------------------------------------------------------------------- /set-flags.js: -------------------------------------------------------------------------------- 1 | module.exports = !+process.versions.node[0] ? 2 | require('setflags').setFlags : 3 | require('v8').setFlagsFromString 4 | 5 | 6 | -------------------------------------------------------------------------------- /usage.txt: -------------------------------------------------------------------------------- 1 | cute [-hts] [--help] [--type] [--stacksize] [file] 2 | 3 | -h | --help Display usage 4 | -t | --type Set display type, default pretty see #Types 5 | -s | --stacksize Set stack size, default 10 6 | 7 | # Types 8 | 9 | * pretty (default) 10 | * table 11 | * json 12 | * pretty-json 13 | --------------------------------------------------------------------------------