├── test └── index.js ├── .travis.yml ├── assets ├── demo.gif └── example.js ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── src ├── request.js └── index.js /test/index.js: -------------------------------------------------------------------------------- 1 | require('../src')({}) 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pd4d10/friendly-error/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /assets/example.js: -------------------------------------------------------------------------------- 1 | // Add this at the beginning of your node.js entry file 2 | require('friendly-error')({ 3 | proxy: 'socks5://localhost:1080' 4 | }) 5 | 6 | const http = require('http') 7 | const server = http.createServer() 8 | 9 | server.listen(9000) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | lib/ 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 pd4d10 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendly-error", 3 | "version": "1.1.0", 4 | "description": "Make errors friendly in Node.js", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "rm -rf lib; babel src -d lib", 8 | "test": "node test" 9 | }, 10 | "files": [ 11 | "lib" 12 | ], 13 | "author": "pd4d10 ", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/pd4d10/friendly-error.git" 18 | }, 19 | "dependencies": { 20 | "babel-runtime": "^6.20.0", 21 | "chalk": "^1.1.3", 22 | "cheerio": "^0.22.0", 23 | "http-proxy-agent": "^1.0.0", 24 | "node-fetch": "^1.6.3", 25 | "simple-spinner": "0.0.5", 26 | "socks5-https-client": "^1.2.0" 27 | }, 28 | "devDependencies": { 29 | "babel-cli": "^6.18.0", 30 | "babel-core": "^6.21.0", 31 | "babel-plugin-transform-runtime": "^6.15.0", 32 | "babel-preset-env": "^1.1.8" 33 | }, 34 | "babel": { 35 | "presets": [ 36 | [ 37 | "env", 38 | { 39 | "targets": { 40 | "node": 4 41 | }, 42 | "exclude": [ 43 | "transform-regenerator" 44 | ] 45 | } 46 | ] 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # friendly-error 2 | 3 | Show uncaught errors friendly in Node.js. 4 | 5 | Demo 6 | 7 | ## Usage 8 | 9 | ```sh 10 | npm install --save friendly-error 11 | ``` 12 | 13 | Then add a line at the beginning of your entry file: 14 | 15 | ```js 16 | require('friendly-error')() 17 | ``` 18 | 19 | That's it. 20 | 21 | It use Google, so make sure you have network access to Google. StackOverflow's search API seems not working as expected. 22 | 23 | ## Configuration 24 | 25 | ### Proxy 26 | 27 | `friendly-error` support HTTP, HTTPS and SOCKS5 proxy. 28 | 29 | ```js 30 | // HTTP proxy 31 | // Just replace 'http://' with 'https://' to use HTTPS proxy 32 | require('friendly-error')({ 33 | proxy: 'http://localhost:8080' 34 | }) 35 | 36 | // SOCKS5 proxy 37 | require('friendly-error')({ 38 | proxy: 'socks5://localhost:1080' 39 | }) 40 | ``` 41 | 42 | ### Colors 43 | 44 | If you don't like default colors, you could change it easily. 45 | 46 | Default configuration is as follows: 47 | 48 | ```js 49 | require('friendly-error')({ 50 | errorMessage: chalk.red, // Error message 51 | errorStack: chalk.reset // Error stack 52 | breakPoint: chalk.bgWhite.black, // Break point where error happens 53 | answer: chalk.yellow // Best answer from stackoverflow 54 | link: chalk.cyan.underline // Links 55 | }) 56 | ``` 57 | 58 | Visit [chalk](https://github.com/chalk/chalk) for more information. 59 | 60 | ## License 61 | 62 | MIT 63 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | const qs = require('querystring') 3 | const fetch = require('node-fetch') 4 | const cheerio = require('cheerio') 5 | const Socks5Agent = require('socks5-https-client/lib/Agent') 6 | const HttpAgent = require('http-proxy-agent') 7 | 8 | function getAgentFromProxy(proxy) { 9 | // No agent 10 | if (typeof proxy === 'undefined') { 11 | return null 12 | } 13 | 14 | if (typeof proxy !== 'string') { 15 | throw new Error('Proxy must be a string, please check it') 16 | } 17 | 18 | const { protocol, hostname, port } = url.parse(proxy) 19 | 20 | switch (protocol) { 21 | case 'http:': 22 | case 'https:': 23 | return new HttpAgent(proxy) 24 | case 'socks5:': 25 | return new Socks5Agent({ 26 | socksHost: hostname, 27 | socksPort: port, 28 | }) 29 | default: 30 | throw new Error('Proxy not valid, please check it') 31 | } 32 | } 33 | 34 | async function requestGoogle(message, proxy) { 35 | const keyword = encodeURIComponent(`${message} site:stackoverflow.com`) 36 | const res = await fetch(`https://www.google.com/search?q=${keyword}`, { 37 | agent: getAgentFromProxy(proxy), 38 | }) 39 | const text = await res.text() 40 | const $ = cheerio.load(text) 41 | 42 | return $('h3.r>a').map(function () { 43 | const $this = $(this) 44 | 45 | return { 46 | title: $this.text(), 47 | href: $this.attr('href'), 48 | } 49 | }).get() 50 | } 51 | 52 | async function requestStackoverflow(id, proxy) { 53 | const params = qs.stringify({ 54 | // Votes desc 55 | order: 'desc', 56 | sort: 'votes', 57 | site: 'stackoverflow', 58 | // Filter field is generated from https://api.stackexchange.com/docs/answers-on-questions 59 | // query only `body` and `is_accepted` field 60 | filter: '!bGqd9*)(j28_aP', 61 | }) 62 | 63 | const res = await fetch(`https://api.stackexchange.com/2.2/questions/${id}/answers?${params}`, { 64 | agent: getAgentFromProxy(proxy), 65 | }) 66 | const { items } = await res.json() 67 | 68 | // No answer 69 | if (items.length === 0) { 70 | return '' 71 | } 72 | 73 | const acceptedAnswers = items.filter(item => item.is_accepted) 74 | 75 | // If no answer is accepted, use the most votes 76 | if (acceptedAnswers.length === 0) { 77 | return items[0].body 78 | } 79 | 80 | return acceptedAnswers[0].body 81 | } 82 | 83 | exports.requestGoogle = requestGoogle 84 | exports.requestStackoverflow = requestStackoverflow 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const cheerio = require('cheerio') 3 | const spinner = require('simple-spinner') 4 | 5 | const { requestGoogle, requestStackoverflow } = require('./request') 6 | 7 | // Simple wrapper for `console.log` 8 | function log(content, beautify) { 9 | if (typeof beautify === 'function') { 10 | return console.log(beautify(content)) 11 | } 12 | return console.log(content) 13 | } 14 | 15 | module.exports = ({ 16 | errorMessage = chalk.red, 17 | errorStack = chalk.reset, 18 | breakPoint = chalk.bgWhite.black, 19 | answer = chalk.yellow, 20 | link = chalk.cyan.underline, 21 | proxy, 22 | } = {}) => { 23 | async function handleError(err) { 24 | try { 25 | log(err.stack, (stack) => errorStack(stack 26 | .replace(/^.*?\n/, str => errorMessage(str)) 27 | .replace(/\(.*?:\d+:\d+\)/g, str => breakPoint(str)) 28 | )) 29 | log('') 30 | spinner.start() 31 | 32 | const res = await requestGoogle(err.message, proxy) 33 | spinner.stop() 34 | 35 | let isRequested = false 36 | for ({ title, href } of res) { 37 | if (isRequested) { 38 | return 39 | } 40 | 41 | const questionId = href.replace(/.*stackoverflow.com\/questions\/(\d+)\/.*/, '$1') 42 | const answerContent = await requestStackoverflow(questionId, proxy) 43 | 44 | // If no answer, try next link 45 | if (answerContent === '') { 46 | continue 47 | } 48 | 49 | const formatedAnswerContent = cheerio.load(answerContent).text() 50 | log(formatedAnswerContent, answer) 51 | isRequested = true 52 | 53 | log('For more answers:') 54 | // Sometimes google's results url is not the original url, like 'url?xxx' 55 | // This `replace` is to extract url from it. 56 | log(href.replace(/.*(http:\/\/stackoverflow.com\/questions\/\d+\/[^&]+).*/, '$1'), link) 57 | 58 | const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(err.message)}` 59 | log('For more search results:') 60 | log(googleUrl, link) 61 | } 62 | } catch (err) { 63 | spinner.stop() 64 | log('There appears to be some trouble, aborted.') 65 | log('If you think this is a bug, please click url as follows to report issue:') 66 | log(chalk.cyan(`https://github.com/pd4d10/friendly-error/issues/new?title=${encodeURIComponent(err.message)}&body=${encodeURIComponent(err.stack)}`)) 67 | } 68 | } 69 | 70 | process.on('uncaughtException', handleError) 71 | } 72 | --------------------------------------------------------------------------------